Sem Göksu
Sem Göksu
Yazılım · Yolculuk · Fenerbahçe
C#

C# Ustası — Bölüm 5: Generic Math, Static Abstracts ve Compile-Time Çoklubirim Tasarımı

C# 11 ile gelen static abstract interface uyeleri ve .NET 7 Generic Math: INumber, IAdditionOperators, CRTP tarzi self-referential constraints, generic Sum/Vector ornekleri, unit-of-measure tasarimi, JIT specialization ve limitasyonlar. Serinin son bolumu.

05 Mart 2026 9 dk okuma 7 0

Merhaba arkadaşlar, C# Ustası serisinin beşinci ve son bölümüne geldik. Bu yazıda C# 11'le birlikte gelen ama gerçek olgunluğunu .NET 7 - 9 boyunca kazanan çok özel bir özelliğe bakacağız — Generic Math, yani static abstract interface üyeleri. Dil yıllarca bu özellik için kıvrandı; F# ve Rust gibi dillerde doğal olan bir şey C#'ta uzun yıllar "yapılamaz" olarak bildik. Başlamadan bir kahve alın, çünkü bu sefer dilin en genç yeteneğini detaylıca konuşacağız.

Eski Dert — Generic Toplam Neden Yazılamıyordu?
Çok temel bir ihtiyaç düşünün:

// Hayal edilen ama C# 10 ve öncesinde YAZILAMAZ:
public static T Topla<T>(IEnumerable<T> items) where T : ???
{
    T toplam = default;
    foreach (var x in items) toplam += x; // T.operator +(T, T) yok!
    return toplam;
}

Bu method int, double, decimal için ayrı ayrı yazmak zorundaydık. Sebep basit: + operatörü primitive tiplerde IL seviyesinde özel komutlar (add, add.ovf). Interface'te tanımlanamayacağı için constraint olarak talep edilemezdi.

Yıllarca çözüm olarak:

- Delegate enjeksiyonu: Func<T,T,T> toplayici parametresi geçmek — ugly ve yavaş (delegate çağrısı).
- dynamic kullanmak: ((dynamic)x + (dynamic)y) — runtime binding, çok yavaş, tipsiz.
- Expression tree derleme: Başlangıçta maliyetli, sonra hızlı ama karmaşık.
- MathNet gibi kütüphaneler, her tip için elle yazılmış özelleşmeler.

Hepsi çirkin. İşte static abstract interface members bu hikayenin sonuna geldi.

static abstract — Nedir, Nasıl Çalışır?
C# 11'de gelen bu özellik, interface'lerde statik üye talep etmeyi mümkün kıldı:

public interface ISayilabilir<TSelf> where TSelf : ISayilabilir<TSelf>
{
    static abstract TSelf Sifir { get; }
    static abstract TSelf operator +(TSelf a, TSelf b);
}

public readonly struct Para : ISayilabilir<Para>
{
    public decimal Miktar { get; }
    public Para(decimal m) => Miktar = m;

    public static Para Sifir => new(0m);
    public static Para operator +(Para a, Para b) => new(a.Miktar + b.Miktar);
}

Önemli detay: TSelf generic parametresi ve where TSelf : ISayilabilir<TSelf> kısıtı. Bu CRTP (Curiously Recurring Template Pattern) olarak bilinen fonksiyonel-bilgisayar-bilimi tekniğinin C# versiyonu — "bir tip kendisini referans ederek" diye düşünün.

Generic Math — INumber<T> ve Arkadaşları
.NET 7'de framework kendi static abstract interface ailesini tanıttı:

- INumber<T>: Tüm sayı tipleri için temel interface.
- IAdditionOperators<TSelf, TOther, TResult>: + operatörü.
- ISubtractionOperators<..>: - operatörü.
- IMultiplyOperators<..>: * operatörü.
- IDivisionOperators<..>: / operatörü.
- IEqualityOperators<..>: ==, !=.
- IComparable<T>: <, >.
- IMinMaxValue<T>: T.MinValue, T.MaxValue.
- INumberBase<T>: T.Zero, T.One, T.Abs, T.Parse.

