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

C# 14 Yenilikleri: Extension Members, field ve Null-Conditional Atama

Mert Özen 7 Kas 2026 8 dk 4 görüntülenme
C# 14 Yenilikleri: Extension Members, field ve Null-Conditional Atama

C# 10 ve C# 13'teki karşılıklarıyla C# 14: null-conditional atamada sağ tarafın atlanması, extension property, static extension member ve extension operatör, field keyword'ü, array'lerin span'e dönüşümü, nameof(List<>), lambda parametre modifier'ları, partial constructor/event ve compound atama operatörleri.

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

Önce Tahmin Et

C# 14 ile ?. operatörü atamanın sol tarafına da geçebiliyor. Aşağıda iki müşteri arıyoruz; biri bulunuyor, diğeri bulunmuyor. Sonra ikisine de son sipariş tutarını yazıyoruz. CalculateTotal kaç kez çalışır? Ekranda kaç satır görürsün?

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

Customer? ada = FindCustomer("ada@example.com");
Customer? nobody = FindCustomer("nobody@example.com");

ada?.LastOrderTotal = CalculateTotal("ada");
nobody?.LastOrderTotal = CalculateTotal("nobody");

Console.WriteLine(ada?.LastOrderTotal);

static Customer? FindCustomer(string email) =>
    email == "ada@example.com" ? new Customer { Email = email } : null;

static decimal CalculateTotal(string who)
{
    Console.WriteLine($"calculating for {who}");
    return 80.5m;
}

public class Customer
{
    public string Email { get; set; } = "";
    public decimal LastOrderTotal { get; set; }
    public int Points { get; set; }
}

Null-Conditional Atama

Çıktı iki satır:

calculating for ada
80.5

CalculateTotal yalnızca bir kez çalıştı. nobody null olduğu için atamanın sağ tarafı hiç değerlendirilmedi. Kural basit: sol taraf null ise expression'ın tamamı atlanır, sağ taraf dahil. C# 10'da aynı şeyi bir if ile yazardın:

Customer? ada = new Customer();
if (ada != null)
    ada.Points += 10;
Console.WriteLine(ada?.Points);

C# 14'te compound atama da çalışıyor:

Customer? ada = new Customer();
Customer? nobody = null;

ada?.Points += 10;
nobody?.Points += 10;
Console.WriteLine($"{ada?.Points} {nobody?.Points}");

public class Customer
{
    public int Points { get; set; }
}
10 

İlk değer 10, ikincisi boş, çünkü nobody?.Points null. ++ ve -- ise bilerek dışarıda bırakıldı: c?.Points++; satırı error CS1059: The operand of an increment or decrement operator must be a variable, property or indexer verdi. C# 13 ile derleyince: error CS9260: Feature 'null conditional assignment' is not available in C# 13.0. Please use language version 14.0 or greater. Bu yazıda kıyas için C# 13'ü kullandım. C# 14'ten önceki her sürüm bu özellikleri reddediyor, yalnızca hata kodu değişiyor: C# 10 ile aynı satır error CS8936: Feature 'null conditional assignment' is not available in C# 10.0. Please use language version 14.0 or greater. veriyor.

Extension Member'lar

Extension method'lar C# 3'ten beri var ama hep "method" olarak kaldılar. C# 10'da bir kitabın pahalı olup olmadığını söyleyen bir extension şöyle görünüyordu:

public static class BookExtensions
{
    public static bool IsExpensive(this Book book) => book.Price > 40m;
}

Çağrı da parantezli: book.IsExpensive(). Extension property, bir tipe static member eklemek ya da extension operatör yazmak mümkün değildi. C# 14 bunları extension bloğu ile açtı:

public record Book(string Title, decimal Price);

public static class BookExtensions
{
    extension(Book book)
    {
        public bool IsExpensive => book.Price > 40m;
    }

    extension(Book)
    {
        public static Book Placeholder => new("Untitled", 0m);
    }

    extension(IEnumerable<Book> books)
    {
        public decimal TotalPrice => books.Sum(b => b.Price);
    }
}

books[0].IsExpensive True, books.TotalPrice 80.5, Book.Placeholder.Title de Untitled yazdırdı. extension(Book book) bloğu bir receiver tanımlıyor; içindeki her member o receiver'ı kullanabiliyor. extension(Book) ise isimsiz receiver; içindeki static member'lar sanki Book'un kendisine eklenmiş gibi çağrılıyor. Receiver bir interface de olabiliyor; TotalPrice her IEnumerable<Book> üzerinde çalışıyor. Operatörler de aynı yoldan geliyor:

