Mert Özen Her Satırda Daha İleri
Backend C# Temelleri — #15

C# Enum: Underlying Type, [Flags], Parse ve switch

Mert Özen 7 Kas 2026 9 dk 2 görüntülenme
C# Enum: Underlying Type, [Flags], Parse ve switch

Enum'ın altındaki sayı: neden (OrderStatus)7 geçerli bir değer, default'un 0 olması, değerleri elle vermek, Parse ve TryParse'ın sürprizleri, switch expression'da CS8524 uyarısı ve [Flags] ile birleşebilen seçenekler.

Sürüm notu: Bu yazıdaki kodlar C# 10 ve .NET 6 API'leriyle yazılmıştır; .NET 10 SDK ile derlenip denenmiştir.

Önce Tahmin Et

Bookstore uygulamamızda siparişin durumunu tutan beş değerlik bir enum var. Aşağıdaki kod derleniyor ve hiçbir uyarı vermiyor. Üç satır ne yazdırır? İlk satır bir exception mı fırlatır, boş mu kalır, yoksa bir şey mi yazar?

OrderStatus status = (OrderStatus)7;
Console.WriteLine(status);

bool ok = Enum.TryParse("3", out OrderStatus parsed);
Console.WriteLine(ok);
Console.WriteLine(parsed);

enum OrderStatus
{
    Pending,
    Paid,
    Shipped,
    Delivered,
    Cancelled
}

Enum Bir İsim Listesi, Altında Bir Sayı

Çıktı şu:

7
True
Delivered

İlk satır exception fırlatmadı, çünkü enum'ın altında düz bir tamsayı yatıyor. OrderStatus tanımında 7 diye bir member yok ama OrderStatus tipindeki bir değişken 7 değerini gayet rahat taşıyabiliyor; ToString() karşılık gelen bir isim bulamayınca sayının kendisini yazıyor. İkinci satırda da TryParse "3" metnini isim olarak değil sayı olarak okudu ve 3 numaralı member'ı, yani Delivered'ı verdi.

Enum'ı bir otoparkın numaralı yerleri gibi düşün. Tabelada "Pending", "Paid" gibi isimler yazıyor, ama asfaltta sadece numaralar boyalı. Tabelası olmayan 7 numaraya da araba park edebilirsin; kimse seni durdurmaz. Enum'ın sana verdiği şey, sihirli sayılar yerine okunabilir isimler ve derleyicinin tip kontrolü. Verdiği garanti ise sandığından daha zayıf.

Varsayılan olarak ilk member 0, sonraki her member bir fazlası olur. Member ile sayı arasında iki yönde de cast yapabilirsin:

OrderStatus status = OrderStatus.Shipped;

Console.WriteLine(status);                 // Shipped
Console.WriteLine((int)status);            // 2
Console.WriteLine(status.ToString("D"));   // 2
Console.WriteLine(OrderStatus.Paid < OrderStatus.Delivered); // True

foreach (OrderStatus s in Enum.GetValues<OrderStatus>())
{
    Console.Write($"{s}={(int)s} ");
}
// Pending=0 Paid=1 Shipped=2 Delivered=3 Cancelled=4

Enum bir value type'tır; atadığında kopyalanır, == ile karşılaştırdığında sayılar karşılaştırılır. < karşılaştırması da sayıya göre çalışır, bu yüzden Paid < Delivered doğru çıkıyor. Enum.GetValues<T>() generic hâliyle .NET 5'ten beri var ve bütün member'ları tanım sırasıyla dolaşmanı sağlar; bir açılır liste doldururken işine yarar.

Underlying Type ve Değerleri Kendin Vermek

Enum'ın altındaki tipe underlying type denir ve varsayılanı int'tir. İstersen byte, short, long gibi başka bir tamsayı tipi seçebilir, değerleri de tek tek kendin verebilirsin:

Console.WriteLine(Enum.GetUnderlyingType(typeof(Priority))); // System.Byte
Console.WriteLine(sizeof(Priority));                         // 1
Console.WriteLine((int)ShippingMethod.Express);              // 2

enum Priority : byte
{
    Low = 1,
    Normal = 5,
    High = 10
}

enum ShippingMethod
{
    Standard = 1,
    Express = 2,
    SameDay = 3
}

Underlying type'ı değiştirmeye nadiren ihtiyaç duyarsın. Milyonlarca kaydın tutulduğu bir struct içinde bellek kazanmak ya da bir dosya formatına veya donanım protokolüne birebir uymak gibi özel durumlar dışında int kalsın.

