Mert Özen Her Satırda Daha İleri
Backend C# Sürümleri — #02

C# 11 Yenilikleri: Raw String, required ve List Pattern

Mert Özen 7 Kas 2026 8 dk 5 görüntülenme
C# 11 Yenilikleri: Raw String, required ve List Pattern

C# 10'da nasıl yazıyorduk, C# 11'de nasıl yazıyoruz: raw string literal'ler ve kültür tuzağı, required member'lar ve SetsRequiredMembers, list pattern'ler, dil ve kütüphane ayağıyla generic math, file-local tipler ve UTF-8 literal'ler.

Sürüm notu: Bu yazı C# 11 ile gelen dil yeniliklerini anlatır; kodlar C# 11 / .NET 7 hedefiyle .NET 10 SDK'da derlenip denenmiştir.

Önce Tahmin Et

C# 11'in en çok sevilen yeniliğiyle başlayalım: üç tırnakla açılan raw string literal. Aşağıdaki kod bir kitabı JSON olarak yazdırıyor. Kültürü bilerek tr-TR yaptım, çünkü Türkiye'deki bir sunucuda varsayılan bu olabilir. Çıktı geçerli bir JSON mu?

using System.Globalization;

CultureInfo.CurrentCulture = new CultureInfo("tr-TR");

string title = "Clean Code";
decimal price = 42.5m;

string json = $$"""
    {
      "title": "{{title}}",
      "price": {{price}}
    }
    """;
Console.WriteLine(json);

Raw String Literal'ler

Değil. Çıktı şu:

{
  "title": "Clean Code",
  "price": 42,5
}

42,5 JSON'da bir sayı değil; bir JSON parser bu satırda hata verir. Suç raw string'in değil, interpolation'ın: {{price}} değeri o anki kültürle string'e çevriliyor ve Türkçede ondalık ayırıcı virgül. Bu tuzağa aşağıda yine döneceğiz. Önce raw string'in ne çözdüğüne bakalım. C# 10'da aynı JSON'u şöyle yazıyorduk:

string json = $@"{{
  ""title"": ""{title}"",
  ""price"": {price},
  ""tags"": [""craft"", ""c#""]
}}";

Verbatim interpolated string'de her tırnağı iki kez, her süslü parantezi de iki kez yazman gerekiyor. Kısa bir JSON'da bile metni okumak zorlaşıyor, bir test dosyasına uzun bir payload yapıştırdığında ise tamamen kopyala-düzelt işine dönüyor. C# 11'deki karşılığı:

string json = $$"""
    {
      "title": "{{title}}",
      "price": {{price}},
      "tags": ["craft", "c#"]
    }
    """;
{
  "title": "Clean Code",
  "price": 42.5,
  "tags": ["craft", "c#"]
}

Bu sefer kültürü CultureInfo.InvariantCulture yaptım ve 42.5 geldi. Raw string en az üç tırnakla açılır ve aynı sayıda tırnakla kapanır; içinde tek ya da çift tırnak kaçış gerektirmez. Kapanış tırnaklarının bulunduğu sütun önemli: o sütunun solundaki boşluklar her satırdan silinir. Kodda dört boşluk girintili yazdığım JSON, çıktıda sıfırdan başladı. Başındaki $$ ise "interpolation için iki süslü parantez kullanacağım" demek. Böylece JSON'un kendi tek süslü parantezleri olduğu gibi kalıyor, {{title}} ise değişkenin değerini yazıyor. Raw string içeren küçük bir test dosyasını LangVersion 10 ile derleyince:

Program.cs(2,15): error CS8936: Feature 'raw string literals' is not available in C# 10.0. Please use language version 11.0 or greater.

required Member'lar

C# 10'da bir Book'un ISBN'siz yaratılmasını engellemenin tek sağlam yolu constructor'dı:

public class Book
{
    public Book(string isbn, string title, decimal price)
    {
        Isbn = isbn;
        Title = title;
        Price = price;
    }

    public string Isbn { get; }
    public string Title { get; }
    public decimal Price { get; }
}

Çalışır ama object initializer'ın okunaklılığını kaybedersin; property sayısı arttıkça constructor parametre listesi de uzar. C# 11 bunun yerine required modifier'ını getirdi:

public class Book
{
    public required string Isbn { get; init; }
    public required string Title { get; init; }
    public decimal Price { get; init; }

    public override string ToString() => $"{Title} ({Isbn}) {Price}";
}

new Book { Isbn = "978-0132350884", Title = title, Price = price } derlenir ve Clean Code (978-0132350884) 42.5 yazdırır. Isbn'i unutup var book = new Book { Title = "Clean Code" }; yazarsan compiler durdurur:

Program.cs(1,16): error CS9035: Required member 'Book.Isbn' must be set in the object initializer or attribute constructor.

Price zorunlu değil, verilmezse 0 kalır. Kural compile time'da işliyor. C# 10 ile derleyince tanıdık pattern geliyor: Feature 'required members' is not available in C# 10.0. Please use language version 11.0 or greater.

List Pattern'ler

Bir siparişteki kalem adetlerini tek satırda anlatan bir fonksiyon düşün. C# 10'da uzunluk kontrolleri ve index'lerle yazardın:

static string Describe(int[] quantities) =>
    quantities.Length == 0 ? "empty order"
    : quantities.Length == 1 ? $"one line, qty {quantities[0]}"
    : $"{quantities.Length} lines, first {quantities[0]}, last {quantities[^1]}";

C# 11 ile array'in şeklini doğrudan pattern olarak yazabiliyorsun:

static string Describe(int[] quantities) => quantities switch
{
    [] => "empty order",
    [var single] => $"one line, qty {single}",
    [var first, .., var last] => $"{quantities.Length} lines, first {first}, last {last}"
};
empty order
one line, qty 2
4 lines, first 2, last 5

[] boş array'i, [var single] tek element'li olanı yakalıyor. .. slice pattern'i "arada kaç element olursa olsun" demek; ilk ve son element'i aynı anda isimlendirmeni sağlıyor. Asıl kazanç okunaklılıktan çok güvenlik: index'e elle dokunmadığın için quantities[0]'ı boş array'de okuyup exception almak gibi bir hata yapamıyorsun. C# 10'daki hata: Feature 'list pattern' is not available in C# 10.0. Please use language version 11.0 or greater.

Generic Math: Dil ve Kütüphane Birlikte

C# 10'da "herhangi bir sayı tipinin toplamını alan tek bir generic method" yazamıyordun. T için + operatörünü ya da sıfır değerini isteyebileceğin bir constraint yoktu; LINQ'in Sum'ının int, long, decimal, double için ayrı ayrı overload'larla gelmesi de bu yüzden. C# 11 ve .NET 7 ile:

using System.Numerics;

public static class Pricing
{
    public static T Sum<T>(IEnumerable<T> values) where T : INumber<T>
    {
        T total = T.Zero;
        foreach (T value in values)
            total += value;
        return total;
    }
}
6
80.5
0.30000000000000004

Aynı method int, decimal ve double array'leriyle çalıştı; son satırdaki kayan nokta sonucu da double'ın bildiğimiz davranışı. T.Zero ve += mümkün oldu, çünkü INumber<T> ve onun miras aldığı interface'ler bunları static abstract member olarak tanımlıyor. Bu özelliğin iki ayağı var ve ikisini ayrı ayrı kırarak gördüm. net7.0 hedefinde LangVersion 10 iken dil ayağı eksik:

Program.cs(7,19): error CS8936: Feature 'static abstract members in interfaces' is not available in C# 10.0. Please use language version 11.0 or greater.
Program.cs(9,13): error CS8936: Feature 'static abstract members in interfaces' is not available in C# 10.0. Please use language version 11.0 or greater.

net6.0 hedefinde ise LangVersion'ı 11 yapsan bile kütüphane ayağı eksik, çünkü INumber<T> .NET 6'nın BCL'inde yok:

Program.cs(5,61): error CS0246: The type or namespace name 'INumber<>' could not be found (are you missing a using directive or an assembly reference?)

Generic math'i kendi generic collection'larında nasıl kullanacağını C# Koleksiyonlar ve Generics serisinin onuncu yazısında ayrıntılı görüyoruz.

file-local Tipler ve UTF-8 Literal'ler

Bir dosyanın içinde kalması gereken küçük bir yardımcı class için C# 10'da elindeki en dar erişim internal'dı; yani bütün assembly onu görebiliyordu. C# 11'in file modifier'ı tipi tek bir dosyaya hapseder:

using System.Globalization;

public static class Invoice
{
    public static string Line(string title, decimal price) => $"{title}: {PriceFormatter.Format(price)}";
}

file static class PriceFormatter
{
    public static string Format(decimal price) => price.ToString("0.00", CultureInfo.InvariantCulture) + " TRY";
}

Aynı projede Label.cs dosyası da kendi file static class PriceFormatter'ını tanımlıyor ve ikisi çakışmıyor. Program.cs'te Invoice.Line("Clean Code", 42.5m) çağrısı Clean Code: 42.50 TRY yazdırdı; ama PriceFormatter.Format'ı doğrudan çağırmayı denediğimde error CS0103: The name 'PriceFormatter' does not exist in the current context geldi.

