CSharp 15 ile Gelen Union Types
C# ile geliştirme yaparken zaman zaman bir metodun birden fazla farklı tipte değer döndürmesi ya da bir değişkenin birkaç farklı tipten birini tutması gerektiği durumlarla karşılaşıyoruz. Bu tür senaryolarda object, marker interface ya da abstract class gibi çözümlere başvuruyorduk. Fakat bunların hiçbiri tam anlamıyla tatmin edici değildi. .NET 11 Preview 2 ile birlikte duyurulan C# 15, bu soruna çok daha zarif bir çözüm getiriyor: Union Types.
Önce Neydi, Ne Sıkıntısı Vardı?
Diyelim ki bir API'den gelecek cevap ya Success, ya NotFound, ya da Error olacak. Bunu C# ile modellemenin yollarına bakalım.
object kullanımı:
public object GetApiResponse()
{
return new Success("İşlem tamamlandı.");
}
Bu yaklaşımda tip güvenliği yok. Success, NotFound ya da Error dışında herhangi bir şey de dönebilir, compiler buna engel olmaz. switch yazarken de tüm durumları kapsadığından emin olamazsın.
Abstract class veya interface kullanımı:
public abstract class ApiResponse { }
public class Success : ApiResponse { public string Message { get; set; } }
public class NotFound : ApiResponse { }
public class Error : ApiResponse { public string Details { get; set; } }
Bu biraz daha kontrollü ama "kapalı" değil. Başka biri ApiResponse miras alıp yeni bir tip ekleyebilir. Compiler da switch ifadelerinde tüm durumların karşılandığını garanti edemez. Ayrıca birbiriyle ilgisiz tipleri (string ile int gibi) aynı hiyerarşide toplamak mümkün değil.
Union Types ile Gelen Yaklaşım
C# 15, union anahtar kelimesini tanıtıyor. Bununla bir tipin sadece belirli tiplerden birini tutabileceğini derleyiciye bildiriyorsun. Kapalı bir set oluşturuyorsun ve bu set dışına çıkılamıyor.
public record class Success(string Message); public record class NotFound(); public record class Error(string Details); public union ApiResponse(Success, NotFound, Error);
Tek satırla ApiResponse tipini tanımladık. Artık buraya sadece Success, NotFound ya da Error atanabilir. Başka hiçbir tip geçemez.
ApiResponse response = new Success("Kullanıcı oluşturuldu.");
ApiResponse response2 = new NotFound();
ApiResponse response3 = new Error("Sunucuya ulaşılamadı.");
// Derleme hatası! Fish bu union'ın bir parçası değil.
// ApiResponse response4 = new Fish();
Exhaustive Pattern Matching
Union types'ın en güçlü yanı burada ortaya çıkıyor. Compiler, switch ifadesinde tüm olası tiplerin kapsandığını kontrol ediyor. Bir tane bile eksik bırakırsan derleme uyarısı alıyorsun.
string message = response switch
{
Success s => s.Message,
NotFound => "Kaynak bulunamadı.",
Error e => $"Hata: {e.Details}"
};
Şimdi bir de union'a yeni bir tip ekleyelim:
public record class Unauthorized(string Reason); public union ApiResponse(Success, NotFound, Error, Unauthorized);
Bu değişikliğin ardından yukarıdaki switch ifadesine Unauthorized case'ini eklemezsen compiler seni uyarıyor. Runtime'da patlayan bir switch yerine derleme aşamasında yakalanan bir hata. Bu tam olarak istediğimiz şey.
Domain Modeli Örneği
Bir e-ticaret uygulamasında sipariş durumlarını yönettiğimizi düşünelim:
public record class Pending(DateTime CreatedAt); public record class Shipped(string TrackingNumber, DateTime ShippedAt); public record class Delivered(DateTime DeliveredAt); public record class Cancelled(string Reason); public union OrderStatus(Pending, Shipped, Delivered, Cancelled);
Kullanımı:
public string GetStatusMessage(OrderStatus status) => status switch
{
Pending p => $"Siparişiniz {p.CreatedAt:dd.MM.yyyy} tarihinde alındı, hazırlanıyor.",
Shipped s => $"Kargonuz yolda. Takip numarası: {s.TrackingNumber}",
Delivered d => $"Siparişiniz {d.DeliveredAt:dd.MM.yyyy} tarihinde teslim edildi.",
Cancelled c => $"Sipariş iptal edildi. Sebep: {c.Reason}"
};
Yarın bir gün Returned durumu eklenirse, bu metodu güncellemeyi unutma ihtimalin artık yok. Compiler hatırlatıyor.
Union Gövdesine Metot Ekleme
Union tiplerine tıpkı sıradan bir tipe metot ekler gibi davranabiliyorsun. Ortak davranışları dışarıya sızdırmak yerine union içinde tanımlayabiliyorsun.
public union ApiResponse(Success, NotFound, Error)
{
public bool IsSuccess() => this is Success;
public string Summarize() => this switch
{
Success s => $"[OK] {s.Message}",
NotFound => "[404] Kaynak bulunamadı.",
Error e => $"[ERR] {e.Details}"
};
}
Kullanımı:
ApiResponse response = new Success("Ürün listelendi.");
Console.WriteLine(response.IsSuccess()); // true
Console.WriteLine(response.Summarize()); // [OK] Ürün listelendi.
Generic Union ile Esnek Yapılar
Union tipler generic olarak da tanımlanabiliyor. Klasik bir Result<T> pattern'ı artık çok daha temiz:
public record class Ok<T>(T Value);
public record class Err(string Message);
public union Result<T>(Ok<T>, Err);
Result<List<string>> result = new Ok<List<string>>(new List<string> { "Ali", "Veli" });
string output = result switch
{
Ok<List<string>> ok => string.Join(", ", ok.Value),
Err e => $"Hata: {e.Message}"
};
Console.WriteLine(output); // Ali, Veli
Bu yaklaşım exception fırlatmak yerine hataları tip sistemiyle temsil etmeni sağlıyor. Fonksiyonel programlamadaki Railway-Oriented Programming yaklaşımını C#'a özgü bir sözdizimle getiriyor.
Özetle
Union types, C#'a uzun süredir beklenen bir özelliği getiriyor. object ile yaşanan tip güvensizliğinden, abstract class hiyerarşilerinin kısıtlamalarından kurtulup ilgisiz tipleri tek ve kapalı bir set altında toplayabiliyorsun. Compiler da seni her switch ifadesinde tüm durumları ele almaya zorluyor. Bu, özellikle API response modelleme, error handling ve domain modelleri gibi senaryolarda kodun doğruluğunu derleme aşamasında garanti altına alıyor.
Benim için en değerli yanı şu: artık "acaba yeni bir durum ekledim de bir yerde switch güncellemeyi unuttum mu?" diye düşünmek zorunda kalmıyorum. Compiler beni buluyor.