Skip to content

Bölüm 12 / 13

Modern C#

Pattern matching, switch expression, top-level statements, primary constructor.

13 dakikalık okuma

Bu hattın 2. dersi is null / is not null kalıbını, 4. dersi catch ... when süzgecini kullanmış ve "bunlar desen eşleştirmenin parçası, mekanizmasını 12. ders açacak" demişti. Şimdi açıyoruz. Ders iki katmanlıdır: birincisi desen eşleştirme ve switch ifadeleri — C#'ın son on yıldaki en büyük ifade gücü artışı; ikincisi kodu kısaltan sözdizimi katmanı. Ama asıl mesele sözdizimi listesi ezberlemek değil: hangi kısaltma okunabilirliği artırır, hangisi gizler sorusunu cevaplayabilmek. Kod incelemelerinde en çok tartışılan başlık budur.

01Ne öğreneceksin

  • Desen eşleştirme ailesi: tip, sabit, özellik, ilişkisel, mantıksal, demet ve liste desenleri
  • switch ifadesi: deyimden farkı, tamlık denetimi, when koşulu
  • Gürültüyü azaltan sözdizimi: en üst düzey ifadeler, dosya kapsamlı namespace, global using, hedef tipli new, birincil yapıcı, koleksiyon ifadeleri
  • Sürüm haritası: C# 8'den 14'e neyin ne zaman geldiği ve LangVersion
  • Tuzaklar: birincil yapıcının record'dan farkı, aşırı kısaltmanın maliyeti
  • Eski kodu modernleştirme ve ekip kararını .editorconfig ile yazıya dökme

02Desen eşleştirme

Desen (pattern), bir değerin "şu biçimde mi" diye sınandığı ve aynı anda parçalarına ayrıldığı ifadedir. Hat 01'in if dersinde yazdığın kontrol zincirlerinin ve tip dönüşümlerinin yerine geçer. Aile şöyle büyüdü:

DesenÖrnekAnlamı
Tip / bildirimo is Musteri mTip uyuyorsa ve değişkene atarsa
Sabitx is 0, s is "iptal"Belirli bir değere eşitse
nullx is null / is not nullNull denetimi (2. dersin kalıbı)
Özelliks is { Tutar: > 1000, Durum: "acik" }Alanları birlikte sınar; iç içe yazılabilir
İlişkiselyas is >= 18Karşılaştırma operatörü desen içinde
Mantıksalis >= 18 and < 65, is not nulland / or / not ile birleştirme
Demet (tuple)(rol, tutar) switch { ... }Birden çok değeri tek kararda sınar
Listedizi is [1, .., var son]Dizinin biçimini sınar, parçasını yakalar

Kazancı somut görmek için, Hat 01'de yazdığın tarzda bir kontrolü karşılaştır:

// eski
if (nesne != null && nesne is Siparis)
{
    var s = (Siparis)nesne;
    if (s.Tutar > 1000 && s.Durum == "acik") IndirimUygula(s);
}

// desenlerle: tek ifade, tek okuma
if (nesne is Siparis { Tutar: > 1000, Durum: "acik" } s) IndirimUygula(s);