Değerleri açıkça yazmak ise gerçek hayatta çok daha önemli. Enum değeri bir veritabanı kolonuna, bir JSON'a ya da başka bir servise sayı olarak gittiği anda o sayılar bir contract'a dönüşür. Birisi listenin ortasına yeni bir member eklerse, numaraları otomatik verilen enum'da sonraki bütün member'lar bir kayar; dün Shipped olarak kaydedilen sipariş bugün Delivered görünür. Dışarıya çıkan her enum'da değerleri elle yaz ve yeni member'ı her zaman sona ekle. EF Core serisinde enum'ları veritabanına sayı ya da metin olarak kaydetmeyi ayrıca konuşuyoruz.

default Değeri ve Sıfırın Önemi

Her value type gibi enum'ın da bir default değeri var ve bu değer her zaman 0'dır. Tanımında 0 olsun olmasın:

OrderStatus emptyStatus = default;
ShippingMethod emptyMethod = default;

Console.WriteLine(emptyStatus); // Pending
Console.WriteLine(emptyMethod); // 0

ShippingMethod'da 0'a karşılık gelen bir isim olmadığı için ekrana sadece 0 düştü. Bu, bir array'i yeni oluşturduğunda, bir field'ı hiç atamadığında ya da bir JSON'da field eksik geldiğinde karşına çıkan değerdir. OrderStatus'ta durum daha sinsi: 0 değeri Pending'e denk geldiği için "unutulmuş" bir değer gerçek bir durum gibi görünür.

Benim tercihim, anlamlı bir başlangıcı olmayan enum'lara 0 değerli bir Unknown ya da None member'ı koymak. Böylece atanmamış bir değeri kodda açıkça yakalayabilirsin. Microsoft'un tasarım kılavuzu da her enum'da 0'a karşılık gelen bir member olmasını öneriyor.

String'den Enum'a: Parse ve TryParse

Kullanıcıdan, bir dosyadan ya da bir query string'den gelen metni enum'a çevirmek için Enum.Parse ve Enum.TryParse var. Kurallarını bilmezsen ikisi de seni şaşırtabilir:

Console.WriteLine(Enum.Parse<OrderStatus>("paid", ignoreCase: true)); // Paid

Console.WriteLine(Enum.TryParse("paid", out OrderStatus a));   // False: büyük/küçük harfe duyarlı
Console.WriteLine(Enum.TryParse(" Paid ", out OrderStatus b)); // True: baştaki ve sondaki boşluk atılır
Console.WriteLine(b);                                          // Paid
Console.WriteLine(Enum.TryParse("42", out OrderStatus c));     // True: sayı da kabul edilir
Console.WriteLine(c);                                          // 42
Console.WriteLine(Enum.TryParse("Pending, Paid", out OrderStatus d)); // True: virgüllü liste OR'lanır
Console.WriteLine(d);                                          // Paid

Enum.Parse<OrderStatus>("Lost");
// Unhandled exception. System.ArgumentException: Requested value 'Lost' was not found.

Varsayılan karşılaştırma büyük/küçük harfe duyarlı; ignoreCase: true vermezsen "paid" bulunamaz. Asıl tehlikeli olan sondaki iki satır: TryParse sayısal bir metni, enum'da öyle bir member olup olmadığına bakmadan kabul ediyor. Virgülle ayrılmış isimleri de bit bit OR'layıp tek değere çeviriyor; 0 ile 1'in OR'u 1 olduğu için "Pending, Paid" sessizce Paid oldu. Bu davranış aşağıda göreceğimiz [Flags] enum'lar için tasarlanmış, ama normal enum'larda da çalışıyor.

Dışarıdan gelen bir değeri enum'a çevirirken TryParse'ı Enum.IsDefined ile birlikte kullan:

foreach (string raw in new[] { "delivered", "7", "1", "Lost" })
{
    bool ok = Enum.TryParse(raw, ignoreCase: true, out OrderStatus status)
              && Enum.IsDefined(status);
    Console.WriteLine($"{raw,-10} -> {(ok ? status.ToString() : "rejected")}");
}
delivered  -> Delivered
7          -> rejected
1          -> Paid
Lost       -> rejected

