İç içe if blokları, tekrar eden as dönüşümleri ve sonunda hangi koşulun hangi dala düştüğünü çözmeye çalışan bir okuyucu: eski C# kodunda bu tablo çok yaygındır. C# pattern matching bu tür kodu, koşulları ve veriyi ayıklamayı tek bir ifadede toplayarak kısaltır.
Bu yazı sözdizimi sözlüğü değil, bir refactoring rehberi. Önce "kötü" kodu, ardından aynı davranışı veren pattern'li halini göreceksiniz. Her pattern'in hangi C# sürümüyle geldiğini ve nerede abartıya kaçıldığını da bulacaksınız. Temel kavramlar için C# pattern matching ile daha okunur kod yazma yazısına da göz atabilirsiniz.
Önce sorun: koşul zinciri neden okunmaz?
Aşağıdaki metot, bir ödeme nesnesini açıklayan metin üretiyor. Davranışı basit, ama okuması zahmetli.
public abstract record Payment;
public record CreditCard(string Last4, int Installments) : Payment;
public record BankTransfer(string Iban) : Payment;
public record Cash : Payment;
public static string DescribeOld(Payment payment)
{
if (payment == null)
{
throw new ArgumentNullException(nameof(payment));
}
var card = payment as CreditCard;
if (card != null)
{
if (card.Installments > 1)
{
return "Kart ****" + card.Last4 + ", " + card.Installments + " taksit";
}
return "Kart ****" + card.Last4 + ", tek çekim";
}
var transfer = payment as BankTransfer;
if (transfer != null)
{
return "Havale " + transfer.Iban;
}
if (payment is Cash)
{
return "Nakit";
}
throw new ArgumentOutOfRangeException(nameof(payment));
}
Üç sorun var. Tip kontrolü ile değişken ataması ayrı satırlarda yapılıyor. Koşullar iç içe girdikçe akışı takip etmek zorlaşıyor. Ayrıca yeni bir Payment türü eklendiğinde bu metodu güncellemeyi hatırlamak tamamen geliştiriciye kalıyor.
Type pattern ve switch expression
Aynı metot, switch expression (C# 8) ve type pattern ile şöyle yazılır:
public static string Describe(Payment payment) => payment switch
{
CreditCard { Installments: > 1 } c => $"Kart ****{c.Last4}, {c.Installments} taksit",
CreditCard c => $"Kart ****{c.Last4}, tek çekim",
BankTransfer b => $"Havale {b.Iban}",
Cash => "Nakit",
_ => throw new ArgumentOutOfRangeException(nameof(payment))
};
Burada tip kontrolü, değişken tanımı ve ek koşul tek satırda. Switch expression bir değer döndürdüğü için return tekrarı da ortadan kalkıyor. Son satırdaki _ hiçbir kola uymayan değerleri yakalar; null da buraya düşer.
Sıra önemlidir: kollar yukarıdan aşağıya denenir. Genel kolu özel kolun üstüne yazarsanız derleyici, alttaki kolun hiçbir zaman çalışmayacağını söyleyip hata verir. Bu, eski if zincirinde sessizce gözden kaçan bir hatayı derleme zamanına taşır.
is ile tek seferlik kontroller
Her şeyin switch olması gerekmez. Tek bir tip kontrolü ve değişken ihtiyacı için is yeterlidir:
if (payment is BankTransfer { Iban: var iban } && iban.StartsWith("TR"))
{
Console.WriteLine($"Yurt içi havale: {iban}");
}
Null kontrolünde de x is null ve x is not null yazımını tercih edin. Bu yazım, == operatörünün aşırı yüklenmiş (overloaded) olma ihtimalinden etkilenmez; ayrıca niyeti daha net söyler.
Property pattern ile nesne içi koşullar
İş kuralları genellikle nesnenin alanlarına bakar. Kargo ücreti hesabını önce klasik biçimde düşünelim: sipariş null mı, tutar yeterli mi, ülke ne, ağırlık ne kadar... Property pattern bu kontrolleri nesnenin şekline benzeyen bir yazımla ifade eder.
public record Address(string Country, string City);
public record Order(Address Address, decimal TotalWeight, decimal Total);
public static decimal CalculateShipping(Order order) => order switch
{
null => throw new ArgumentNullException(nameof(order)),
{ Total: >= 1000 } => 0m,
{ Address.Country: "TR", TotalWeight: <= 5 } => 49m,
{ Address.Country: "TR" } => 79m,
{ Address.Country: "DE" or "FR" or "NL" } => 250m,
_ => 400m
};
{ Address.Country: "TR" } yazımı extended property pattern'dir ve C# 10 ile geldi. Eşdeğeri { Address: { Country: "TR" } } olduğundan, Address null ise kol sessizce eşleşmez; ayrıca null kontrolü yazmanız gerekmez.
Kuralların okunma sırası aynı zamanda iş önceliğidir: önce ücretsiz kargo, sonra yurt içi, sonra yurt dışı. Yeni bir kural eklemek, doğru yere bir satır eklemek demektir.
Relational ve logical pattern'ler
C# 9 ile gelen <, >, <=, >= karşılaştırmaları ve and, or, not birleştiricileri, aralık kontrollerini sadeleştirir.
public static string Grade(int score) => score switch
{
< 0 or > 100 => "Geçersiz",
>= 85 => "AA",
>= 70 => "BB",
>= 50 => "CC",
_ => "FF"
};
public static bool IsBusinessTime(DateTime time) =>
time.DayOfWeek is not (DayOfWeek.Saturday or DayOfWeek.Sunday)
&& time.Hour is >= 9 and < 18;
Eski yazımdaki score >= 0 && score <= 100 gibi değişkeni tekrar eden ifadeler yerine, değişken bir kez yazılır ve koşullar ona uygulanır. Notlandırma örneğinde aralıkları alttan değil üstten kontrol etmek, kolların birbirine bağımlılığını da düşürür.
and, or ve not pattern içindir; && ve || ise bool ifadeler arasında kullanılır. İkisini karıştırmamak gerekir. Yukarıdaki ikinci metotta her ikisi de bilinçli olarak yan yana duruyor.
Tuple pattern ile durum geçişleri
İki değerin birleşimine göre karar veriliyorsa, iç içe switch yerine tuple pattern kullanılır. Sipariş durum makinesi iyi bir örnektir:
public enum OrderState { Created, Paid, Shipped, Cancelled }
public static OrderState Next(OrderState state, string action) => (state, action) switch
{
(OrderState.Created, "pay") => OrderState.Paid,
(OrderState.Paid, "ship") => OrderState.Shipped,
(OrderState.Created or OrderState.Paid, "cancel") => OrderState.Cancelled,
_ => throw new InvalidOperationException($"{state} durumunda '{action}' geçersiz.")
};
Geçerli geçişler bir tablo gibi okunur. Tabloda olmayan her kombinasyon son kola düşer ve açık bir hata mesajı üretir. İç içe iki switch ile aynı şeyi yazmak hem daha uzun hem de eksik kombinasyonu gözden kaçırmaya daha açıktır.
List pattern ile dizi ve liste yapıları
C# 11 ile gelen list pattern, bir koleksiyonun uzunluğunu ve elemanlarını aynı anda eşleştirir. Basit bir komut satırı ayrıştırıcısı için uygundur:
public static string Parse(string[] args) => args switch
{
[] => "Komut girilmedi",
["help"] => "Kullanım: add <isim> [etiketler]",
["add", var name] => $"Ekle: {name}",
["add", var name, .. var tags] => $"Ekle: {name} (etiketler: {string.Join(", ", tags)})",
_ => "Bilinmeyen komut"
};
.. dilim (slice) pattern'idir; kalan elemanları yakalar ve var tags ile bir değişkene bağlanır. Uzunluk kontrolü, indeks erişimi ve IndexOutOfRangeException riski tek yazımda ortadan kalkar. Bu yapı, string, dizi ve sayılabilir/indekslenebilir koleksiyonlarla çalışır; IEnumerable ile doğrudan çalışmaz.
Hangi pattern ne zaman?
| Durum | Pattern | Sürüm |
|---|---|---|
| Tipe göre dallanma | Type pattern | C# 7 / switch expression C# 8 |
| Alan değerine göre karar | Property pattern | C# 8 |
| İç içe alana erişim | Extended property pattern | C# 10 |
| Aralık ve not/and/or | Relational ve logical pattern | C# 9 |
| İki değerin birleşimi | Tuple pattern | C# 8 |
| Dizi yapısı ve uzunluk | List pattern | C# 11 |
Abartmamak: pattern matching ne zaman kötü fikir?
Her koşulu switch expression'a çevirmek okunabilirliği artırmaz. Şu durumlarda geri adım atmak gerekir:
- Çok uzun pattern'ler: Bir kolda üç dört iç içe pattern ve
whenkoşulu varsa, ara değişkenlere ya da adı olan yardımcı metotlara bölmek daha okunurdur. - Her yeni tipte değişen kodlar: Aynı
switchprojenin beş farklı yerinde tekrarlanıyorsa sorun pattern'de değil tasarımdadır; davranışı tipin kendisine taşımak (polymorphism) daha doğru olabilir. - Sık değişen iş kuralları: Fiyat ve kampanya tabloları gibi veriler koda gömülmek yerine yapılandırmada veya veritabanında tutulmalıdır.
Ayrıca kapalı hiyerarşilerde bile _ kolunu bilinçli kullanın: unutulan bir tipin sessizce varsayılan sonuca düşmesi yerine hata fırlatması genellikle daha güvenlidir. Veri taşıyan tipler için record ve class farkları yazısı, pattern'lerin neden record'larla bu kadar iyi çalıştığını anlamanıza yardımcı olur. Genel okunabilirlik ilkeleri için temiz kod kuralları yazısına bakabilirsiniz.
Sık Sorulan Sorular
Switch expression ile klasik switch arasındaki fark nedir?
Switch expression bir değer üretir ve her kolu bir ifadedir; break ve return tekrarı gerektirmez. Klasik switch ise deyimdir (statement) ve birden fazla satırlık işlemler için hâlâ uygundur.
Hiçbir kola uymayan değer gelirse ne olur?
Switch expression'da hiçbir kol eşleşmezse çalışma zamanında exception fırlatılır. Derleyici kapsamlı olmayan switch expression için uyarı verir. Bu yüzden bilinmeyen durumlar için _ kolunda anlamlı bir hata fırlatmak iyi bir alışkanlıktır.
Pattern matching performansı düşürür mü?
Genel olarak okunabilirlik kaygısıyla yazılan eşdeğer if zincirinden belirgin bir fark beklenmez. Sıcak bir yolda (hot path) karar vermeden önce kendi senaryonuzda BenchmarkDotNet ile ölçüm yapın.
Property pattern null değerlerle nasıl davranır?
Süslü parantezli bir property pattern, değer null ise eşleşmez. Bu nedenle { Address.Country: "TR" } yazımında Address null olduğunda ayrıca bir null kontrolüne gerek kalmaz; kol sadece eşleşmemiş sayılır.
Hangi C# sürümünden itibaren kullanabilirim?
Type pattern C# 7 ile, switch expression ve property pattern C# 8 ile, relational ve logical pattern C# 9 ile, extended property pattern C# 10 ile, list pattern ise C# 11 ile geldi. .NET 10 ve C# 14 kullanan bir projede hepsi hazırdır.
Sonuç
C# pattern matching, koşul, tip kontrolü ve veri ayıklamayı tek yerde toplayarak kodu kısaltır. Asıl kazanç ise derleyicinin kapsamı ve kol sırasını denetlemesidir. Hatırlanacak üç nokta:
- İç içe
ifveaszincirlerini switch expression ile, nesne içi koşulları property pattern ile sadeleştirin. - Kol sırası iş önceliğidir; son kolda anlamlı bir hata fırlatın.
- Pattern uzayıp tekrarlanmaya başladığında, tasarımı gözden geçirip polymorphism veya veri odaklı çözüm düşünün.