UTF-8 string literal'i ise HTTP ve protokol kodunda işe yarar. C# 10'da byte'lara Encoding.UTF8.GetBytes("application/json") ile ulaşırdın; bu her çağrıda yeni bir array demek. C# 11'de u8 son ekiyle literal doğrudan ReadOnlySpan<byte> olur:

ReadOnlySpan<byte> contentType = "application/json"u8;
Console.WriteLine($"{contentType.Length} bytes, first: {contentType[0]}");
16 bytes, first: 97

97, küçük a harfinin UTF-8 kodu. C# 10'daki hata: Feature 'UTF-8 string literals' is not available in C# 10.0. Please use language version 11.0 or greater.

Küçük Ama Kullanışlı Diğerleri

C# 11'in listesi bunlarla bitmiyor. Generic attribute'lar artık preview değil: class MyAttr<T> : Attribute yazıp typeof parametresi yerine tip argümanı verebiliyorsun. Interpolation içindeki expression'lar birden fazla satıra yayılabiliyor. Struct'larda constructor'ın atamadığı field'lar otomatik olarak default değerini alıyor (auto-default structs). nameof, method üzerindeki attribute'larda parametre adlarını görebiliyor. >>> işaretsiz sağa kaydırma operatörü ve checked user-defined operatörler geldi. nint ve nuint, IntPtr ve UIntPtr'ın alias'ı olan basit tipler hâline geldi. Performans tarafında ref field'lar ve scoped ref var. Son olarak warning wave 7 geldi; küçük harfli tip isimlerine verilen CS8981 uyarısını sürüm yükseltmeyi anlattığımız yazıda gerçek bir projede göreceğiz.

Sık Düşülen Tuzaklar

Raw string'i JSON üretme aracı sanmak. Tahmin sorusunda gördüğün gibi raw string yalnızca metni yazmayı kolaylaştırır. Kültüre bağlı biçimlendirme ve kaçış senin sorumluluğunda kalır. Kitap adında bir tırnak işareti olsaydı JSON yine bozulurdu. Test verisi ya da log mesajı için raw string harika; API'ye gönderdiğin gerçek payload için JsonSerializer kullan.

required'ı doğrulama sanmak. Compiler yalnızca property'nin initializer'da atanıp atanmadığına bakar, değerin anlamına bakmaz. Birazdan göreceğin örnekteki new Book { Isbn = "", Title = "Refactoring" } sorunsuz derlendi ve Isbn boş string olarak kaldı.

Constructor ekleyince required'ın ortadan kalktığını sanmak. Tüm property'leri atayan bir constructor yazsan bile compiler bunu bilmez. new Book("978-0132350884", "Clean Code") çağrısı, constructor işaretsizken şu iki hatayı verdi:

Program.cs(1,13): error CS9035: Required member 'Book.Isbn' must be set in the object initializer or attribute constructor.
Program.cs(1,13): error CS9035: Required member 'Book.Title' must be set in the object initializer or attribute constructor.

Çözüm, constructor'ı [SetsRequiredMembers] ile işaretlemek:

using System.Diagnostics.CodeAnalysis;

var a = new Book("978-0132350884", "Clean Code");
var b = new Book { Isbn = "", Title = "Refactoring" };
Console.WriteLine($"[{a.Title}] [{b.Isbn}]");

public class Book
{
    public Book() { }

    [SetsRequiredMembers]
    public Book(string isbn, string title)
    {
        Isbn = isbn;
        Title = title;
    }

    public required string Isbn { get; init; }
    public required string Title { get; init; }
}
[Clean Code] []

Bu attribute bir söz: "bu constructor gerçekten hepsini atıyor" diyorsun ve compiler sana güveniyor. Sonradan yeni bir required property eklersen o sözü güncellemeyi unutma.

Generic math'i .NET 6 projesinde kullanmaya çalışmak. Yukarıda gördüğün gibi LangVersion'ı yükseltmek yetmez; INumber<T> için .NET 7 ya da sonrasını hedeflemen gerekir.

Küçük Bir Deneme

★☆☆ Tahmin sorusundaki JSON'u JsonSerializer.Serialize(new { title, price }) ile üret ve tr-TR kültüründe çıktının değişip değişmediğine bak.

★★☆ Describe'a, ilk kalemi 10'dan büyük olan siparişler için ayrı bir arm ekle: [> 10, ..]. Arm'ların sırası sonucu nasıl etkiliyor?

★★★ Pricing'e INumber<T> kullanan bir Average<T> yaz. Boş bir collection'da ne yapmalı, int için bölme nasıl davranıyor?

Bir sonraki yazıda C# 12'ye geçiyoruz: primary constructor'lar, köşeli parantezli collection expression'lar, lambda'larda default parametreler ve using ile her tipe alias verebilmek. Primary constructor'ın record'lardakinden neden farklı davrandığını da orada göreceğiz.