7 artık reddediliyor. "1" ise hâlâ kabul ediliyor, çünkü 1 tanımlı bir değer. Sadece isim kabul etmek istiyorsan metnin bir rakamla başlamadığını ayrıca kontrol etmen gerekir. Parse'ı ise yalnızca değerin geçerli olduğundan emin olduğun yerlerde, mesela kendi yazdığın bir configuration dosyasını okurken kullan. Kullanıcı girdisinde exception fırlatan bir yol yerine TryParse tercih edilir; exception handling yazısında bu ayrımı konuşmuştuk.

Enum ve switch

Enum'ların en doğal yoldaşı switch. Koşullar yazısında gördüğümüz switch expression, her duruma karşılık gelen değeri tek bir expression'da toplar:

Console.WriteLine(Describe(OrderStatus.Shipped)); // On the way
Console.WriteLine(CanCancel(OrderStatus.Paid));   // True
Console.WriteLine(Describe((OrderStatus)9));      // ?

static string Describe(OrderStatus status) => status switch
{
    OrderStatus.Pending => "Waiting for payment",
    OrderStatus.Paid => "Preparing your order",
    OrderStatus.Shipped => "On the way",
    OrderStatus.Delivered => "Delivered",
    OrderStatus.Cancelled => "Cancelled",
};

static bool CanCancel(OrderStatus status) => status switch
{
    OrderStatus.Pending or OrderStatus.Paid => true,
    OrderStatus.Shipped => false,
};

Bu kodu derlediğimde iki farklı uyarı aldım:

Program.cs(14,53): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'OrderStatus.Delivered' is not covered.
Program.cs(5,54): warning CS8524: The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. For example, the pattern '(OrderStatus)5' is not covered.

CanCancel için gelen CS8509 haklı: Delivered ve Cancelled'ı unuttuk. Describe için gelen CS8524 ise ilk bakışta tuhaf, çünkü beş member'ın hepsini yazdık. Derleyici, yazının başında gördüğümüz gerçeği hatırlatıyor: bir OrderStatus değişkeni isimsiz bir sayı da taşıyabilir. Bu uyarıyı görmezden gelirsen üçüncü satırdaki Describe((OrderStatus)9) çağrısı runtime'da SwitchExpressionException fırlatır: Non-exhaustive switch expression failed to match its input. Mesajın altında da Unmatched value was 9. yazar.

Doğru çözüm, beklenmeyen değeri açıkça reddeden bir discard kolu eklemek:

static string Describe(OrderStatus status) => status switch
{
    OrderStatus.Pending => "Waiting for payment",
    OrderStatus.Paid => "Preparing your order",
    OrderStatus.Shipped => "On the way",
    OrderStatus.Delivered => "Delivered",
    OrderStatus.Cancelled => "Cancelled",
    _ => throw new ArgumentOutOfRangeException(nameof(status), status, "Unknown order status")
};

Artık uyarı yok ve hatalı bir değer geldiğinde mesaj ne olduğunu söylüyor: Unknown order status (Parameter 'status') ve hemen altında Actual value was 9. Buradaki ince nokta şu: _ kolunu "geri kalan her şey" anlamında normal bir durum için kullanma. Enum'a yarın yeni bir member eklendiğinde derleyicinin seni uyarabilmesi için gerçek member'ların hepsini tek tek yaz, _'yi sadece geçersiz değerlere bırak.

[Flags]: Birden Fazla Değeri Tek Değişkende Tutmak

Bir sipariş aynı anda hem "Paid" hem "Shipped" olamaz; durumlar birbirini dışlar. Ama bir kitap aynı anda hem karton kapak hem e-kitap olarak satılabilir. Böyle birleşebilen seçenekler için enum'a [Flags] attribute'u eklenir ve her member'a ikinin bir kuvveti verilir, yani her member ayrı bir bit olur:

BookFormat formats = BookFormat.Paperback | BookFormat.Ebook;

Console.WriteLine(formats);                                // Paperback, Ebook
Console.WriteLine((int)formats);                           // 5
Console.WriteLine(formats.HasFlag(BookFormat.Ebook));      // True
Console.WriteLine((formats & BookFormat.Hardcover) != 0);  // False

formats |= BookFormat.Audiobook;   // flag ekle
Console.WriteLine(formats);        // Paperback, Ebook, Audiobook

formats &= ~BookFormat.Paperback;  // flag kaldır
Console.WriteLine(formats);        // Ebook, Audiobook

