C# Dünyasında Çığır Açan Yenilik: .NET 11 ile Birleşim Tipleri Geliyor!
C# geliştiricilerinin uzun zamandır beklediği bir özellik olan birleşim tipleri (union types), .NET 11 önizlemesiyle (veya daha doğru bir ifadeyle C# 15 ile) sonunda kullanıma sunuluyor. Bu makalede, bu desteğin ne anlama geldiğini, nasıl kullanılabileceğini, arka planda nasıl uygulandığını ve hatta kendi özel birleşim tiplerinizi nasıl oluşturabileceğinizi detaylı bir şekilde inceleyeceğiz.
Bu yazı, .NET 11 preview 4 sürümündeki özellikler kullanılarak yazılmıştır. .NET 11'in son sürümü çıkana kadar birçok şey değişebilir.
Birleşim Tipleri (Union Types) Nedir?
Birleşim tipleri, fonksiyonel programlama dünyasında sıkça kullanılan temel veri yapılarından biridir. F#, TypeScript, Rust gibi hemen hemen tüm fonksiyonel odaklı dillerde bulunurlar. Temel olarak, bir türün iki farklı şeyi temsil etmesine olanak tanırlar.
En basit birleşim tiplerinden bazıları 'Option<T>' ve 'Result<TSuccess, TError>' tipleridir. Bunların "standart" bir versiyonu olmasa da, özel uygulamalarını görmek oldukça yaygındır. 'Result<>' iki durumdan birinde olabileceği için açıklaması en kolay olanlardan biridir:
- Başarı (Success)—Bu durumda, 'Result<>' nesnesi, başarılı bir işlemin sonucunu temsil eden bir 'TSuccess' değeri içerir.
- Hata (Error)—Bu durumda, 'Result<>' nesnesi, başarısız bir işlemin hatasını temsil eden bir 'TError' değeri içerir.
Bir metodunuzdan 'Result<>' nesnesi döndürdüğünüzde, çağrıcı başarıyı varsaymak yerine her iki durumu da açıkça ele almak zorundadır.
C# 15'te 'union' Anahtar Kelimesi ile Birleşim Tipleri
Önceki bölümde 'Result<>' tipini birleşim tipi örneği olarak kullandık, ancak birleşim tipleri bundan çok daha çok yönlüdür. Birbirinden farklı olabilecek verilerle uğraşmak istediğiniz her durumda idealdirler.
Örneğin, farklı özelliklere sahip, İşletim Sistemlerini temsil eden üç farklı 'record' tipimiz olduğunu varsayalım:
public record Windows(string Version);public record Linux(string Distro, string Version);public record MacOS(string Name, int Version);Bu tiplerin ortak hiçbir değeri olmadığını unutmayın. C# 15'ten önce, bir 'Windows' VEYA 'Linux' VEYA 'MacOS' nesnesi olabilecek bir şeyi ele almanın ana seçenekleri şunlardı:
- Tüm tiplerin türediği bir temel sınıf oluşturmaya çalışmak. Bu işe yarayabilir, ancak bu tipleri bir kütüphaneden geliyorsa kontrol edemiyorsanız ne olur?
- Tipi bir 'object' örneğinde depolamak. Bu işe yarar, ancak bu durumda tiplerle çalışmanın tüm güvenliğini kaybedersiniz.
- Nesnenizin hangi tipi içerdiğini takip etmek için bir "etiket" değeri kullanmak, örn. bunu izlemek için bir 'enum' kullanmak.
C# 15'te, 'union' anahtar kelimesi ile bu senaryo için doğrudan destek alıyoruz:
// 👇 'union'ı tip olarak kullanınpublic union SupportedOS(Windows, Linux, MacOS);// 👆 Birleşimin parçası olan tipleri listeleyin'SupportedOS' tipinin bir örneğini birkaç şekilde oluşturabilirsiniz:
// Yeni oluşturup bir örnek geçebilirsinizSupportedOS os = new SupportedOS(new MacOS("Tahoe", 25));// Veya örtük dönüşüm kullanabilirsiniz (arka planda new() çağırır)SupportedOS os = new MacOS("Tahoe", 25);Oluşturulan 'union' tipi 'IUnion' arayüzünü uygular:
public interface IUnion{ object? Value { get; }}Böylece 'Value' özelliğini kullanarak her zaman "iç" durum değerini bir 'object?' olarak geri alabilirsiniz:
// Depolanan "iç" nesneye '.Value' kullanarak erişebilirsinizConsole.WriteLine(os.Value); // MacOS { Name = Tahoe, Version = 25 }Ancak, birleşim tipleriyle çalışmanın kanonik yolu bir 'switch' ifadesi kullanmaktır:
string GetDescription(SupportedOS os) => os switch{ Windows windows => $"Windows {windows.Version}", Linux linux => $"{linux.Distro} {linux.Version}", MacOS macOS => $"MacOS {macOS.Name} ({macOS.Version})",}; // Not: '_' atma gerekli değil'switch' ifadesi, iç durum tipini otomatik olarak çıkarır ve çok hoş bir şey de '_. =>' "atma" durumunu dahil etmenize gerek olmamasıdır: derleyici, izin verilen değerlerin her birini kontrol etmenizi sağlar, ancak yalnızca bu değerleri kontrol etmeniz gerekir. Ve birini unutursanız, bir uyarı alırsınız:
warning CS8509: The switch expression does not handle all possible values of its input type(it is not exhaustive). For example, the pattern 'MacOS' is not covered.Not: Durum tiplerinizden biri nullable ise, örn. 'MacOS?', o zaman 'switch' ifadelerinizde 'null'u da ele almanız gerekir.
Tam daire çizmek gerekirse, 'Result<>' tipini aşağıdaki gibi uygulayabiliriz (sadece bir örnek, seçebileceğimiz birçok farklı uygulama var!):
public union Result<T>(T, Exception);Veya başka bir klasiği, 'Option<T>' tipini göstermek için:
public record class None;public union Option<T>(None, T);Bunlar C# 15'teki 'union' tiplerinin temelleridir, şimdi onları bugün nasıl kullanabileceğinize bakacağız, sonra da arka planda nasıl uygulandıklarına geçeceğiz.
.NET 11'de 'union' Tiplerini Kullanma
'union' tiplerini kullanmak için iki şey yapmanız gerekir:
- .NET 11 preview 2+ SDK'sını yükleyin. İlk 'union' desteği preview 2'de eklendi, ancak preview 4+ yüklerseniz daha sorunsuz bir deneyim yaşarsınız.
- '.csproj' dosyalarınıza '<LangVersion>preview</LangVersion>' ekleyerek önizleme dili desteğini etkinleştirin.
<Project Sdk='Microsoft.NET.Sdk'> <PropertyGroup> <OutputType>Exe</OutputType> <!-- 👇 Bunu ekleyin --> <LangVersion>preview</LangVersion> <TargetFrameworks>net11.0;net8.0;net48</TargetFrameworks> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup></Project>.NET 11 SDK'sını kullanmanız gerekse de, yukarıdaki '.csproj' dosyasında olduğu gibi net11.0;net8.0;net48 gibi daha önceki çalışma zamanlarını hedefleyebilirsiniz. 'union' desteği bir derleyici özelliği olarak uygulandığı için daha önceki çalışma zamanlarında da mevcuttur (teknik olarak desteklenmese bile).
Ancak, daha önceki çalışma zamanlarını hedefliyorsanız (veya .NET 11 preview 2 veya preview 3 kullanıyorsanız), projenize bazı yardımcı tipleri de eklemeniz gerekecektir:
#if !NET11_0_OR_GREATERnamespace System.Runtime.CompilerServices;[AttributeUsage(Class | Struct, AllowMultiple = false, Inherited = false)]public sealed class UnionAttribute : Attribute;public interface IUnion{ object? Value { get; }}Bu yardımcı tipler .NET 11 preview 4'te eklendi, bu nedenle daha yeni bir SDK kullanıyorsanız otomatik olarak kullanılabilir olacaklar, ancak daha önceki çalışma zamanlarını hedefliyorsanız yine de bunları dahil etmeniz gerekecek.
Tahmin edebileceğiniz gibi, derleyici 'union' tiplerini oluşturduğunda bu özniteliği kullanır ve bu arayüzü uygular. Birleşim tiplerinin nasıl uygulandığını anlamak için bir sonraki bölümde oluşturulan kodun neye benzediğine bakacağız.
IDE desteği açısından, Visual Studio Preview veya VS Code'un C# DevKit Insiders'ı kullanıyorsanız, ilk desteğe sahip olmalısınız. JetBrains Rider için destek henüz bekleniyor.
'union' Tipleri Nasıl Uygulanır?
'union' tipleri için tam spesifikasyonu burada bulabilirsiniz, ancak standart oluşturulan kod gerçekten oldukça basittir:
using System.Runtime.CompilerServices;[Union]public struct SupportedOS : IUnion{ public object? Value { get; } // Her durum tipi için kurucular public SupportedOS(Windows value) => this.Value = (object) value; public SupportedOS(Linux value) => this.Value = (object) value; public SupportedOS(MacOS value) => this.Value = (object) value;}Gördüğünüz gibi, oluşturulan 'SupportedOS' tipi:
- '[Union]' özniteliği ile süslenmiş bir 'struct'tır.
- 'IUnion' arayüzünü uygulayan tek, salt okunur bir 'object? Value' özelliğine sahiptir.
- Desteklediği her durum tipi için bir kurucuya sahiptir.
Durum tiplerinden 'SupportedOS' tipine örtük bir dönüşüm olmadığını görmek beni biraz şaşırttı, çünkü şöyle bir kod yazabiliyoruz:
SupportedOS os = new MacOS("Tahoe", 25);Ancak derleyici bunu basitçe '[Union]' kurucusunu kullanacak şekilde yeniden yazıyor gibi görünüyor:
// SupportedOS os = new MacOS("Tahoe", 25);// Derleyici şu şekilde kod yayar:SupportedOS os = new SupportedOS(new MacOS("Tahoe", 25));Bu örtük dönüşüm tamamen '[Union]' özniteliği tarafından yönlendirilir. Bunu, örneğimizi 'union' anahtar kelimesini kullanmayacak şekilde, ancak önceki örnekte gösterilen uygulama kodunu '[Union]' özniteliğini dahil etmeyi "unutarak" yeniden yazarsak görebilirsiniz:
using System.Runtime.CompilerServices;SupportedOS os = new MacOS("Tahoe", 25); // 'MacOS' tipi 'SupportedOS' tipine örtük olarak dönüştürülemez.var description = os switch{ Windows windows => $"Windows {windows.Version}", // 'SupportedOS' tipinde bir ifade 'Windows' tipinde bir desenle işlenemez. Linux linux => $"{linux.Distro} {linux.Version}", // 'SupportedOS' tipinde bir ifade 'Linux' tipinde bir desenle işlenemez. MacOS macOS => $"MacOS {macOS.Name} ({macOS.Version})", // 'SupportedOS' tipinde bir ifade 'MacOS' tipinde bir desenle işlenemez.};// Aşağıdaki öznitelik geçerli bir Union tipi olması için gereklidir,// sadece gösterim amacıyla burada kaldırılmıştır// [Union] public struct SupportedOS : IUnion{ public object? Value { get; } public SupportedOS(Windows value) => this.Value = (object) value; public SupportedOS(Linux value) => this.Value = (object) value; public SupportedOS(MacOS value) => this.Value = (object) value;}Yukarıdaki kod, '[Union]' özniteliğinin örtük dönüşümleri ve 'switch' ifadelerini nasıl yönlendirdiğini gösteren aşağıdaki hatalarla derlenemez:
error CS0029: Cannot implicitly convert type 'MacOS' to 'SupportedOS'error CS8121: An expression of type 'SupportedOS' cannot be handled by a pattern of type 'Windows'.error CS8121: An expression of type 'SupportedOS' cannot be handled by a pattern of type 'Linux'.error CS8121: An expression of type 'SupportedOS' cannot be handled by a pattern of type 'MacOS'.'[Union]' özniteliğini yeniden eklerseniz, her şey düzgün bir şekilde derlenir ve çalışır, bu da kendi özel birleşim tiplerinizi nasıl oluşturabileceğinizi gösterir.
Özel Birleşim Uygulamalarıyla Boxing'i Önleme
Birleşim tipleri için henüz destek alıyor olsak da, neden özel 'Union' tipleri oluşturmak isteyebiliriz? Bir neden, 'OneOf' veya 'Sasa' gibi kütüphaneler tarafından sağlanan özel birleşim tiplerini zaten kullanıyor olmanız olabilir. Bu durumlarda, kütüphaneler basitçe 'IUnion' arayüzünü uygulayarak ve '[Union]' özniteliğini ekleyerek yerleşik dil desteğinden (örn. 'switch' ifadesi desteği) yararlanabilirler.
Başka bir durum ise "durum tipini bir 'object' örneğinde saklama" yaklaşımının sizin için yeterince iyi olmamasıdır. Oluşturulan birleşim tipi her zaman tek bir 'object' alanına sahip bir 'struct'tır. Bu, birden çok 'struct' tipinin birleşimini oluşturuyorsanız, bu tiplerin yığın üzerinde box'lanacağı anlamına gelir.
Örneğin, bir 'int' veya bir 'bool'u temsil edebilen şu birleşime ihtiyacınız olduğunu düşünün:
public union IntOrBool(int, bool);Sorun şu ki, 'IntOrBool'un kurucusuna geçirilen 'int' veya 'bool' hemen bir 'object'e box'lanır ve 'Value' özelliğinde saklanır:
[Union]public struct IntOrBool : IUnion{ public object? Value { get; } // Struct argümanları her zaman box'lanır, yığın üzerinde bellek tahsis edilir public IntOrBool(int value) => this.Value = (object) value; public IntOrBool(bool value) => this.Value = (object) value;}Bu, yığın üzerinde bellek tahsis eder, ki bu genellikle istenmeyen bir durumdur, çünkü birleşim tiplerinin büyük ölçüde performans açısından şeffaf olması amaçlanır. Bu uygulamayı kullanan herhangi bir 'switch' ifadesi de benzer şekilde 'Value' özelliğini kullanacaktır. Örneğin, yerleşik temel 'union' uygulamasıyla, aşağıdaki ifade:
IntOrBool intOrBool;var description = intOrBool switch{ int i => "integer", bool b => "bool",};Şuna benzer koda dönüşür:
IntOrBool unmatchedValue = new IntOrBool(23);object obj = unmatchedValue.Value; // 👈 Box'lanmış değere erişirstring str;if (obj is int _){ str = "integer";}else if (obj is bool _){ str = "bool";}else{ ThrowSwitchExpressionException((object) unmatchedValue); // Olamaz, ama yine de ele alınır}Çoğu durumda, boxing bellek tahsisi pek önemli olmayacaktır, ancak "kritik yollar" gibi diğer yerlerde boxing istenmez. Bunu hesaba katmak için, 'union' özelliği bir "non-boxing" uygulamasına, yani bir 'TryGetValue' desenine izin verir. Bu, aşağıdakileri uygulamanızı gerektirir:
- 'bool HasValue { get; }' depolanan değerin 'null' olmadığı durumlarda 'true' döndürür.
- Her durum tipi 'T' için 'bool TryGetValue(out T value)'
Örneğin, yukarıdaki 'IntOrBool' tipinin boxing'i önleyen potansiyel bir uygulaması şöyledir:
[Union]public struct IntOrBool : IUnion{ private readonly bool _isBool; private readonly int _value; public IntOrBool(int value) { _isBool = false; _value = value; } public IntOrBool(bool value) { _isBool = true; _value = value ? 1 : 0; } public bool HasValue => true; // değerler asla null değildir public bool TryGetValue(out int value) // boxing olmadan int değeri alır { value = _value; return !_isBool; } public bool TryGetValue(out bool value) // boxing olmadan bool değeri alır { value = _isBool && _value is 1; return _isBool; } // 👇 IUnion'ı karşılamak için bunu uygulamak zorundayız, // ve hala box'lama yapar, ancak varsayılan olarak kullanılmaz. public object Value => _isBool ? _value is 1 : _value;}'TryGetValue()' metotlarını uyguladığınızda, derleyici bunları 'Value' özelliği yerine 'switch' ifadelerinde otomatik olarak kullanır, böylece yukarıdaki 'switch' ifadesi şuna dönüşür:
IntOrBool unmatchedValue = new IntOrBool(23);string str;// 👇 Boxing yapan Value özelliği yerine TryGetValue'ı çağırırif (unmatchedValue.TryGetValue(out int _)) { str = "integer";}else if (unmatchedValue.TryGetValue(out bool _)){ str = "bool";}else{ ThrowSwitchExpressionException((object) unmatchedValue); // Olamaz, ama yine de ele alınır}Kod yollarınıza ve kullanım durumlarınıza bağlı olarak, bu tür özel non-boxing uygulamaları oluşturmaya değip değmeyeceği, 'union' tiplerini kod tabanınızda ne için kullandığınıza bağlıdır.
Gelecekteki Özellikler ve Yol Haritası
'union' uygulaması mevcut haliyle kullanılabilir olsa da, dil teklifinde ele aldığımın ötesinde daha fazlası var. İşte henüz gelmemiş bazı ilgili özellikler:
- Birleşim Üyesi Sağlayıcıları (Union member providers). Bunlar, birleşim tipinin üyelerini birleşimin kendisinden farklı bir tipte tanımlamanın bir yolunu sunar.
- Kapalı Enümasyonlar (Closed enums). Bunlar, 'enum' için 'switch' ifadesinde bir "tümünü yakala" ifadesi ('_ =>') dahil etmenize gerek olmayan 'enum'lardır.
- Kapalı Hiyerarşiler (Closed hierarchies). Bu, türetilmiş sınıfların tanımlayan derleme dışında bildirilmesini önlemek için bir 'class' üzerinde 'closed' değiştiricisinin eklenmesine olanak tanır, bu da benzer şekilde bir "tümünü yakala" ifadesi olmadan kapsamlı 'switch' ifadelerine izin verir.
Bu özellikler .NET 11'e dahil olabilir veya olmayabilir, ancak eğer olurlarsa kesinlikle onları ele alacağım!
Özet
Bu makalede, .NET 11 preview 2'de sunulan birleşim tipleri desteğini ele aldım. Onları uygulamak için adımları ve 'switch' ifadelerini kullanarak birleşim tiplerini nasıl ayıracağınızı açıkladım. 'union' bildirim sözdizimini, arka planda nasıl uygulandıklarını ve bir birleşim tipinin non-boxing versiyonunu nasıl uygulayacağınızı gösterdim. Son olarak, birleşim tipleri ve henüz yayınlanmamış C# dilindeki kapsamlılık iyileştirmeleri için bazı planları ve yol haritasını tartıştım.
Andrew Lock | .Net Escapades