using System.Globalization;

var a = new Money(10m);
var b = new Money(5.5m);
Console.WriteLine((a + b).Amount.ToString(CultureInfo.InvariantCulture));

public record Money(decimal Amount);

public static class MoneyExtensions
{
    extension(Money)
    {
        public static Money operator +(Money left, Money right) => new(left.Amount + right.Amount);
    }
}
15.5

Klasik this parametreli extension method'lar aynen çalışmaya devam ediyor; mevcut kodunu değiştirmen gerekmiyor. Kendi tipine dokunamadığın durumlarda, örneğin bir NuGet paketinden gelen bir DTO'ya hesaplanmış bir property eklemek istediğinde, bu syntax çok daha doğal.

field Keyword'ü

Bir property'nin setter'ına küçük bir mantık eklemek istediğinde C# 10'da auto-property'den vazgeçip backing field'ı elle yazmak zorundaydın:

public class Author
{
    private string _name = "";

    public string Name
    {
        get => _name;
        set => _name = value.Trim();
    }
}

C# 14'te field compiler'ın ürettiği backing field'ı temsil ediyor:

public class Author
{
    public required string Name
    {
        get;
        set => field = value.Trim();
    }
}

new Author { Name = " Robert C. Martin " } için iki sürüm de [Robert C. Martin] yazdırdı. get auto-accessor olarak kaldı, yalnızca set'e mantık ekledik. Field'a isim vermek ya da class'ın başka bir yerinden yanlışlıkla ona doğrudan yazmak gibi dertler kalmıyor; field'a yalnızca property'nin accessor'larından ulaşılıyor. C# 13'teki hata: error CS9260: Feature 'field keyword' is not available in C# 13.0. Please use language version 14.0 or greater. C# 13'te bu özellik yalnızca preview olarak vardı. Bir uyarı: class'ında zaten field adında bir member varsa C# 14 ile anlamı değişir; bunu sürüm yükseltmeyi anlattığımız sonraki yazıda gerçek bir uyarıyla göreceğiz.

Span Dönüşümleri

C# 14 array ile Span<T>/ReadOnlySpan<T> arasındaki dönüşümleri dilin bir parçası yaptı. En görünür sonucu extension method'larda:

public static class SpanExtensions
{
    public static decimal Total(this ReadOnlySpan<decimal> values)
    {
        decimal total = 0;
        foreach (decimal value in values)
            total += value;
        return total;
    }
}

decimal[] prices = [42.5m, 38m, 55m]; için prices.Total() C# 14'te 135.5 yazdırdı. Aynı kod C# 13'te derlenmiyor:

Program.cs(2,19): error CS1929: 'decimal[]' does not contain a definition for 'Total' and the best extension method overload 'SpanExtensions.Total(ReadOnlySpan<decimal>)' requires a receiver of type 'System.ReadOnlySpan<decimal>'

Array'den span'e dönüşüm eskiden de vardı, ama bir user-defined dönüşüm olarak; extension method receiver'ı için dikkate alınmıyordu. Artık alınıyor, generic type inference da span'leri daha iyi anlıyor. Bu iyileştirme bir de breaking change getirdi: array.Reverse() gibi bir çağrı artık LINQ yerine span'in yerinde ters çeviren Reverse'üne bağlanabiliyor. Bunun gerçek hatasını da sonraki yazıda göstereceğim.

nameof ve Lambda Parametre Modifier'ları

İki küçük ama hoş yenilik. nameof artık tip argümanı verilmemiş generic tipleri kabul ediyor:

Console.WriteLine(nameof(List<>));
Console.WriteLine(nameof(Dictionary<,>));
List
Dictionary

C# 10'da nameof(List<int>) gibi rastgele bir tip argümanı uydurman gerekiyordu. C# 13'teki hata: Feature 'unbound generic types in nameof operator' is not available in C# 13.0.

İkincisi lambda'larda. out parametreli bir delegate için C# 10'da lambda parametrelerinin tiplerini tek tek yazmak zorundaydın. C# 14'te modifier'ı yazmak yetiyor, tip delegate'ten çıkarılıyor:

TryParser<decimal> parse = (text, out result) => decimal.TryParse(text, CultureInfo.InvariantCulture, out result);
Console.WriteLine(parse("42.5", out decimal parsed) ? parsed : -1);