Console.WriteLine((BookFormat)3);  // Print
Console.WriteLine((BookFormat)16); // 16
Console.WriteLine(Enum.Parse<BookFormat>("Hardcover, Ebook")); // Hardcover, Ebook

[Flags]
enum BookFormat
{
    None = 0,
    Paperback = 1,
    Hardcover = 2,
    Ebook = 4,
    Audiobook = 8,
    Print = Paperback | Hardcover
}

Bir bayrağı | ile eklersin, & ~ ile kaldırırsın, HasFlag ya da & ile sorgularsın. [Flags] bitleri senin yerine ayarlamaz; değerleri 1, 2, 4, 8 diye vermek senin işin. Attribute'un asıl etkisi ToString() ve Parse üzerinde: birleşik değeri "Paperback, Ebook" diye virgülle yazar ve aynı metni geri okuyabilir. Print gibi hazır birleşimler de tanımlayabilirsin; 3 değeri tam olarak Print'e denk geldiği için çıktıda "Paperback, Hardcover" yerine "Print" göründü.

15-flags-bits-tr.png

Ne zaman flags, ne zaman normal enum? Soruyu "bu değerlerden aynı anda birden fazlası doğru olabilir mi?" diye sor. Cevap evetse ve seçenek sayısı küçük ve sabitse [Flags] uygun. Değerler birbirini dışlıyorsa normal enum kullan.

Sık Düşülen Tuzaklar

Enum'ın sadece tanımlı değerleri taşıyacağını varsaymak. Cast ve TryParse her sayıyı kabul eder. Dışarıdan gelen her değeri Enum.IsDefined ile kontrol et, switch'e de bir _ => throw kolu koy. [Flags] enum'larda dikkat: Enum.IsDefined(BookFormat.Paperback | BookFormat.Ebook) false döner, çünkü 5 adında tek bir member yok. Birleşik değerleri doğrulamak için tanımlı bitlerin dışında bit kalmadığını kontrol etmen gerekir.

HasFlag(None) ile "hiç format yok mu?" diye sormak. HasFlag aslında (value & flag) == flag sorusunu sorar; flag 0 olunca bu her zaman doğrudur. Boşluğu kontrol etmek için formats == BookFormat.None yaz.

[Flags] enum'da değerleri 1, 2, 3 diye vermek. 3 hem kendi member'ı hem de 1 ile 2'nin birleşimi olur. Böyle bir enum'da A | B yazdığında ekranda "C" görürsün ve hata saatlerce gözden kaçar.

Enum değerlerini kalıcı bir yere sayı olarak yazıp sonra listenin ortasına member eklemek. Kayıtlı veri sessizce anlam değiştirir. Değerleri elle yaz, yeni member'ı sona ekle, eski member'ı silmek yerine kullanımdan kaldır.

Kullanıcıya ToString() çıktısını göstermek. "SameDay" bir kod ismidir, ekranda "Same-day delivery" ya da Türkçe arayüzde "Aynı gün teslimat" görünmeli. Enum'ı ekrandaki metne switch expression ile ya da bir resource dosyasıyla map'le; isimleri değiştirdiğin anda kullanıcı arayüzü de değişmesin.

Küçük Bir Deneme

★☆☆ OrderStatus'a Unknown = 0 member'ını ekle, diğer member'lara 1'den başlayan değerleri elle ver ve default değerin artık ne yazdırdığını gör.

★★☆ Kullanıcıdan bir sipariş durumu okuyan ve sadece isimleri kabul eden bir TryReadStatus(string? input, out OrderStatus status) fonksiyonu yaz: "7" ve "1" reddedilsin, "shipped" kabul edilsin.

★★★ BookFormat için, değerin sadece tanımlı bitlerden oluşup oluşmadığını söyleyen bir IsValidFormat(BookFormat value) fonksiyonu yaz. İpucu: bütün member'ları | ile birleştirip bir maske oluştur ve value & ~mask sonucuna bak. (BookFormat)5 geçerli, (BookFormat)16 geçersiz çıkmalı.

Enum'larla siparişin hangi durumda olduğunu tuttuk. Siparişin ne zaman verildiği, kargonun ne zaman çıkacağı ve kitapçının saat kaçta açıldığı ise başka bir dünya. Bir sonraki yazıda DateTime, DateTimeOffset, .NET 6 ile gelen DateOnly ve TimeOnly, TimeSpan ve UTC ile tarih ve saat konusunu açıyoruz.