C# Koşullar: if/else, switch ve Switch Expression
C#'ta karar vermenin yolları: if/else zincirinde sıranın önemi, süslü parantez ve scope, fall-through'a izin vermeyen switch statement, değer döndüren switch expression ve C# 9 ile gelen relational ve logical pattern'ler.
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
Kitapçımızda kargo ücretini sipariş tutarına göre belirliyoruz. 200 TL üstü kargo bedava, 100 TL üstü indirimli, geri kalanı standart. Kodu yazdık, derledik, uyarı da yok. Tutar 250 TL iken ekrana ne yazar?
decimal orderTotal = 250m;
string shipping;
if (orderTotal > 100m)
{
shipping = "Discounted";
}
else if (orderTotal > 200m)
{
shipping = "Free";
}
else
{
shipping = "Standard";
}
Console.WriteLine(shipping);
Cevap Discounted. 250, 100'den büyük olduğu için ilk koşul doğru çıkar, o blok çalışır ve zincirin geri kalanına hiç bakılmaz. else if merdiveninde koşullar yukarıdan aşağı sırayla denenir ve ilk doğru olan kazanır. Daha dar olan koşulu (> 200) üste almadıkça "Free" dalı bu kod için hiç çalışmaz. Bu yazıda C#'ta karar vermenin bütün yollarını göreceğiz: if, switch statement, switch expression ve C# 9 ile güçlenen pattern'ler.
if, else if ve else
if parantez içindeki expression'ı değerlendirir; sonuç true ise bloğu çalıştırır. else if bir önceki koşul tutmadığında denenecek yeni bir koşul ekler, else ise hiçbiri tutmadığında çalışan son kapıdır. Yukarıdaki örneğin doğru hâli koşulları en dardan en genişe dizmek: önce > 200m, sonra > 100m, en son else.
C#'ta koşul her zaman bool olmak zorunda. C, JavaScript ya da Python'dan geliyorsan sıfır olmayan sayının "doğru" sayıldığı dünyaya alışmışsındır; C# buna izin vermez:
int stock = 3;
if (stock)
{
Console.WriteLine("In stock");
}
Program.cs(2,5): error CS0029: Cannot implicitly convert type 'int' to 'bool'
Niyetini açıkça yazman gerekir: if (stock > 0). İlk bakışta fazladan yazı gibi gelir ama bu kural, "0 mı kastettin, null mı, boş string mi?" sorusunu baştan siler. Kod okuyan biri koşulun tam olarak neyi kontrol ettiğini görür.
Süslü Parantez ve Scope
Bloğun içinde tek satır varsa süslü parantezi yazmasan da kod derlenir. Sorun, ikinci satırı eklediğin gün başlar:
int stock = 0;
if (stock > 0)
Console.WriteLine("In stock");
Console.WriteLine("Ships today");
Ships today
Stok sıfır ama ekrana "Ships today" yazıldı. Girinti C# için hiçbir anlam taşımaz; süslü parantez yoksa if'e yalnızca hemen arkasındaki tek statement bağlıdır. İkinci Console.WriteLine, girintisi ne olursa olsun, her zaman çalışır. Birçok ekibin "tek satır da olsa parantez" kuralı koymasının sebebi bu; IDE'lerdeki IDE0011 kod stili kuralı da tam olarak bunu zorlamak için var.
Süslü parantez aynı zamanda bir scope açar. Blok içinde tanımladığın değişken, blok bitince yok olur:
int stock = 4;
if (stock > 0)
{
string label = stock < 5 ? "Last copies" : "In stock";
Console.WriteLine(label);
}
Console.WriteLine(label);
Program.cs(9,19): error CS0103: The name 'label' does not exist in the current context
Değişkene blok dışında da ihtiyacın varsa onu bloktan önce tanımla. Burada küçük bir koşul için ?: operatörünü de kullandık; tek bir değer seçeceksen if/else yazmaktan daha kısa ve okunaklıdır, ama iç içe üç ?: görürsen o kodu if ya da switch'e çevir.
switch Statement
Tek bir değeri birçok sabit değerle karşılaştırırken if/else if zinciri uzar ve okunması zorlaşır. switch statement bu iş için var:
string category = "novel";
switch (category)
{
case "novel":
case "poetry":
Console.WriteLine("Literature shelf");
break;
case "science":
Console.WriteLine("Science shelf");
break;
default:
Console.WriteLine("General shelf");
break;
}
Literature shelf
Alt alta yazılan case etiketleri aynı bloğu paylaşır: "novel" ve "poetry" aynı rafa gider. default, hiçbir case tutmadığında çalışır. Her bölüm break ile biter (ya da return, throw gibi akışı kesen bir statement ile).
C'den gelenlerin alıştığı "bir case'ten diğerine düşme" (fall-through) C#'ta yasak. İçinde kod olan bir case bölümünü break olmadan bırakırsan compiler durdurur:
int dayOfWeek = 6;
switch (dayOfWeek)
{
case 1:
Console.WriteLine("Monday: new arrivals");
case 6:
case 7:
Console.WriteLine("Weekend: free gift wrap");
break;
default:
Console.WriteLine("Regular day");
break;
}
Program.cs(5,5): error CS0163: Control cannot fall through from one case label ('case 1:') to another
Bu, C ve C++'ta yıllarca hataya yol açmış bir davranışın bilinçli olarak kapatılması. Gerçekten bir sonraki bölüme geçmek istiyorsan goto case 6; yazabilirsin ama bunu gerçek projede neredeyse hiç görmezsin; genelde ortak kodu bir method'a almak daha temizdir.
switch Expression
C# 8 ile gelen switch expression, switch'in değer döndüren hâli. Amacın bir değere göre başka bir değer seçmekse statement'tan çok daha kısa ve net:
string format = "ebook";
decimal shippingFee = format switch
{
"hardcover" => 4.90m,
"paperback" => 2.90m,
"ebook" => 0m,
_ => throw new ArgumentException($"Unknown format: {format}")
};
Console.WriteLine(shippingFee);
0
Değişken önde, switch arkada yazılır. Her satır bir kol (arm): solda pattern, => işaretinin sağında sonuç. Kollar virgülle ayrılır, break yoktur, expression'ın sonunda noktalı virgül vardır. _ "geri kalan her şey" demek; statement'taki default'un karşılığı. Bir kolun değer üretmek yerine exception fırlatabilmesi de mümkün, çünkü C# 7'den beri throw bir expression olarak da kullanılabiliyor.
Peki _ kolunu unutursan ne olur? Compiler bunu fark eder ama sadece uyarı verir:
string format = "audiobook";
decimal shippingFee = format switch
{
"hardcover" => 4.90m,
"paperback" => 2.90m,
"ebook" => 0m
};
Console.WriteLine(shippingFee);
Program.cs(3,30): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '""' is not covered.
Unhandled exception. System.Runtime.CompilerServices.SwitchExpressionException: Non-exhaustive switch expression failed to match its input.
Unmatched value was audiobook.
at <PrivateImplementationDetails>.ThrowSwitchExpressionException(Object unmatchedValue)
at Program.<Main>$(String[] args) in /src/Bookstore/Program.cs:line 3
Kod derlendi, ama eşleşmeyen bir değer geldiğinde runtime SwitchExpressionException fırlattı. CS8509'u hiçbir zaman görmezden gelme; ya _ kolu ekle ya da bütün durumları kapsadığından emin ol.
Pattern'ler: Type, Relational ve Logical
case ve switch expression kolunun solunda yalnızca sabit değer değil, pattern yazabilirsin. C# 9 ile gelen relational pattern'ler (<, <=, >, >=) ve logical pattern'ler (and, or, not) aralık kontrolünü çok okunaklı hâle getirir. Adet arttıkça artan bir indirim oranı:
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
foreach (int quantity in new[] { 1, 4, 12, 60 })
{
Console.WriteLine($"{quantity} -> {DiscountRate(quantity)}");
}
static decimal DiscountRate(int quantity) => quantity switch
{
<= 0 => throw new ArgumentOutOfRangeException(nameof(quantity)),
< 3 => 0m,
>= 3 and < 10 => 0.05m,
>= 10 and < 50 => 0.10m,
_ => 0.15m
};
1 -> 0
4 -> 0.05
12 -> 0.10
60 -> 0.15
Ondalık sayıların çıktısı culture'a bağlı olduğu için örneğin başında culture'ı sabitledik; Türkçe culture'da 0,05 görürdün. Kollar yine sırayla denenir, bu yüzden >= 3 kontrolü aslında gereksiz: o kola gelen bir değerin 3'ten küçük olmadığını zaten biliyoruz. Yine de aralığın iki ucunu da yazmak, kodu okuyana "bu kol tam olarak hangi aralık" sorusunun cevabını tek bakışta verir.
Type pattern ise değerin tipine bakar ve eşleşirse değeri o tipte yeni bir değişkene koyar. when ile bir kola ek koşul (case guard) ekleyebilirsin:
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
object?[] items = { 42, -1, 19.90m, "978-0441013593", "Dune", null, true };
foreach (object? item in items)
{
Console.WriteLine(Describe(item));
}
static string Describe(object? value) => value switch
{
null => "nothing",
int count when count > 0 => $"quantity {count}",
int => "invalid quantity",
decimal price => $"price {price}",
string isbn when isbn.StartsWith("978", StringComparison.Ordinal) => $"ISBN {isbn}",
string text => $"text '{text}'",
_ => $"unsupported: {value.GetType().Name}"
};
quantity 42
invalid quantity
price 19.90
ISBN 978-0441013593
text 'Dune'
nothing
unsupported: Boolean
int count when count > 0 kolu hem tipi kontrol ediyor hem de değeri count adıyla kullanılabilir kılıyor. -1 bu kolu geçemedi ve bir sonraki int koluna düştü. ISBN kontrolünde StartsWith'e StringComparison.Ordinal verdik; ISBN culture'dan bağımsız bir kod, karşılaştırmanın da öyle olması gerekir. Pattern'ler switch dışında is ile de çalışır:
char grade = 'b';
bool isLetter = grade is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
Console.WriteLine(isLetter);
string? coupon = null;
if (coupon is not null)
{
Console.WriteLine($"Coupon: {coupon}");
}
else
{
Console.WriteLine("No coupon");
}
object stock = 7;
if (stock is int units && units > 5)
{
Console.WriteLine($"Plenty: {units}");
}
True
No coupon
Plenty: 7
and, or'dan önce bağlanır; yani ilk satır "a ile z arası ya da A ile Z arası" diye okunur. is not null, null kontrolünün modern ve tavsiye edilen yazımı. switch statement da aynı pattern'leri ve when'i kabul eder:
decimal price = 120m;
bool isMember = true;
switch (price)
{
case < 0:
Console.WriteLine("Invalid price");
break;
case 0:
Console.WriteLine("Free");
break;
case > 100 when isMember:
Console.WriteLine("Member discount applies");
break;
default:
Console.WriteLine("Regular price");
break;
}
Member discount applies
Pattern matching'in property, positional ve tuple pattern gibi daha derin hâllerini C# İleri Seviye serisinde ayrı bir yazıda açacağız.
Hangisini Ne Zaman Kullanmalı
Benim pratikte kullandığım kural basit. İki üç dallı, birbirinden farklı şeyleri kontrol eden koşullar için if/else. Bir değerden yola çıkıp başka bir değer seçiyorsan switch expression: kısa, sonucu tek bir değişkene atanıyor ve compiler eksik durumlar için seni uyarıyor. Her dalda birkaç satırlık iş yapılacaksa (log yaz, stok düş, e-posta gönder) switch statement.
Bir de şunu fark et: switch expression bir expression olduğu için method'un dönüş değerine, bir property'ye ya da string interpolation'ın içine doğrudan yazılabilir. DiscountRate örneğinde method'un tamamı tek bir switch expression'dan ibaret. Aynı işi if ile yazsaydık dört return ve bir sürü süslü parantez görecektik.
Koşul sayısı onlarca olmaya başladıysa durup tasarıma bak. Formatlara göre kargo ücreti gibi "veri" niteliğindeki mapping'ler çoğu zaman koda değil, bir Dictionary'ye ya da veritabanındaki bir tabloya aittir. Koleksiyonlar serisinde bu tarz lookup tablolarına döneceğiz.
Sık Düşülen Tuzaklar
Karşılaştırma yerine atama yazmak. bool değişkenlerle == yerine = yazarsan kod derlenir:
bool isMember = true;
if (isMember = false)
{
Console.WriteLine("Member price");
}
Console.WriteLine(isMember);
Program.cs(3,5): warning CS0665: Assignment in conditional expression is always constant; did you mean to use == instead of = ?
False
Koşul hiç çalışmadı ve üstüne değişkenin değeri değişti. bool için == true ya da == false yazmak yerine doğrudan if (isMember) ve if (!isMember) yazarsan bu hataya düşemezsin. int gibi tiplerde ise if (stock = 0) zaten CS0029 ile derlenmez.
Kolların sırasını yanlış kurmak. Geniş pattern'i dar olanın üstüne koyarsan alttaki kol hiçbir zaman çalışamaz. Compiler bunu switch expression'da yakalar:
int quantity = 5;
string rate = quantity switch
{
> 0 => "positive",
> 10 => "bulk",
_ => "none"
};
Console.WriteLine(rate);
Program.cs(6,5): error CS8510: The pattern is unreachable. It has already been handled by a previous arm of the switch expression or it is impossible to match.
Ama yazının başındaki else if zincirinde aynı hatayı yakalamaz; orada sıralama tamamen senin sorumluluğunda.
Süslü parantezi atlamak. Yukarıda gördüğün gibi girinti seni kandırır; her blokta parantez kullan.
string karşılaştırmasında büyük-küçük harfi unutmak. switch içindeki "novel" kolu "Novel" ile eşleşmez. Kullanıcıdan gelen metni karşılaştırmadan önce normalize et ya da string.Equals(a, b, StringComparison.OrdinalIgnoreCase) kullan; string'ler yazısında culture tuzaklarını görmüştük.
Küçük Bir Deneme
★☆☆ Yazının başındaki kargo örneğini, koşulları doğru sıraya dizerek düzelt ve 50, 150, 250 için çıktıları kontrol et.
★★☆ Aynı kargo kuralını orderTotal switch { ... } şeklinde relational pattern'lerle yeniden yaz. Negatif bir tutar için ArgumentOutOfRangeException fırlatan bir kol ekle.
★★★ Describe method'una decimal price when price > 1000m kolu ekle ve "expensive" döndürsün. Kolu decimal price kolunun altına koyduğunda compiler ne diyor, üstüne koyduğunda ne oluyor? Farkı açıkla.
Koşullar bir kararı bir kez verir. Ama kitapçıdaki bütün kitapları tek tek gezip stoğu biten her kitap için aynı kararı vermek istediğinde tekrara ihtiyacın var. Bir sonraki yazıda döngüleri görüyoruz: for, while, do-while, foreach, break ve continue ile iç içe döngülerden doğru şekilde çıkmak.