Bu interface'ler int, long, double, decimal, BigInteger, Half, float gibi tüm sayı tipleri üzerinde implement edilmiş durumda. Yani:

public static T Topla<T>(IEnumerable<T> items) where T : INumber<T>
{
    T toplam = T.Zero;
    foreach (var x in items) toplam += x;
    return toplam;
}

// Hem int hem double hem BigInteger hem decimal ile çalışır:
Console.WriteLine(Topla(new[] { 1, 2, 3 }));                    // 6
Console.WriteLine(Topla(new[] { 1.5, 2.5 }));                   // 4.0
Console.WriteLine(Topla(new[] { BigInteger.Parse("10"), BigInteger.Parse("20") })); // 30

20 yıllık C# kariyerimizin isyan ettiği bir problem — bir generic metod. Kuvvet güzel, basitlik daha güzel.

Visual Studio tarafında bu dünyayı ilk keşfederken yaptığım şey şuydu: boş bir method imzasına where T : yazıp Ctrl+Space'e basmak. IntelliSense size tüm numeric interface hiyerarşisini sıralar — INumber, IFloatingPoint, IBinaryInteger, ISignedNumber, ITrigonometricFunctions. Sonra INumber<T> yazıp metod içinde T. yazdığınızda IntelliSense Zero, One, Abs, Parse, TryParse, Min, Max, CreateChecked, CreateSaturating, CreateTruncating gibi onlarca static abstract üyeyi çıkarır. Bu liste aslında özelliğin ne kadar derin olduğunu da gösterir — tüm numerik algoritma kütüphanelerini artık generic yazabilirsiniz.

Generic Math INumber<T> autocomplete
Visual Studio 2022 — INumber<T> üzerinde static abstract üye IntelliSense listesi (Zero, One, Abs, Parse, CreateChecked)

Pratik Örnek: Generic Vektör Hesapları

public readonly struct Vector<T> where T : INumber<T>
{
    public T X { get; }
    public T Y { get; }

    public Vector(T x, T y) { X = x; Y = y; }

    public static Vector<T> operator +(Vector<T> a, Vector<T> b)
        => new(a.X + b.X, a.Y + b.Y);

    public static Vector<T> operator *(Vector<T> a, T scalar)
        => new(a.X * scalar, a.Y * scalar);

    public T DotProduct(Vector<T> other) => X * other.X + Y * other.Y;

    public override string ToString() => $"({X}, {Y})";
}

// Kullanım
var v1 = new Vector<int>(3, 4);
var v2 = new Vector<int>(1, 2);
Console.WriteLine(v1 + v2);              // (4, 6)
Console.WriteLine(v1.DotProduct(v2));    // 11

var v3 = new Vector<double>(3.5, 4.5);
var v4 = new Vector<double>(1.0, 2.0);
Console.WriteLine(v3 * 2.0);             // (7, 9)

Aynı struct hem int, hem double, hem decimal ile çalışıyor. Eskiden bunun için VectorInt, VectorDouble, VectorDecimal diye üç sınıf yazıyorduk. Şimdi tek struct ile tamamı.

Self-Referential Generic Constraints — CRTP Detayı
TSelf : IAdditionOperators<TSelf, TSelf, TSelf> ilk görüldüğünde yadırgatıcı olabilir. Şu fikirden yola çıkın: "Ben kendimi aynı tipe ekleyebilir, sonuçta yine kendi tipimi veririm." Matematiksel olarak bu bir commutative group veya ring tanımına yaklaşır.

public static T IkiKatla<T>(T x) where T : IAdditionOperators<T, T, T>
{
    return x + x;
}

// Tipler farklı olduğunda:
public static TResult Carp<TLeft, TRight, TResult>(TLeft l, TRight r)
    where TLeft : IMultiplyOperators<TLeft, TRight, TResult>
{
    return l * r;
}

