C# 12, .NET 8 ile birlikte Kasım 2023'te yayınlandı. Dilin okunabilirliğini ve geliştirici deneyimini iyileştiren bu sürüm, özellikle büyük kod tabanlarında fark yaratan birkaç önemli yenilik getirdi.

1. Primary Constructors

C# 12'nin en çok konuşulan özelliği primary constructors oldu. Sınıf ve struct'ların parametre listesini doğrudan tanım satırına yazabiliyorsunuz.

Eski Yöntem

public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly ILogger<OrderService> _logger;
    private readonly IEmailService _email;

    public OrderService(
        IOrderRepository repository,
        ILogger<OrderService> logger,
        IEmailService email)
    {
        _repository = repository;
        _logger = logger;
        _email = email;
    }

    public async Task<Order?> GetByIdAsync(int id)
    {
        _logger.LogInformation("Order {Id} isteniyor", id);
        return await _repository.FindAsync(id);
    }
}

Primary Constructor ile

public class OrderService(
    IOrderRepository repository,
    ILogger<OrderService> logger,
    IEmailService email)
{
    public async Task<Order?> GetByIdAsync(int id)
    {
        logger.LogInformation("Order {Id} isteniyor", id);
        return await repository.FindAsync(id);
    }

    public async Task CompleteAsync(int id)
    {
        var order = await repository.FindAsync(id) ?? throw new NotFoundException(id);
        order.Complete();
        await repository.SaveAsync(order);
        await email.SendConfirmationAsync(order.CustomerEmail);
    }
}

Dikkat: Primary constructor parametreleri field değil, capture edilen değişkenlerdir. Eğer parametreyi hem constructor hem başka bir method kullanıyorsa C# onu otomatik olarak saklar. Ama sadece constructor'da kullanılıyorsa fazladan alan ayrılmaz.

Record ile Fark

// Record: otomatik property üretir, eşitlik karşılaştırması yapar
public record Point(double X, double Y);

// Class primary ctor: sadece parametre alır, property üretmez
public class Point(double x, double y)
{
    public double X { get; } = x; // Elle tanımlamak gerekir
    public double Y { get; } = y;
    public double Distance => Math.Sqrt(X * X + Y * Y);
}

2. Collection Expressions

Koleksiyon başlatma sözdizimi [...] ile birleşik hale geldi. Aynı sözdizimi array, List, Span ve daha fazlası için çalışıyor.

// Dizi
int[] primes = [2, 3, 5, 7, 11, 13];

// List<T>
List<string> cities = ["Ankara", "İstanbul", "İzmir"];

// ImmutableArray
ImmutableArray<int> scores = [100, 95, 87, 72];

// Span<T> — stack allocation
Span<byte> buffer = [0x01, 0x02, 0x03, 0xFF];

// Spread operatörü (..) ile birleştirme
int[] evens = [2, 4, 6];
int[] odds  = [1, 3, 5];
int[] all   = [..evens, ..odds, 7, 8]; // [2, 4, 6, 1, 3, 5, 7, 8]

// Metot parametresi olarak
void PrintAll(IEnumerable<string> items) { /* ... */ }
PrintAll(["bir", "iki", "üç"]);

Derleyici hedef türe bakarak en uygun koleksiyon tipini seçiyor. Bu sayede new List<string>() { ... } yerine sade [...] yazmak yeterli.

3. Inline Arrays

Performans kritik senaryolarda sabit boyutlu, stack üzerinde yaşayan diziler tanımlanabiliyor. Span<T> ile çalışan her API bunu kullanabilir.

[System.Runtime.CompilerServices.InlineArray(16)]
public struct SixteenByteBuffer
{
    private byte _element0; // Tek field yeterli; derleyici gerisini halleder
}

// Kullanım
SixteenByteBuffer buf = default;
Span<byte> span = buf;
span[0] = 0xAB;
span[1] = 0xCD;

// Örnek: küçük ID listesi için heap allocation yok
[InlineArray(8)]
private struct SmallIdBuffer { private int _; }

SmallIdBuffer ids = default;
ids[0] = 1001;
ids[1] = 1002;

Inline arrays, özellikle networking, serialization ve parser kütüphanelerinde belleği heap'e çıkarmadan işlemek için kullanılıyor.

4. Default Lambda Parameters

Lambda ifadelerine artık varsayılan parametre değeri verilebiliyor:

// Basit örnek
var multiply = (int x, int y = 2) => x * y;
Console.WriteLine(multiply(5));    // 10
Console.WriteLine(multiply(5, 3)); // 15

// Gerçek kullanım: isteğe bağlı sayfalama
Func<int, int, IEnumerable<T>> Paginate<T>(IEnumerable<T> source)
    => (int page = 1, int size = 20) => source.Skip((page - 1) * size).Take(size);

// null varsayılan değer
var log = (string message, string? level = null) =>
    Console.WriteLine($"[{level ?? "INFO"}] {message}");

5. Alias Any Type

C# 12 öncesinde using alias sadece namespace ve sınıf isimlerine uygulanabiliyordu. Artık tuple, pointer ve primitive dahil her türe alias verilebiliyor:

// Tuple alias
using Point = (double X, double Y);
using Rect  = (double Left, double Top, double Right, double Bottom);

Point origin = (0, 0);
Rect viewport = (0, 0, 1920, 1080);

// Karmaşık generic alias
using StringMap = Dictionary<string, List<string>>;
StringMap tags = new() { ["C#"] = ["oop", "functional"] };

// Güvenli array alias
using ByteBlock = byte[];

6. ref readonly Parametreler

Büyük struct'ları kopyalamadan metoda geçirmek için ref readonly kullanılıyordu ama call site'ta ref yazmak zorundaydınız. C# 12 bunu daha esnek hale getirdi:

// Kopyalama olmadan geçirme, ama değişiklik de yok
void Analyze(ref readonly Matrix4x4 matrix)
{
    // matrix değiştirilemez, ama kopyalanmadı
    Console.WriteLine(matrix.M11);
}

// C# 12'de: ref yazmadan da çağrılabilir
var m = Matrix4x4.Identity;
Analyze(in m); // 'in' ile de çalışır
Analyze(m);    // Doğrudan da geçirilebilir (warning verebilir)

Sonuç

C# 12 büyük paradigma değişikliği getirmek yerine günlük geliştiriciyi rahatlatmaya odaklandı. Primary constructors DI ağır projelerde onlarca satır boilerplate kaldırıyor. Collection expressions ise farklı koleksiyon türleri için tutarlı bir sözdizimi sağlıyor. .NET 8 LTS sürümüyle birlikte gelen bu özellikler, production projelerinizde güvenle kullanılabilir.