public delegate bool TryParser<T>(string text, out T result);
42.5

out gibi modifier'lar için tip yazmak gerekmiyor; params ise hâlâ tip istiyor. C# 13'teki hata: Feature 'simple lambda parameter modifiers' is not available in C# 13.0.

Partial Constructor, Partial Event ve Compound Atama Operatörü

C# 13'ün partial property'lerinden sonra sıra constructor ve event'lerde:

var book = new Book("  Clean Code ");
Console.WriteLine($"[{book.Title}]");

public partial class Book
{
    public partial Book(string title);

    public string Title { get; private set; } = "";
}

public partial class Book
{
    public partial Book(string title)
    {
        Title = title.Trim();
    }
}
[Clean Code]

Event için de aynı pattern: bir parçada public partial event EventHandler? StockChanged;, diğerinde add ve remove accessor'larıyla implementasyon. Bunu da denedim, subscribe olan lambda stock changed yazdı. Partial property'lerde olduğu gibi tipik kullanım, bir parçasını source generator'ın ürettiği kod.

Son büyük yenilik, kendi compound atama operatörünü yazabilmek. C# 10'da cart += 42.5m yazdığında compiler bunu cart = cart + 42.5m'ye çevirirdi; yani + operatörü her seferinde yeni bir object üretmek zorundaydı. C# 14'te +='yi instance üzerinde yerinde çalışan bir operatör olarak tanımlayabiliyorsun:

var cart = new CartTotal();
cart += 42.5m;
cart += 38m;
Console.WriteLine(cart.Value);

public class CartTotal
{
    public decimal Value { get; private set; }

    public void operator +=(decimal amount) => Value += amount;
}
80.5

Operatör void dönüyor ve static değil; hedef object'i değiştiriyor. Büyük buffer ya da matris gibi kopyalaması pahalı tiplerde fark yaratır. C# 13'teki hata: Feature 'user-defined compound assignment operators' is not available in C# 13.0. Listeyi tamamlamak için: .NET 10'un dotnet run app.cs ile çalışan file-based app'leri için #:package gibi yeni direktifler ve #! shebang satırı da C# 14'ün parçası.

Sık Düşülen Tuzaklar

?. atamasının sağ tarafının her zaman çalıştığını sanmak. Tahmin sorusunda gördüğün gibi sol taraf null ise sağ taraf hiç çalışmaz. Sağ tarafta bir yan etkiye (log, sayaç, veritabanı çağrısı) güveniyorsan bu davranış seni şaşırtır.

Null olmadığını bildiğin bir değişkende ?. kullanmak. var ada = new Customer(); ada?.Points += 10; yazıp bir sonraki satırda ada.Points'i okuduğumda compiler warning CS8602: Dereference of a possibly null reference. verdi. ?. kullandığın anda flow analysis "demek ki null olabilir" diye düşünüyor.

Eski LangVersion'da anlamlı bir mesaj beklemek. Diğer özelliklerin aksine extension bloğu C# 13'te bir "feature not available" hatası vermiyor; compiler syntax'ı hiç tanımadığı için şunları söylüyor:

Program.cs(6,6): error CS1513: } expected
Program.cs(9,1): error CS1022: Type or namespace definition, or end-of-file expected

Bu hataları extension geçen bir dosyada görürsen önce dil sürümüne bak.

Extension member'ları her yere yaymak. Kendi tipine property eklemek için extension yazma; tipi değiştir. Extension, sahibi olmadığın tipler için.

Küçük Bir Deneme

★☆☆ BookExtensions'a IEnumerable<Book> için Cheapest adında bir extension property ekle ve books.Cheapest.Title ile çağır.

★★☆ Author'daki field örneğine value boşsa ArgumentException fırlatan bir kontrol ekle. required ile birlikte nasıl davranıyor?

★★★ CartTotal'a hem static operator + hem de instance operator += tanımla. cart += 1m hangisini çağırıyor, var other = cart + 1m hangisini? İkisinde de cart'ın referansı değişiyor mu?

Böylece C# 14'e, bugünkü LTS sürümü .NET 10'un diline ulaştık. Serinin son yazısında öğrendiklerimizi bir projeye uyguluyoruz: .NET 6'da kalmış bir projeyi .NET 10'a taşıyor, yol boyunca çıkan gerçek uyarıları ve breaking change'leri tek tek okuyoruz.