// Örnek: DateTime + TimeSpan = DateTime — farklı tipler
public static DateTime IleriAt(DateTime dt, TimeSpan ts)
    where DateTime : IAdditionOperators<DateTime, TimeSpan, DateTime>
{
    return dt + ts;
}

Dikkat: TSelf : IAdditionOperators<TSelf, TSelf, TSelf> ile INumber<TSelf> çoğu zaman aynı fonksiyonelliği sağlar ama INumber daha geniş bir sözleşmedir (Zero, One, Parse, vs. ister).

Unit of Measure — Gerçek Dünya Senaryosu
Generic Math'in en çok kullanıldığı yerlerden biri ölçü birimi sistemleri. Aşağıdaki örnekte metre ve saniye birbirine karıştırılamaz hale geliyor:

public readonly record struct Metre(double Value) :
    IAdditionOperators<Metre, Metre, Metre>
{
    public static Metre operator +(Metre a, Metre b) => new(a.Value + b.Value);
    public override string ToString() => $"{Value} m";
}

public readonly record struct Saniye(double Value)
{
    public override string ToString() => $"{Value} s";
}

public readonly record struct Hiz(double MSaniye)
{
    public override string ToString() => $"{MSaniye} m/s";
}

// Metre / Saniye = Hiz (custom operator)
public static class FizikOp
{
    public static Hiz Bol(Metre m, Saniye s) => new(m.Value / s.Value);
}

// Kullanım
var yol = new Metre(100);
var zaman = new Saniye(9.58);
var hiz = FizikOp.Bol(yol, zaman);
Console.WriteLine(hiz); // 10.44 m/s

// Yanlış kullanım artık DERLEME hatası:
// Metre yanlis = zaman + new Metre(5); // Error: can't add Saniye to Metre

Domain-Driven Design'ın value object yaklaşımı artık performans kaybı olmadan yapılabilir.

JIT Specialization — Performans Nasıl Sağlanıyor?
"Generic koda operator çağrısı runtime'da yavaşlamaz mı?" sorusu haklı bir endişe. Cevap: JIT runtime'da generic tipi özelleştirir. Topla<int> ve Topla<double> farklı iki IL işlemi derlenmiş gibi çalışır — biri add, diğeri fadd kullanır.

Ama dikkat: bu özelleştirme value type'larda olur. Reference type'ta aynı IL kalır, sanal çağrı yapılır. Yani Topla<string> yazmak INumber implement etmediği için zaten derlenmez.

