C# Ustası — Bölüm 4: Pattern Matching'in Son Sürümü — List, Relational, Recursive, Property Patterns
C# 7'den 12'ye pattern matching evrimi: type, property, relational, logical, recursive ve list patterns. Switch expression'larda exhaustiveness, kucuk bir ifade parser ornegi, pattern'lerin IL cikisi, yaygin tuzaklar ve 'ne zaman pattern matching DEGIL klasik kod' sorusunun cevabi.
Merhaba arkadaşlar, C# Ustası serisinin dördüncü bölümünde, C#'ın son yıllardaki en güzel katkılarından birine bakacağız — pattern matching. C# 7'den C# 12'ye kadar her versiyonda bir parça daha olgunlaşan bu özellik, bugün artık kendine has bir dil içinde dil halini aldı. Doğru kullanıldığında kod okunabilir ve ifadesel oluyor; yanlış kullanıldığında ise kim yazdığını anlamak bile imkansız bir canavara dönüşüyor. Başlamadan önce bir kahve alın, çünkü bu yazıda hem tüm pattern türlerini hem de "ne zaman dur" demeyi birlikte öğreneceğiz.
Tarihsel Yolculuk — Nereden Nereye Geldik
Pattern matching dilin içine bir anda girmedi, katman katman geldi:
- C# 7.0 (2017): Type pattern (is T t), constant pattern (is null).
- C# 8.0: Switch expressions, property pattern ({ Age: 18 }), tuple pattern, recursive pattern.
- C# 9.0: Relational patterns (> 10, <= 5), logical patterns (and, or, not), type pattern kısaltması.
- C# 10.0: Extended property patterns ({ Inner.Name: "x" }).
- C# 11.0: List patterns ([1, 2, .., _]), slice pattern.
- C# 12.0: Pattern matching iyileştirmeleri — daha akıllı exhaustiveness.
Bu evrim, F# ve Scala gibi fonksiyonel dillerdeki güçlü pattern matching'i yavaş yavaş C#'a getirdi.
Type Pattern — Tipi Sorgulayıp Yakalama
object o = "Merhaba";
if (o is string s)
{
Console.WriteLine($"Uzunluk: {s.Length}");
}
// Eski yöntem:
if (o is string)
{
var s = (string)o; // ayrı cast
}
Tek satırda hem kontrol hem cast. Ayrıca is not da kullanılabilir: if (o is not null) ....
Property Pattern — İçeride Eşleşme
record Kullanici(string Ad, int Yas, string Ulke);
static string Kategori(Kullanici k) => k switch
{
{ Yas: < 18 } => "Genc",
{ Yas: >= 65 } => "Yasli",
{ Ulke: "TR", Yas: >= 18 and < 65 } => "Yetiskin - Turkiye",
{ Ulke: "US" } => "ABD'li",
_ => "Diger"
};
Ne güzel okunuyor: "Yaşı 18'den küçükse genç, değilse ve 65'ten büyükse yaşlı..." — bir cümle gibi.
Extended Property Pattern — İç İçe
record Adres(string Sehir, string Ulke);
record Kullanici(string Ad, Adres Adres);
static bool Istanbul(Kullanici k) => k is { Adres.Sehir: "Istanbul" };
// Eskiden: k.Adres.Sehir == "Istanbul"
Nokta notasyonu ile derinlemesine iniyoruz. Null-safety de otomatik — Adres null ise match false döner, exception yok.
Relational Pattern — Sayısal Karşılaştırma
static string SicaklikDurum(double c) => c switch
{
< 0 => "Donuyor",
>= 0 and < 10 => "Soguk",
>= 10 and < 20 => "Serin",
>= 20 and < 30 => "Ilik",
>= 30 => "Sicak",
double.NaN => "Gecersiz"
};
and, or, not ile bunları kombinleyebiliyoruz. not null çok yaygın kullanılan bir pattern.
Recursive Pattern — Deconstruction ile Eşleşme
Record'lar ve Deconstruct method'u olan tipler pattern matching'e açılıyor:
record Nokta(int X, int Y);
static string NoktaYeri(Nokta p) => p switch
{
(0, 0) => "Merkez",
(> 0, > 0) => "Birinci ceyrek",
(< 0, > 0) => "Ikinci ceyrek",
(< 0, < 0) => "Ucuncu ceyrek",
(> 0, < 0) => "Dorduncu ceyrek",
_ => "Eksen uzerinde"
};
Positional pattern ile deconstruction otomatik çalışıyor. Kendi sınıflarınızda public void Deconstruct(out ...) yazarak aynı özelliği kazanabilirsiniz.
List Pattern — Koleksiyonları Eşleştirmek
C# 11 ile gelen ve en sık yeni özellik olarak sevdiğim:
int[] dizi = { 1, 2, 3, 4, 5 };
// İlk ve son elemanı yakala, ortası ne olursa olsun:
if (dizi is [var ilk, .., var son])
{
Console.WriteLine($"Ilk: {ilk}, Son: {son}");
}
// Tam uzunluk eşleşmesi:
if (dizi is [1, 2, 3])
{
Console.WriteLine("Tam 1,2,3");
}
// İlk üç tanesi sabit, sonrası değişken:
if (dizi is [1, 2, 3, .. var kalan])
{
Console.WriteLine($"Kalan {kalan.Length} eleman");
}
// En az 2 elemanlı:
if (dizi is [_, _, ..])
{
// ...
}
Liste pattern'i, veri ayrıştırma algoritmalarında — örneğin komut satırı argümanları, tokenizer'lar, REPL'ler — çok güçlü bir araç.
Switch Expression'lar ve Exhaustiveness
Switch statement'tan farklı olarak switch expression değer döndürür ve tüm durumları kapsamazsanız derleyici uyarı verir:
enum Renk { Kirmizi, Mavi, Yesil }
static string Kod(Renk r) => r switch
{
Renk.Kirmizi => "#FF0000",
Renk.Mavi => "#0000FF",
Renk.Yesil => "#00FF00",
// _ => "..." unutulduysa uyarı alırsınız (enum için tüm değerler kapsanmalı)
};
Bu özellik refactor'larda altın değerinde. Enum'a yeni değer eklediğinizde derleyici tüm switch'leri otomatik olarak size hatırlatır.
Visual Studio tarafında switch expression yazarken editörün ne kadar yardımcı olduğunu görmek için boş bir obj switch { } yazıp içine imleci koyup Ctrl+Space'e basın. IntelliSense size obj'nin tipini analiz edip altındaki concrete tipleri, discard pattern'ını ve eğer sealed record hierarchy kurduysanız tüm alt case'leri listeler. Çok faydalı bir başka özellik: bir switch arm eksikse sarı ampul (Ctrl+.) bastığınızda "Add missing cases" refactoring'i çıkar — exhaustiveness eksikliğini otomatik kapatır. Enum'larda ve sealed class hierarchy'de parlayan bu özelliği bilmeyen çok developer var.
Gerçek Örnek: Küçük Bir İfade Parser'ı
Pattern matching'in gücünü göstermenin en iyi yolu küçük bir parser yazmak. Aşağıdaki kod "3 + 4 * 2" gibi aritmetik ifadeleri hesaplar:
// Token tipleri
abstract record Token;
record Sayi(double Value) : Token;
record Toplama : Token;
record Carpma : Token;
record ParantezAc : Token;
record ParantezKapat : Token;
record Son : Token;
// Tokenizer - pattern matching ile
static IEnumerable<Token> Tokenize(string input)
{
int i = 0;
while (i < input.Length)
{
switch (input[i])
{
case ' ':
i++; break;
case '+':
yield return new Toplama(); i++; break;
case '*':
yield return new Carpma(); i++; break;
case '(':
yield return new ParantezAc(); i++; break;
case ')':
yield return new ParantezKapat(); i++; break;
case var c when char.IsDigit(c):
int basla = i;
while (i < input.Length && (char.IsDigit(input[i]) || input[i] == '.')) i++;
yield return new Sayi(double.Parse(input[basla..i]));
break;
default:
throw new InvalidOperationException($"Gecersiz karakter: {input[i]}");
}
}
yield return new Son();
}
// AST
abstract record Ifade;
record Literal(double Value) : Ifade;
record Topla(Ifade Sol, Ifade Sag) : Ifade;
record Carp(Ifade Sol, Ifade Sag) : Ifade;
// Evaluator - pattern matching ile saf fonksiyonel
static double Degerle(Ifade i) => i switch
{
Literal(var v) => v,
Topla(var sol, var sag) => Degerle(sol) + Degerle(sag),
Carp(var sol, var sag) => Degerle(sol) * Degerle(sag),
_ => throw new InvalidOperationException()
};
// Kullanım
var ifade = new Topla(new Literal(3), new Carp(new Literal(4), new Literal(2)));
Console.WriteLine(Degerle(ifade)); // 11
Literal(var v) ile hem tipi kontrol ediyor hem değeri yakalıyoruz. 20 satırlık bir evaluator, F#'a benzer ifadesellikte.
Pattern Matching Nasıl Derleniyor?
Switch expression'ın arka planında derleyici çoğu zaman if-else zinciri üretir, ama enum'lar gibi kısıtlı değer uzaylarında jump table'a çevirebilir. ILSpy veya SharpLab ile çıktıyı görürseniz:
- Constant patterns çok olduğunda jump table.
- Type patterns genelde isinst IL komutu ile.
- List patterns length kontrolü + indeksli erişim.
- Property patterns property getter çağrısı + karşılaştırma.
Performans açısından klasik if-else'e göre genelde aynı veya az daha iyi. Ama bu bir kâbus değil — asıl kazanç okunabilirlik.
Visual Studio tarafında pattern matching refactor'larında bir başka kıymetli özellik: klasik switch statement'ınızın üstüne Ctrl+. tuşlayıp "Convert to switch expression" seçeneğini çalıştırın. Doğru koşullar varsa Roslyn'in refactor engine'i kodu otomatik çevirir, default case'i _'ya maps'ler, return'leri lambda'ya dönüştürür. Aynı menünün altında "Simplify switch expression"ı da bulursunuz — gereksiz nested pattern'leri düzeltir. Tools > Options > Text Editor > C# > Code Style > Pattern Matching Preferences altında onlarca ayar var: "Prefer pattern matching over 'is' with 'cast' check", "Prefer pattern matching over 'as' with 'null' check", vs. Takımınızla standart belirlerseniz bu tercihleri EditorConfig'e export edip repo'ya commit edebilirsiniz — her developer aynı stilde kod yazar.
Tuzaklar ve Anti-Pattern'ler
- Aşırı karmaşık pattern: Beş seviye iç içe tuple/property pattern artık okunaklı değil. Method çıkarın.
- Default case'i unutmak: Switch expression'da tüm durumları kapsamıyorsanız runtime'da SwitchExpressionException atar. _ => mutlaka olsun (enum'da derleyici uyarı verir, ama her zaman değil).
- Property pattern'de yan etki: Pattern eşleşirken property getter'ı çağrılır. Eğer getter'da side effect varsa (DB çağrısı, vs.) felaket.
- Hierarchical type match: Base class'ı önce yazmak, subclass'a hiç ulaşmamanıza neden olur. Daha spesifik olan önce gelsin.
- List pattern'i Count olmayan tiplerde kullanmak: Length veya Count property'si olmayan kolleksiyonlarla çalışmıyor. Dikkat edin.
Ne Zaman Pattern Matching KULLANMAYALIM?
Pattern matching havalı ama her yere konulmamalı:
- İki case varsa: Normal if/else yazın. Switch expression bazen overkill.
- Ekibiniz hazır değilse: is { Foo: { Bar: [.., var last] } } junior ekip arkadaşınıza karanlık büyü gibi gelebilir. Kademe kademe gidin.
- Debug zorluğu: Pattern matching içinde breakpoint koymak zor. Karmaşık pattern'leri bölün.
- Refactor güvenliği: Uzun switch expression'da bir string literal değiştiğinde fark etmeyebilirsiniz. Enum'lara öncelik verin.
- OOP polymorphism yerine koymayın: Bir tip hiyerarşisi varsa ve davranış tipe bağlıysa, virtual method daha iyi bir OOP çözümü olabilir. Pattern matching "açık dünya" için, OOP "kapalı dünya" için.
Pratik Tavsiye — Nerede Çizgiyi Çekelim?
Yıllar içinde pişirdiğim kural:
- Pattern expression tek satıra sığmalı (maksimum 80 karakter).
- Switch arm sayısı 10'u geçmemeli. Geçerse, domain'e özel bir tip hiyerarşisi kurmak daha temiz.
- İç içe pattern iki seviyeyi aşmasın.
- Property pattern ile FirstOrDefault gibi LINQ'u karıştırmayın — birini seçin.
Özet
Pattern matching, doğru kullanıldığında kodu ifadesel, refactor'a güvenli ve F#-benzeri bir tatlılığa taşır. Yanlış kullanıldığında ise sadece "havalı görünmek için" karanlık labirentlere sürükler. Her feature'da olduğu gibi: nerede ne, bunun dengesini kurmak mesleğimiz.
Bir sonraki ve son bölümde, C#'ın en geç olgunlaşan ama artık hakikaten işe yarar hale gelen özelliği Generic Math ve static abstract interface üyelerine bakacağız. T.Add(a, b) yazarken aslında neler oluyor, neden uzun süre beklememiz gerekti — hepsini anlatacağız. Görüşürüz!