Buradaki üç kazanç, dersin özüdür: null denetimi desene dahildir (tip deseni null'ı zaten eler), dönüştürme ile denetim tek adımdır (ayrı bir cast yok — 1. dersteki geçersiz dönüşüm riski kalkar) ve koşul okunduğu gibidir.

not operatörünün 2. dersteki özel değeri hatırlanmalı: if (x is not null), if (x != null)'dan daha güvenlidir — çünkü != operatörü tip tarafından ezilebilir, desen ise ezilemez; her zaman gerçek null denetimi yapar.

03switch ifadesi

Klasik switch bir deyimdir (statement): dallanır, iş yapar. C# 8'in switch ifadesi (expression) ise değer üretir:

// deyim: 12 satır, break'ler, geçici değişken
string etiket;
switch (siparis.Durum)
{
    case "acik":   etiket = "Açık";    break;
    case "kapali": etiket = "Kapalı";  break;
    default:       etiket = "Bilinmiyor"; break;
}

// ifade: 5 satır, atama tek yerde, derleyici tamlığı denetler
string etiket = siparis.Durum switch
{
    "acik"   => "Açık",
    "kapali" => "Kapalı",
    _        => "Bilinmiyor"        // _ : her şeyi yakalayan desen
};

Asıl güç, desenlerle birleşince ortaya çıkar. Aşağıdaki örnek, iş kurallarını bir tabloya çevirir — Hat 04'te göreceğin "koşul karmaşasını veriye çevirme" fikrinin dildeki karşılığıdır:

decimal Komisyon(Musteri m, decimal tutar) => (m.Segment, tutar) switch
{
    ("kurumsal", >= 100_000) => tutar * 0.02m,
    ("kurumsal", _)          => tutar * 0.03m,
    ("bireysel", > 0) when m.YilSayisi >= 5 => tutar * 0.04m,   // when: desenin yetmediği yerde koşul
    ("bireysel", > 0)        => tutar * 0.05m,
    _ => throw new ArgumentOutOfRangeException(nameof(tutar))
};

Üç davranış kuralını bilmek gerekir:

  • Sıra önemlidir. Kollar yukarıdan aşağıya denenir; ilk eşleşen kazanır. Daha genel bir deseni yukarı koyarsan alttakiler ölü kod olur — derleyici bunu genelde uyarı olarak söyler.
  • Tamlık (exhaustiveness) denetlenir. Derleyici, kapsanmayan bir değer olabileceğini görürse uyarır. Hiçbir kol eşleşmezse çalışma zamanında SwitchExpressionException fırlar — sessizce yanlış bir değer dönmez, bu iyi bir şeydir.
  • when süzgeci, desenin ifade edemediği koşullar içindir (başka bir nesneye bakmak, bir metot çağırmak). 4. dersteki catch (X ex) when (...) tam olarak aynı mekanizmadır — o dersin senedi burada kapanıyor: when, hem switch kollarında hem catch bloklarında "bu dalı seç" kararının süzgecidir.

04Gürültüyü azaltan sözdizimi

İkinci katman, aynı kodu daha az tören ile yazmayı sağlar. Faydası tek tek küçük, toplamı büyüktür:

// —— Program.cs, en üst düzey ifadeler (top-level statements, C# 9) ——
// class Program / static void Main sarmalı yok:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Run();

// —— dosya kapsamlı namespace (C# 10): bir seviye girinti kazanılır ——
namespace Sirket.Siparis;              // süslü parantez yok

// —— global using (C# 10): her dosyada tekrarlanan using'ler tek yerde ——
global using System.Text.Json;

// —— hedef tipli new (C# 9): tip zaten solda yazılı, sağda tekrarlama ——
private readonly Dictionary<string, List<int>> _kutu = new();

// —— koleksiyon ifadeleri (C# 12) ——
int[] sayilar = [1, 2, 3];
List<string> adlar = [ilk, .. digerleri, "son"];      // .. yayma (spread)

// —— ham metin değişmezleri (C# 11): kaçış işaretsiz JSON/SQL ——
string sorgu = """
    SELECT Ad, Soyad FROM Musteri
    WHERE Segment = "kurumsal"
    """;

Birincil yapıcı (primary constructor, C# 12) ayrı bir başlığı hak eder, çünkü en çok yanlış anlaşılan özelliktir. Sınıf adının yanına yazılan parametreler sınıfın tüm gövdesinde kullanılabilir:

public class SiparisServisi(ISiparisDeposu depo, ILogger<SiparisServisi> log)
{
    public async Task<Siparis?> GetirAsync(int id, CancellationToken token)
    {
        log.LogInformation("Sipariş {Id} isteniyor", id);   // parametreler doğrudan kullanılıyor
        return await depo.BulAsync(id, token);
    }
}

Bu, bağımlılık enjeksiyonu yazan sınıflarda (Hat 05'in her servisi) alan tanımı + yapıcı + atama üçlüsünü tek satıra indirir. Ama record'la karıştırma — 10. dersle çapraz sorulan fark:

record birincil yapıcısıclass birincil yapıcısı
Özellik üretir mi?Evet — public init özelliklerHayır — yalnız gövdede görünür parametre
Dışarıdan okunur mu?EvetHayır (kullanılıyorsa gizli bir alana yakalanır)
Değer eşitliği?EvetHayır
Değiştirilebilir mi?Hayır (init)Evet — parametreye gövdeden atama yapılabilir

Son satır pratik bir uyarıdır: birincil yapıcı parametresi readonly değildir. Değişmezliği garanti etmek istiyorsan klasik yolu kullan: private readonly ISiparisDeposu _depo = depo;

05Sürüm haritası

Mülakatta "hangi sürümle geldi" sorulur; sahada ise "bu proje bunu kullanabilir mi" sorulur. İkisinin cevabı aynı tablodadır:

SürümPlatformÖne çıkanlar
C# 8.NET Core 3.xNullable referans tipleri (2. ders), switch ifadesi, using bildirimi, aralık/indeks ([..3]), IAsyncEnumerable, varsayılan arayüz metotları
C# 9.NET 5record, init, en üst düzey ifadeler, ilişkisel ve mantıksal desenler, hedef tipli new
C# 10.NET 6Dosya kapsamlı namespace, global using, record struct, genişletilmiş özellik desenleri
C# 11.NET 7Ham metin değişmezleri, required, liste desenleri, arayüzlerde statik soyut üyeler (generic matematik)
C# 12.NET 8Birincil yapıcılar, koleksiyon ifadeleri [..], tip takma adları, varsayılan lambda parametreleri
C# 13.NET 9params koleksiyonları, yeni Lock tipi, iterator/async içinde ref kullanımı
C# 14.NET 10Extension üyeleri (11. ders), field anahtar kelimesi, null koşullu atama

Üç pratik kural:

  • Dil sürümü, proje dosyasındaki <TargetFramework>'e göre otomatik seçilir. <LangVersion> ile elle yükseltmek genelde yanlıştır: bazı özellikler yalnız sözdizimidir, bazıları çalışma zamanı desteği ister (record'un init'i eski hedeflerde çalışmaz).
  • Kurumsal projelerde LTS sürümleri tercih edilir; kurumun desteklediği çerçeveyi bilmek işin bir parçasıdır. "En yeni özelliği kullanayım" değil, "hedef çerçeve ne veriyor" sorusu doğru sorudur.
  • Yeni dil özellikleri, eski API'leri emekliye ayırmaz. Bir kod tabanında iki biçimin de bulunması normaldir; toplu biçim değişikliği ile davranış değişikliğini aynı commit'e koyma — inceleme yapan kişi ikisini ayıramaz.

06Kısalık ne zaman zarar verir

Bu bölüm dersin asıl kıdem katmanıdır: her yeni özellik bir kazanç değildir. Kod incelemesinde tartışılan başlıklar:

  • Uzun switch ifadeleri. Beş kolun ötesine geçen, iç içe desenler içeren ve when'lerle dolan bir ifade, okunmayan tek bir bloktur. Kural: kollar tek satırlık eşlemeler olduğu sürece switch ifadesi; her kol iş yapmaya başladığında ayrı metot ya da polimorfizm (Hat 01'in 13. dersi, Hat 04'ün Strategy deseni).
  • var kullanımı. Tip sağdan açıkça okunuyorsa (var m = new Musteri()) sorun yok; bir metot çağrısının dönüşünde (var x = Hesapla()) okuyucudan tanıma gitmesini istemiş olursun. Ekip kararıdır ve .editorconfig'e yazılır.
  • Aşırı ifadeleştirme. Üç iç içe üçlü operatör (?:) ya da tek satıra sıkıştırılmış LINQ + desen karışımları, "az satır" ile "az anlaşılan"ı karıştırır. Ölçüt satır sayısı değil, bir kez okumada anlaşılmadır.
  • En üst düzey ifadeler yalnız giriş noktası içindir. Program.cs'de değerlidir; iş mantığını oraya yığmak, yapılandırılmamış bir betik üretir.
  • Liste desenlerinin maliyeti. dizi is [_, _, var son] okunaklıdır ama sayılamayan (lazy) kaynaklarda uzunluk bilgisi gerektirir; sıcak yolda ölçmeden kullanma — 6. dersin ertelenmiş çalışma uyarısı burada da geçerlidir.

Karşı tarafta, tereddütsüz benimsenen özellikler de vardır: is not null, using bildirimi, dosya kapsamlı namespace, nameof, string interpolation ve init. Bunlar hem daha kısa hem daha güvenlidir; "eski tarz" yazmanın savunulabilir bir gerekçesi yoktur.

07Sahada: eski kodu modernleştirmek

Gerçek işte modern C#'ı sıfırdan yazmazsın; yıllardır yaşayan bir kod tabanına taşırsın. İzlenen sıra:

  1. Kararı yazıya dök. .editorconfig dosyası, ekibin biçim ve tarz kararlarını makine tarafından uygulanabilir hâle getirir (csharp_style_prefer_switch_expression, csharp_style_var_when_type_is_apparent gibi). Tartışma bir kez yapılır, sonra araç uygular.
  2. Analizörlere güven. Derleyici ve .NET analizörleri "burada switch ifadesi kullanılabilir", "burada using bildirimi olur" diye önerir; editör tek tuşla uygular. Elle tarama yapma.
  3. Önce güvenlik kazandıranlar. Modernleştirme sırası keyfi değildir: nullable referans tiplerini açmak (2. ders), is not null'a geçmek ve using bildirimlerini uygulamak hata azaltır; birincil yapıcı ve koleksiyon ifadeleri yalnız kısaltır. Önce ilk grup.
  4. Biçim commit'ini ayır. Yüzlerce dosyayı biçimlendiren bir değişiklik, aynı commit'te bir hata düzeltmesi taşıyorsa incelenemez. Hat 16'da bunun araçlarını (git blame gürültüsü ve yok sayma dosyası) göreceksin.

Bu ders, hattın son öğretim dersidir. Sırada Hat 03'ün yeterlilik kontrolü var: 12 dersin hepsini tek bir gerçekçi görevde kullanacaksın — tip kararlarından null güvenliğine, LINQ'dan async'e, kaynak yönetiminden desen eşleştirmeye kadar.

08Mini alıştırma — Koşul zincirini tabloya çevir

Yaklaşık 20 dakika · dotnet run

  1. Desene çevir: object alan ve tipine göre farklı metin döndüren bir metodu önce klasik if + is + cast ile, sonra tek bir switch ifadesiyle yaz (int, string, null, List<int> ve "diğer" kollarıyla). İki sürümün satır sayısını ve okunabilirliğini karşılaştır.
  2. Özellik ve ilişkisel desen: Siparis(decimal Tutar, string Durum, int MusteriYili) record'u tanımla; kargo ücretini switch ifadesiyle hesapla — 1000 üstü ücretsiz, açık siparişte indirimli, 5 yıldan eski müşteride farklı. En az bir kolda when kullan.
  3. Tamlık deneyi: yukarıdaki switch'ten _ kolunu sil ve derle — derleyici ne diyor? Uyarıya rağmen çalıştırıp kapsanmayan bir değer ver: hangi istisnayı aldın?
  4. Liste deseni: int[] alan bir metot yaz; [] (boş), [var tek], [var ilk, .., var son] kollarıyla farklı metinler döndürsün. 0, 1, 2 ve 5 elemanlı dizilerle dene.
  5. Birincil yapıcı farkı: aynı iki parametreyle bir record ve bir class tanımla. Her ikisinde de parametreye dışarıdan erişmeyi dene — hangisi derlendi? class sürümünde bir metottan parametreye atama yap: derlendi mi? Bulgularını 10. dersin tablosuyla karşılaştır.

09Kendini sına

Desen eşleştirme nedir, hangi desen türleri vardır? "x is not null" ile "x != null" arasındaki fark nedir?

Desen eşleştirme, bir değerin belirli bir biçime uyup uymadığını sınayan ve aynı anda parçalarını değişkene bağlayan ifadedir; tip denetimi, dönüştürme ve null kontrolünü tek adıma indirir. Türleri: tip/bildirim deseni (o is Musteri m), sabit deseni, null deseni, özellik deseni ({ Tutar: > 1000 }), ilişkisel desen (is >= 18), mantıksal desenler (and/or/not), demet deseni ve liste deseni ([1, .., var son]). Tip deseni null'ı zaten eler, bu yüzden ayrı null kontrolü gerekmez. "is not null" ile "!= null" arasındaki fark güvenliktir: != operatörü tip tarafından ezilebilir ve beklenmedik bir davranış üretebilir; desen ezilemez, her zaman gerçek null denetimi yapar.

switch ifadesi ile switch deyimi arasındaki fark nedir? Kol sırası, tamlık ve when nasıl çalışır?

switch deyimi dallanır ve iş yapar; switch ifadesi bir değer üretir, dolayısıyla doğrudan atanabilir veya döndürülebilir — break, geçici değişken ve tekrarlanan atama ortadan kalkar. Kollar yukarıdan aşağıya denenir ve ilk eşleşen kazanır; daha genel bir deseni yukarı koymak alttakileri ölü koda çevirir, derleyici bunu genelde uyarır. Derleyici tamlığı denetler: kapsanmayan değer kalabiliyorsa uyarı verir, çalışma zamanında hiçbir kol eşleşmezse SwitchExpressionException fırlar — sessizce yanlış değer dönmez. when süzgeci, desenin ifade edemediği ek koşullar içindir (başka bir nesneye bakmak, metot çağırmak); aynı mekanizma catch bloklarında exception filter olarak kullanılır. Pratik sınır: kollar tek satırlık eşlemeler olduğu sürece switch ifadesi okunaklıdır, her kol iş yapmaya başladığında ayrı metot veya polimorfizm gerekir.

Sınıftaki birincil yapıcı ile record'un birincil yapıcısı arasındaki farklar nelerdir?

record'un birincil yapıcı parametreleri public init-only özelliklere dönüşür: dışarıdan okunabilir, değer eşitliğine katılır, ToString'de görünür ve kurulumdan sonra değiştirilemez. Sınıfın birincil yapıcı parametreleri (C# 12) ise özellik üretmez: yalnız sınıf gövdesinde görünür, dışarıdan erişilemez, değer eşitliği doğurmaz ve kullanıldıklarında derleyici tarafından gizli bir alana yakalanırlar. Ayrıca readonly değildirler — gövdeden atama yapılabilir, yani istemeden değiştirilebilir durum oluşur. Değişmezlik garantisi isteniyorsa parametre klasik biçimde bir readonly alana kopyalanır (private readonly IDepo _depo = depo;). Sınıf birincil yapıcısının asıl değeri, bağımlılık enjeksiyonu alan servislerde alan-yapıcı-atama üçlüsünü tek satıra indirmesidir.

Yeni dil özellikleri her zaman kullanılmalı mı? Eski bir kod tabanını modernleştirirken hangi sırayı izlersin?

Hayır — ölçüt satır sayısı değil, bir kez okumada anlaşılma. Tereddütsüz benimsenenler hem kısa hem güvenli olanlardır: is not null, using bildirimi, dosya kapsamlı namespace, nameof, string interpolation, init. Dikkatle kullanılanlar: uzun ve when'lerle dolmuş switch ifadeleri, iç içe üçlü operatörler, iş mantığını taşıyan en üst düzey ifadeler, sıcak yolda liste desenleri. Ayrıca dil sürümü hedef çerçeveye bağlıdır; LangVersion'ı elle yükseltmek bazı özellikler çalışma zamanı desteği gerektirdiği için yanlıştır ve kurumsal projelerde LTS çerçevesi belirleyicidir. Modernleştirme sırası: önce ekip kararını .editorconfig'e yazmak, sonra analizör önerilerine güvenmek, önce hata azaltan dönüşümleri (nullable referans tipleri, is not null, using bildirimi) uygulamak, en sona yalnız kısaltan dönüşümleri bırakmak; ve biçim değişikliği ile davranış değişikliğini asla aynı commit'e koymamak.

10Sınav köşesi

Yazılıda nasıl sorulur

(1) Kavram: "Pattern matching nedir?", "switch ifadesi ile deyimi arasındaki fark". (2) Sözdizimi eşleme: verilen desenin ne sınadığını yazma (is { Tutar: > 1000 }, is >= 18 and < 65, is [_, .., var son]). (3) Çıktı sorusu: kol sırası değiştirilmiş bir switch ifadesinde hangi kolun çalıştığı. (4) Hangi sürüm: record (C# 9), dosya kapsamlı namespace (C# 10), ham metin değişmezleri ve required (C# 11), birincil yapıcı ve koleksiyon ifadeleri (C# 12). (5) Doğru/yanlış: "sınıfın birincil yapıcı parametreleri public özellik üretir" (yanlış — record'da üretir). (6) when'in iki kullanımı (switch kolu ve catch süzgeci — Hat 03'ün 4. dersiyle çapraz). (7) Kod modernleştirme: verilen eski bloğu desenli sürüme çevirme.

Sözlüde nasıl anlatılır — 2 dakikalık taslak

"Modern C# ile ne değişti" için düzen: (1) Çekirdek — "en büyük değişim desen eşleştirme: tip denetimi, dönüştürme ve null kontrolü tek ifadeye indi; koşul zincirleri switch ifadesiyle okunur bir karar tablosuna dönüştü." (2) İkinci katman — "tören azaltan sözdizimi: en üst düzey ifadeler, dosya kapsamlı namespace, global using, hedef tipli new, birincil yapıcılar, koleksiyon ifadeleri." (3) Sürüm bilinci — "dil sürümü hedef çerçeveye bağlıdır; kurumsal projede LTS ve LangVersion'a elle dokunmama disiplini vardır." (4) Kıdem vuruşu — "her yeni özelliği kullanmam; ölçüt bir kez okumada anlaşılmaktır. is not null ve using bildirimini tereddütsüz kullanırım çünkü hata azaltırlar; ama when'lerle şişmiş bir switch'i polimorfizme çeviririm. Modernleştirmeyi analizörlerle yapar, biçim commit'ini davranış commit'inden ayırırım."