Kısaca: Generic Math struct'lar için sıfır maliyetli, class'lar için az bir ek yük gerektirir (ama yine de interface dispatch'ten daha iyi).

Visual Studio tarafında generic math kodu yazarken en sık karşılaşacağınız problem: "bu metod için gereken interface'i unuttum" uyarıları. Editör "CS0019: operator '+' cannot be applied" veya "CS8703" gibi hatayı gösterdiğinde üstüne Ctrl+. tuşlayıp "Add missing interface to type parameter" seçeneğini kullanabilirsiniz — Roslyn eksik constraint'i imzaya ekler. Debug sırasında T.Zero'nun gerçek runtime değerini Watch penceresine yazarsanız, generic çözünürlüğü çalışır ve değer görünür; bu JIT'in tipi uzman hale getirdiğinin bir göstergesi. Bir tipin sizin tanımladığınız interface'leri implement edip etmediğini hızlıca kontrol etmek için class adının üstüne sağ tık > View Class Hierarchy deyip açılan pencerede implement edilen tüm interface'leri ağaç olarak görebilirsiniz.

Limitasyonlar

- Kod şişmesi: JIT her struct kombinasyonu için ayrı kod üretir. Vector<int>, Vector<double>, Vector<decimal> ayrı native kodlar. Kütüphane genişse assembly boyutunu büyütür.
- Çift-tip operator: Vector<T> * T gibi farklı tip operatörleri tanımlamak CRTP pattern'inden daha karmaşık.
- Mevcut tipler: Custom struct'larınıza INumber implement etmek çok iş — yaklaşık 30 method.
- Conversion operatorleri: intdouble gibi implicit/explicit conversion'lar static abstract'a uymuyor. Manuel olarak TOut T.CreateChecked<TIn>(TIn) gibi static method'lar gerekir.
- Öğrenme eğrisi: CRTP tip imzası junior'ları korkutur. Takım eğitimi şart.

Tuzaklar ve Anti-Pattern'ler

- INumber'a gereksiz bağlılık: Sadece + lazımsa IAdditionOperators kullanın. INumber aşırı geniş.
- Overflow bilinci: int ile T.Zero toplayan generic metodda overflow kontrolü tipte yapılır. checked context'e dikkat.
- Statik state beklemek: T.Sifir gibi property'ler her çağrıda yeni obje üretebilir. Cache gerekirse kendi tipinizde manuel yapın.
- Generic'in sınırını bilmek: Her problem generic math değil. Bazen 3 overload yazmak, generic tanımlamaktan daha temiz.

Ne Zaman Generic Math KULLANMAYALIM?

- Sadece int için yazıyorsanız: Generic yapmak overkill. Doğrudan int ile yazın.
- Küçük bir utility method: int Topla(int a, int b) => a + b; — generic versiyonuna gerek yok.
- Ekip deneyimsizse: CRTP pattern'i bilmeyen ekipte maintenance yükü artar.
- Performance profilinde darboğaz değilse: Generic math biraz daha fazla kod şişirir. Assembly büyüklüğüne hassas uygulamalarda dikkatli olun.

Gerçek Dünyadan Kullanımlar

- ML.NET, TorchSharp: Tensor operasyonları generic math ile yazılıyor. Yeni tipler (Half, BFloat16) kolay ekleniyor.
- NumPy benzeri kütüphaneler: Generic N-dim array hesapları.
- Finans: decimal tabanlı fiyat/miktar hesapları, FX conversion.
- Oyun motorları: float/double arası geçiş yapan fizik motorları.
- Bilimsel hesap: BigInteger ile yüksek hassasiyet, Half ile bellek-duyarlı neural net.

Serinin Genel Tartışması — Hard Konulardan Ne Öğrendik?
5 bölümü geride bıraktık. Tek bir cümlede toparlarsak: C# dili artık low-level performans ile high-level ifadeselliğin ikisini birden sağlıyor. Source generator'lar ile derleme zamanı metaprogramming, Span ile sıfır allocation, async state machine ile verimli concurrency, pattern matching ile fonksiyonel tarz, ve generic math ile matematiksel soyutlama.

Hepsi tek bir dilde — C++ seviyesinde kontrol, F# seviyesinde ifade. Bu yüzden hala C# öğrenmeye değer diyorum.

Sonraki Adımlar
Bu seride değinmediğimiz ama ileri konular:

- System.Numerics.Vector<T> ve SIMD (vektörleştirilmiş aritmetik).
- Native AOT ve reflection olmayan publish stratejileri.
- Hardware intrinsics (System.Runtime.Intrinsics) — CPU spesifik talimatlarla %100+ hız.
- Channels ve Pipes ile yüksek perf producer/consumer pattern'leri.
- Minimal APIs'in endpoint binding source generator çıktılarını okumak.
- DateOnly/TimeOnly ve Roslyn'in immutable tip optimizasyonları.

Bunların her biri ayrı bir yazı olacak — göreceğiz.

Bu seriyi buraya kadar getirdik. Umarım faydalı olmuştur — sorun olursa yoruma atın!

Paylaş:

Yorumlar (0)

Henüz yorum yok. İlk yorumu sen yap!

Yorum bırak

* Yorumlar moderasyon sonrası yayınlanır. E-posta gizli tutulur.