نوعهای اتحادیه (Union Types) در C# 15 و NET 11.
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۳/۱۰ ۰۹:۳۵
آدرس: www.dntips.ir
Result و Option را بسیار تمیزتر و ایمنتر میکند و گزینهای قدرتمند برای مدیریت دادههایی است که ممکن است ساختارهای مختلفی داشته باشند. در این مقاله به بررسی syntax، نحوه استفاده، نحوه پیادهسازی و نکات پیشرفته این ویژگی میپردازیم.Result است:TSuccessTErrorResult برمیگرداند و فراخواننده مجبور است هر دو حالت را به صورت صریح مدیریت کند. این الگو (Result Pattern) خطاهای زمان اجرا را کاهش میدهد و کد را قابل پیشبینیتر میکند.public union SupportedOS(Windows, Linux, MacOS);
public record Windows(string Version); public record Linux(string Distro, string Version); public record MacOS(string Name, int Version);
SupportedOS os1 = new SupportedOS(new MacOS("Tahoe", 25));
// تبدیل ضمنی (Implicit Conversion)
SupportedOS os2 = new MacOS("Tahoe", 25);switch است:string GetDescription(SupportedOS os) => os switch
{
Windows windows => $"Windows {windows.Version}",
Linux linux => $"{linux.Distro} {linux.Version}",
MacOS macOS => $"MacOS {macOS.Name} ({macOS.Version})"
// نیازی به _ => نیست! کامپایلر بررسی میکند همه حالات پوشش داده شده باشند
};MacOS?)، باید حالت null را هم مدیریت کنید.// Result public union Result<T>(T, Exception); // Option public record class None; public union Option<T>(None, T);
.csproj گزینه زیر را اضافه کنید:<LangVersion>preview</LangVersion>
IUnion را پیادهسازی میکنند:[Union]
public struct SupportedOS : IUnion
{
public object? Value { get; }
public SupportedOS(Windows value) => this.Value = value;
public SupportedOS(Linux value) => this.Value = value;
public SupportedOS(MacOS value) => this.Value = value;
}object تبدیل (box) میکند که برای انواع value type (مانند int، bool، structهای کوچک) میتواند overhead ایجاد کند. برای سناریوهای عملکردی حساس، میتوانید Union سفارشی بدون boxing بسازید:[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;
public bool TryGetValue(out int value)
{
value = _value;
return !_isBool;
}
public bool TryGetValue(out bool value)
{
value = _isBool && _value == 1;
return _isBool;
}
public object? Value => _isBool ? (_value == 1) : _value;
}TryGetValue را پیادهسازی کنید، کامپایلر به طور خودکار از آنها در عبارتهای switch استفاده میکند و از boxing جلوگیری میشود.OneOf. با معرفی C# 15 و .NET 11، ویژگی منحصربهفرد Union Types به صورت آزمایشی به زبان اضافه شده است تا این شکاف ساختاری را برای همیشه برطرف کند. در این مقاله، به بررسی عمیق چالشهای موجود و نحوه بازنویسی الگوی Result با این قابلیت جدید میپردازیم.// کنترل جریان برنامه از طریق پرتاب استثنا (رویکرد سنتی)
public User GetUser(int id) =>
_users.TryGetValue(id, out var user)
? user
: throw new UserNotFoundException(id);User است، اما در واقعیت این یک ادعای ناقص است. متد فوق در صورت عدم یافتن کاربر، استثنا پرتاب میکند. این رویکرد دو ایراد ساختاری عمده دارد:| روش جایگزین | مزیت | محدودیت اصلی |
الگوی Try-Parse (خروجی bool و پارامتر out) | عدم پرتاب استثنا | عدم پشتیبانی از برنامهنویسی ناهمگام (Async) و پیچیدگی ترکیب کدهای چندگانه |
کلاس سفارشی Result با فیلدهای Nullable | امضای صریح متد | افزایش کدهای کلیشهای (Ceremony) و عدم اجبار کامپایلر به بررسی خطا |
کتابخانه OneOf | شبیهسازی مناسب نوعهای اتحادی | وابستگی به کتابخانه خارجی و عدم پشتیبانی بومی در سطح زبان |
public union Result<T>(T, Exception);
Result دقیقاً و صرفاً میتواند حاوی مقداری از نوع T یا نوع Exception باشد. در ادامه، یک نمونه ملموستر از ترکیب انواع سوابق (Record Types) مستقل برای مدلسازی روشهای پرداخت آورده شده است:public record CreditCard(string Last4, string Brand); public record PayPal(string Email); public record BankTransfer(string Iban); public union PaymentMethod(CreditCard, PayPal, BankTransfer);
PaymentMethod method = new CreditCard("4242", "Visa");switch نمایان میشود:string Describe(PaymentMethod method) => method switch
{
CreditCard card => $"{card.Brand} ending {card.Last4}",
PayPal paypal => $"PayPal ({paypal.Email})",
BankTransfer ach => $"Bank transfer to {ach.Iban}",
}; // بدون نیاز به بخش default یا دیسکارد (_)default) یا علامت دیسکارد (_) نیست. از آنجا که کامپایلر از بسته بودن این مجموعه آگاه است، جامعیت یا Exhaustiveness Check را ارزیابی میکند. اگر توسعهدهندهای یکی از حالتها را فراموش کند، کامپایلر هشدار زیر را صادر خواهد کرد:warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'BankTransfer' is not covered.
GetUser را بازنویسی میکنیم. ابتدا نوعهای خروجی ممکن را به صورت شفاف تعریف میکنیم:public record NotFound(int Id); public union UserResult(User, NotFound);
public UserResult GetUser(int id) =>
_users.TryGetValue(id, out var user)
? user
: new NotFound(id);IResult response = GetUser(42) switch
{
User user => Results.Ok(user),
NotFound found => Results.NotFound($"No user with id {found.Id}"),
};struct تبدیل میکند. این ساختار حاوی یک فیلد متمایزکننده (Discriminator/Tag) برای تشخیص نوع فعلی و یک ارجاع تخصیصیافته به داده است.object? ذخیره شوند که این امر برای انواع ساختاری منجر به بروز پدیده Boxing میشود. با این حال، تیم توسعه داتنت در حال توسعه یک مسیر بهینهسازی بدون نیاز به Boxing (Non-boxing path) با استفاده از الگوهای پیشرفتهتر چیدمان حافظه (LayoutKind.Explicit) برای کدهای با کارایی بالا (Performance-sensitive) است.Result با این ابزار نوظهور، ما را از معماریهای پیچیده قدیمی و پنهانکاری متدها نجات میدهد. به کمک این قابلیت، امضای متدها بازتابدهنده واقعیت خروجی برنامه خواهد بود و سیستم کنترل خطای زمان کامپایل، ریسک بروز خطاهای پیشبینینشده در زمان اجرا (Runtime) را به حداقل میرساند. این گامی بزرگ به سمت کدهای خواناتر، پایدارتر و با نگهداری آسانتر در اکوسیستم داتنت است.Variant آشنا هستند. این نوع داده انعطافپذیر اجازه میداد یک تابع گاهی یک رشته، گاهی یک عدد، گاهی یک آرایه، و گاهی هم مقدار Nothing یا خطا برگرداند. این الگو، ساده، سریع و بسیار عملی بود، اما هنگام مدرنسازی این کدبیسها به سمت #C، همیشه یکی از بزرگترین چالشها بودهاست.Variant استفاده میکردند:Public Function LookupCustomer(ByVal customerID As Long) As Variant
Dim rs As ADODB.Recordset
On Error GoTo ErrorHandler
Set rs = New ADODB.Recordset
rs.Open "SELECT * FROM Customers WHERE ID = " & customerID, _
gConnection, adOpenStatic, adLockReadOnly
If rs.EOF Then
LookupCustomer = Empty
Else
LookupCustomer = Array(rs!CustomerName, rs!CreditLimit, rs!Status)
End If
rs.Close
Exit Function
ErrorHandler:
LookupCustomer = "ERROR: " & Err.Description
End FunctionIsEmpty، IsArray یا VarType بررسی میکرد که چه چیزی دریافت کرده است. این رویکرد هرچند غیررسمی بود، اما دقیقاً منظور برنامهنویس را بیان میکرد: تابع یکی از سه حالت ممکن را برمیگرداند.public union CustomerLookupResult
{
Customer(string Name, decimal CreditLimit, CustomerStatus Status);
NotFound;
Error(string Message);
}
public CustomerLookupResult LookupCustomer(long customerId)
{
try
{
using var connection = new SqlConnection(_connectionString);
connection.Open();
using var command = new SqlCommand(
"SELECT Name, CreditLimit, Status FROM Customers WHERE Id = @id",
connection);
command.Parameters.AddWithValue("@id", customerId);
using var reader = command.ExecuteReader();
if (!reader.Read())
return new CustomerLookupResult.NotFound();
return new CustomerLookupResult.Customer(
reader.GetString(0),
reader.GetDecimal(1),
Enum.Parse<CustomerStatus>(reader.GetString(2)));
}
catch (SqlException ex)
{
return new CustomerLookupResult.Error(ex.Message);
}
}var result = LookupCustomer(12345);
var message = result switch
{
CustomerLookupResult.Customer c =>
$"Found {c.Name}, credit limit {c.CreditLimit:C}",
CustomerLookupResult.NotFound => "No customer with that ID",
CustomerLookupResult.Error e => $"Lookup failed: {e.Message}"
};Suspended) به اتحادیه اضافه کند، کامپایلر همه switchهای موجود را علامتگذاری میکند تا مطمئن شود همه حالتها مدیریت شدهاند. این چیزی است که نوع Variant در VB6 هرگز نداشت.Any) یا Clarion هستند، اتحادیهها تحول بزرگی ایجاد کردهاند. دیگر نیازی به استفاده از کتابخانههای شخص ثالث مانند OneOf یا ساختن boilerplate زیاد نیست. کد تولیدی هم وفادار به معنای اصلی است و هم ایدیوماتیک برای #C مدرن. این ویژگی بخشی از روند گستردهتری است که در سالهای اخیر در .NET رخ داده: Records، Pattern Matching پیشرفته، Required Members و اکنون Union Types. هر کدام از این امکانات، هدف مهاجرت را جذابتر و هزینه نگهداری روی پلتفرم قدیمی را بالاتر میبرند.public union CustomerLookupResultunion نوعی از نوع داده است که میگوید: «مقدار این متغیر دقیقاً یکی از این حالتها (cases) را دارد، نه بیشتر و نه کمتر.»class معمولی یا record، اینجا کامپایلر میداند که فقط یکی از حالتها ممکن است وجود داشته باشد.public union CustomerLookupResult
{
Customer(string Name, decimal CreditLimit, CustomerStatus Status);
NotFound;
Error(string Message);
}public union CustomerLookupResultrecord یا class عمل میکند اما رفتار متفاوتی دارد.Customer(...)record موقعیتی (positional record) است.Name، CreditLimit و Status را بدهید.NotFound;NotFound یک حالت خالی و ثابت.Error(string Message);CustomerLookupResult ایجاد میکنند.// حالت موفق
return new CustomerLookupResult.Customer(علی رضایی", 15000000", CustomerStatus.Active);
// حالت پیدا نشد
return new CustomerLookupResult.NotFound();
// حالت خطا
return new CustomerLookupResult.Error("اتصال به دیتابیس برقرار نشد");switch expression است:var message = result switch
{
CustomerLookupResult.Customer c =>
$"مشتری یافت شد: {c.Name} - اعتبار: {c.CreditLimit:C}",
CustomerLookupResult.NotFound =>
"مشتری با این شناسه وجود ندارد",
CustomerLookupResult.Error e =>
$"خطا در جستجو: {e.Message}",
// اگر حالتی اضافه شود و اینجا پوشش داده نشود، کامپایلر هشدار میدهد
};Suspended اضافه کنید، تمام switchهای قبلی خطای کامپایل یا warning میگیرند.Variant در VB6 یا object در C# قدیمی، اینجا اشتباه نوع (مثل فرض کردن string بودن وقتی NotFound است) در زمان کامپایل گرفته میشود.abstract class + recordهای مشتقشده + enum discriminator میساختید که خیلی طولانیتر بود.when، property patterns و غیره هم استفاده کنید.| ویژگی | VB6 Variant | C# Union (.NET 11) |
| انعطافپذیری | بسیار بالا | محدود به موارد تعریفشده |
| ایمنی نوع | بسیار کم | بسیار بالا (کامپایلر-محور) |
| بررسی کامل حالتها | دستی (IsEmpty, VarType) | خودکار توسط کامپایلر |
| خوانایی کد | متوسط | بسیار بالا |
| نگهداری در بلندمدت | سخت | آسان |
public union در .NET 11 پلی بسیار قوی بین تجربهٔ عملی زبانهای قدیمی Basic و قدرت مدرن #C ایجاد کرده است.System.Text.Json برای مدیریت دادههای JSON استفاده میکند، Union Typeها بدون نیاز به هیچگونه پیکربندی خاص یا کدهای کلیشهای اضافی (Zero-Configuration)، در تمامی بخشهای ASP.NET Core به عنوان بدنه درخواست (Request Body) یا نوع خروجی (Return Type) پشتیبانی میشوند:RequestDelegateFactory) و هم در لایهی سورسجنراتورها (Source-Generated Request Delegates) پیادهسازی شده است. همچنین ساختارهایی مثل Task، IAsyncEnumerable و Results به طور کامل پوشش داده شدهاند.anyOf به طور خودکار توصیف میشود:// فعالسازی نسخه پیشنمایش در فایل پروژه الزامی است: preview
public record class Dog(string Name);
public record class Cat(int Lives);
public union Pet(Dog, Cat);
// خروجی بر اساس منطق برنامه میتواند سگ یا گربه باشد
app.MapGet("/pets/{id}", Pet (int id) => id == 0 ? new Dog("Rex") : new Cat(9));UnionAttribute و IUnion) را به صورت دستی بازنویسی کنند. اما از نسخه Preview 6 به بعد، این انواع به صورت داخلی در فضای نام System.Runtime.CompilerServices قرار گرفتهاند و پروژه بدون هیچ کد اضافهای کامپایل میشود.System.Text.Json اکنون قرارداد جدیدی به نام JsonTypeInfoKind.Union را شناسایی میکند. این مکانیزم هم در حالت سریالایزر مبتنی بر رفلکشن (Reflection-based) و هم در سورسجنراتورها (Source Generators) کار میکند.Pet در حال حاضر حاوی نمونه Dog("Rex") باشد، خروجی JSON دقیقاً به صورت زیر خواهد بود:{"Name":"Rex"}int و string است، به شکل کاملاً تمیز و بدون آبجکتهای پیچیده تودرتو منتقل میشود:"hello"42JsonUnionAttribute، JsonUnionCaseInfo و کلاسبندهای نوع (JsonTypeClassifier و JsonSerializerOptions.TypeClassifiers) استفاده کنند.private یا internal) که تنها یک پارامتر میگیرند استفاده کنند.not در انطباق الگو (Pattern Matching) اکنون به خودِ مقدار ورودی اتحادیه اعمال میشود، نه به ارزش درونی آن.Create پشتیبانی میکنند.System.Text.Json و ASP.NET Core تبدیل به ابزاری آماده برای تولید محصول (Production-ready) شده است. این ویژگی به ما اجازه میدهد APIهایی با خروجیهای چندگانه و کاملاً مستند (از طریق OpenAPI بومی) بدون تحمیل بار پردازشی سنگین یا کدهای پیچیده، معماری کنیم.System.Text.Json نمایان شده است. برخلاف تصور اولیه، سریالسازی و بازخوانی (Deserialization) دادههای چندشکلی به سادگی و بدون دردسر صورت نمیگیرد.closed معرفی شده است:abstract) رفتار میکند، اما اجازه ارثبری از آن تنها در درون همان اسمبلی (Assembly) ممکن است. تلاش برای ارثبری از کلاسی با نشانگر closed خارج از اسمبلی منجر به خطای کامپایل (CS9382) خواهد شد.switch به کامپایلر میدهند، اما نحوه برخورد سریالایزر با آنها کاملاً متفاوت است.closed سریالایز کنید، تنها فیلدهای کلاس پایه به خروجی منتقل شده و ویژگیهای فرزند از دست میروند.[JsonDerivedType]، گزینهای نوین در JsonSerializerOptions به نام InferClosedTypePolymorphism معرفی شده است تا سلسلهمراتب کلاسهای بسته را در زمان کامپایل/اجرا به صورت خودکار شناسایی کند. با این حال، در پیشنمایشهای اولیه محدودیتهای سختگیرانهای وجود دارد:internal باشد اگر کلاس والد public است).closed یا abstract باشد)، استنتاج خودکار با خطا مواجه میشود و توسعهدهنده کماکان ناچار است از اتریبیوتهای صریح [JsonDerivedType] به همراه تفکیککننده $type استفاده کند.System.Text.Json برای یک اتحادیه بر مبنای «انتقال تمیز» (Clean Payload) است؛ به این معنا که شیء را بدون هیچ کلید اضافی یا تفکیککنندهای نظیر $type مستقیماً به مقدار نوع فعال سریالایز میکند. به عنوان مثال:IntOrString(42) دقیقاً به صورت عدد 42 نوشته میشود، نه { "Value": 42 }.Windows("11") به صورت شیء {"Version": "11"} ذخیره میگردد.// این اتحادیهها به درستی Deserialized میشوند (عدم تداخل توکن) public union IntOrString(int, string); // Number در برابر String public union DoubleOrGuid(double, Guid); // Number در برابر String public union BoolOrDict(bool, Dictionary); // Boolean در برابر Object
System.Text.Json.JsonException متوقف خواهد شد، زیرا سریالایزر متوجه نمیشود که شیء ورودی متعلق به کدام حالت است:// این اتحادیهها به صورت پیشفرض در مرحله Deserialization شکست میخورند! public union Pet(Cat, Dog); // هر دو به صورت Object ذخیره میشوند public union SupportedOS(Windows, Linux, MacOS); // هر سه به صورت Object ذخیره میشوند public union Num(int, long); // هر دو توکن Number تولید میکنند public union When(DateTime, DateTimeOffset); // هر دو توکن String تولید میکنند
$type، توسعهدهنده ناچار است از «تطبیق ساختاری» (Structural Matching) استفاده کند؛ به این معنی که با خواندن توکنهای اولیه JSON و بررسی فیلدهای اختصاصی هر نوع، تشخیص دهد که استراکچر مورد نظر به کدام حالت تعلق دارد:public class SupportedOSClassifier : JsonTypeClassifierFactory<SupportedOS>
{
public override JsonTypeClassifier CreateJsonClassifier(
JsonTypeClassifierContext context, JsonSerializerOptions options)
{
return static (ref Utf8JsonReader reader) =>
{
if (reader.TokenType is not JsonTokenType.StartObject)
return null;
bool sawVersion = false;
while (reader.Read() && reader.TokenType is not JsonTokenType.EndObject)
{
// این فیلدها در هر کلاس منحصربهفرد هستند
if (reader.ValueTextEquals("Distro"u8)) return typeof(Linux);
if (reader.ValueTextEquals("Name"u8)) return typeof(MacOS);
if (reader.ValueTextEquals("Version"u8))
sawVersion = true;
reader.Read(); // رفتن به سمت مقدار
reader.Skip(); // رد کردن مقدار فیلد
}
return sawVersion ? typeof(Windows) : null;
};
}
}
// انتساب کلاسبند به اتحادیه
[JsonUnion(TypeClassifier = typeof(SupportedOSClassifier))]
public union SupportedOS(Windows, Linux, MacOS);JsonUnionTypeStructuralClassifier تعبیه شده است. این ابزار به شکل سراسری و خودکار فیلدهای اختصاصی اشیاء را برای تفکیک بررسی میکند.required یا [JsonRequired] تعریف نشده باشند، طبقهبندی ساختاری همچنان در ابهام باقی میماند:// جهت تضمین عملکرد طبقهبند ساختاری خودکار: public record Windows([property: JsonRequired] string Version); public record Linux([property: JsonRequired] string Distro, string Version); public record MacOS([property: JsonRequired] string Name, int Version);
JsonUnionTypeStructuralClassifier یا سفارشی) بهره بگیرید.چکیده: با معرفی اولیه و آزمایشی انواع اجتماع (Discriminated Unions / Union Types) در نسخه پیشنمایش #C 15 و پلتفرم NET 11.، زبان سیشارپ گام بلندی بهسوی سیستمهای نوعبندی جبری (Algebraic Data Types) و همگامی با الگوهای برنامهنویسی تابعی مدرن برداشته است. بااینحال، در نگارشهای نخستین این ویژگی، قابلیت داخلی زبان یا API مستقیمی برای استخراج فهرستی از کلیه حالتهای معتبر یک نوع اجتماع (Case Types) در زمان اجرا یا کامپایل بدون بررسی دستی پیشبینی نشده است. این مقاله با تکیه بر ساختار کامپایلر Roslyn و پیادهسازی زیربنایی رابط IUnion، متدهای توسعهیافتهای (Extension Methods) مبتنی بر Reflection ارائه میدهد که امکان کشف پویا و استاتیک حالتهای تشکیلدهنده Union را فراهم میسازد. همچنین ضمن تحلیل چالشهای عملکردی Reflection سنتی، راهکارهای بهینهسازی از جمله کشگذاری حافظه و استفاده از مولدهای سورس (Source Generators) بهعنوان راهکار جایگزین مورد ارزیابی قرار میگیرند.Result) یا دامنههای مقید چندحالته، مستلزم وراثت کلاسهای مجرد، کتابخانههای جانبی (مانند OneOf) یا ساختارهای دستی بود که اغلب فاقد بررسی جامعیت الگوی تطبیق (Exhaustive Pattern Matching) در سطح کامپایلر بودند.union را به عنوان بخشی بومی از سیستم نوعبندی معرفی کرده است. با وجود این تکامل، در حوزههایی مانند اعتبارسنجی خودکار، سریالسازی/دیسریالسازی پویا (Serialization)، ساخت اسناد OpenAPI و ابزارهای نگاشت شیء، نیاز به استخراج برنامهنویسیشدهی حالتهای یک Union کماکان وجود دارد. از آنجا که API مستقیمی در زبان برای برشمردن این حالات وجود ندارد، شناخت زیرساخت کامپایلر و استفاده از متدهای کمکی Reflection نقشی کلیدی ایفا میکند.csproj.) بر روی نسخه پیشنمایش الزامی است:<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>IUnion را پیادهسازی مینماید. علاوه بر این، به ازای هر یک از انواع حالتهای مجاز (Case Types)، یک سازنده (Constructor) عمومی تکعاملی تولید میشود که نمونه موردنظر را در قالب نوع Union بستهبندی میکند.public record Dog(string Name);
public record Cat(int NumberOfLives, string? Name = default);
public record Parrot(bool WantsCrackers, string? Name = default);
public record GoldFish(bool MakesBubbles, string? Name = default);
// تعریف Union در C# 15
public union Pet(Dog, Cat, Parrot, GoldFish)
{
public string Description => this switch
{
Dog dog => $"Bark Bark! {dog.Name} says! 🦴 🐕",
Cat cat => $"{cat.Name} says: Meow! I got {cat.NumberOfLives} lives left 🐈",
Parrot parrot => $"🦜 Squawk! {parrot.Name} says: {(parrot.WantsCrackers ? "I want crackers!" : "Give us a kiss!")}",
GoldFish goldFish => $"🐠 {goldFish.Name} says: Blub Blub! {(goldFish.MakesBubbles ? "I make bubbles!" : "I don't make bubbles!")}"
};
}Pet سازندههای صریح یا ضمنی تولید میکند:Pet(Dog caseValue)Pet(Cat caseValue)Pet(Parrot caseValue)Pet(GoldFish caseValue)IUnion، میتوان متدهای کمکی انعطافپذیری توسعه داد که حالتهای مجاز را استخراج کنند:using System.Reflection;
namespace System.Runtime.CompilerServices;
/// <summary>
/// ابزارهای کمکی مبتنی بر Reflection برای کاوش در ساختار انواع Union در C# 15
/// </summary>
public static class UnionExtensions
{
/// <summary>
/// استخراج انواع حالتهای معتبر در زمان کامپایل از طریق پارامتر جنریک
/// </summary>
public static IReadOnlyList<Type> GetCaseTypes<TUnion>() where TUnion : IUnion
{
return GetUnionCaseTypes(typeof(TUnion));
}
/// <summary>
/// استخراج پویای انواع حالتهای معتبر در زمان اجرا بر اساس شیء Type
/// </summary>
public static IReadOnlyList<Type> GetUnionCaseTypes(this Type? type)
{
// بررسی ایمن نوع: در صورت نامعتبر بودن یا عدم پیادهسازی IUnion، آرایه تهی بازگردانده میشود
if (type is null || !typeof(IUnion).IsAssignableFrom(type))
{
return Array.Empty<Type>();
}
return type
.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
.Where(ctor => ctor.GetParameters().Length == 1)
.Select(ctor => ctor.GetParameters()[0].ParameterType)
.Distinct()
.ToArray();
}
}public class Program
{
public static void Main()
{
// ۱. استخراج استاتیک از طریق متد جنریک
var caseNames = UnionExtensions.GetCaseTypes<Pet>().Select(t => t.Name);
Console.WriteLine($"Pet union cases: {string.Join(", ", caseNames)}");
// ۲. تست الگوهای تطبیق و رفتار مشترک
Pet[] pets = [
new Dog("Rex"),
new Cat(7, "Whiskers"),
new Parrot(true, "Polly"),
new GoldFish(true, "Timmy")
];
foreach (var pet in pets)
{
Console.WriteLine(pet.Description);
}
Console.WriteLine("\nListing all pet union case types with properties:");
Console.WriteLine(new string('-', 50));
// ۳. استخراج پویا و بررسی فرادادههای هر حالت
foreach (var caseType in typeof(Pet).GetUnionCaseTypes())
{
var propNames = caseType
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Select(p => p.Name);
Console.WriteLine($"* {caseType.Name,-10} => Props: {string.Join(", ", propNames)}");
}
}
}Pet union cases: Dog, Cat, Parrot, GoldFish Bark Bark! Rex says! 🦴 🐕 Whiskers says: Meow! I got 7 lives left 🐈 🦜 Squawk! Polly says: I want crackers! 🐠 Timmy says: Blub Blub! I make bubbles! Listing all pet union case types with properties: -------------------------------------------------- * Dog => Props: Name * Cat => Props: NumberOfLives, Name * Parrot => Props: WantsCrackers, Name * GoldFish => Props: MakesBubbles, Name
GetConstructors و کاوش پارامترها در مسیرهای پرتردد (Hot Paths) سربار عملکردی ایجاد میکند. با استفاده از ساختار ConcurrentDictionary یا کلاسهای استاتیک جنریک، میتوان نتایج را در اولین فراخوانی کش کرد:private static readonly ConcurrentDictionary<Type, IReadOnlyList<Type>> _caseCache = new();
public static IReadOnlyList<Type> GetUnionCaseTypesCached(this Type type)
{
return _caseCache.GetOrAdd(type, t => t.GetUnionCaseTypes());
}Pet.AllCases یا Pet.CaseTypes تولید کنند. این کار علاوه بر حفظ عملکرد در مقیاس بالا (AOT / Native AOT-friendly)، از خطاهای زمان اجرا جلوگیری میکند.چکیده: یکی از چالشهای مکرر در طراحی رابطهای برنامهنویسی (API Design) در زبان #C، نیاز به پشتیبانی همزمان از یک شیء منفرد و مجموعهای از اشیاء است. رویکرد مرسوم برای حل این مسئله، ارائه چندریختی یا اورلودهای متعدد متدها (Overloads Explosion) است که منجر به پیچیدگی و افزونگی کد میشود. با مطرح شدن و پیشرفت طرح انواع اجتماعی تفکیکشده (Discriminated Unions) در سیشارپ مدرن، قابلیت تعریف بدنه برای انواع اجتماعی (Unions with Bodies) امکان غنیسازی این ساختارها با متدهای کمکی و اعضای سفارشی را فراهم کرده است. در ادامه، ساختار اتحادیههای دارای بدنه، پیادهسازی الگویOneOrMore، مدیریت حالتهای پوچپذیری (Nullability) و وضعیت مقدار پیشفرض (Default State) بررسی میشود؛ همچنین مقایسهای فنی میان این رویکرد نوین و راهکارهای سنتی مبتنی بر اورلود متدها ارائه خواهد شد.
Type Safety) را در زمان کامپایل تضمین کنند. سناریوی متداول زیر را در نظر بگیرید:// رویکرد سنتی: انفجار اورلودها
public void ApplyTags(string tag) { /* ... */ }
public void ApplyTags(IEnumerable<string> tags) { /* ... */ }
public void ApplyTags(params string[] tags) { /* ... */ }object یا تبدیل دستی صریح نیز ایمنی انواع را کاهش داده و تخصیص حافظه غیرضروری (Boxing/Allocation) ایجاد میکنند.Body) به تعریف Union است؛ این بدنه اجازه میدهد اعضا، مشخصهها و متدهای کمکی درست مانند هر نوع داده معمولی (Class یا Struct) در کنار ماهیت Union تعریف شوند.OneOrMoreOneOrMore است:public union OneOrMore<T>(T, IEnumerable<T>)
{
public IEnumerable<T> AsEnumerable() => Value switch
{
T single => [single],
IEnumerable<T> multiple => multiple,
null => []
};
}Call-site) است؛ کامپایلر تبدیلهای ضمنی (Implicit Conversions) مجاز را شناسایی کرده و فراخواننده میتواند بدون نیاز به تبدیل دستی، مقدار دلخواه را ارسال کند:// ارسال یک مقدار منفرد
OneOrMore<string> tags = "dotnet";
// ارسال یک آرایه یا مجموعه
OneOrMore<string> moreTags = new[] { "csharp", "unions", "preview" };
// پردازش یکپارچه در سمت متد
public void ProcessTags(OneOrMore<string> input)
{
foreach (var tag in input.AsEnumerable())
{
Console.WriteLine(tag);
}
}OneOrMore جایگزین تمامی اورلودهای سهگانه پیشین (T ،IEnumerable و params T[]) میشود و در عین حال خوانایی کد مبدا را به حداکثر میرساند.AsEnumerable، مدیریت حالت null صرفاً یک احتیاط ساده نیست، بلکه یک ضرورت مهندسی در تطبیق الگو (Pattern Matching) است:null => []
Maybe-Null State):Value در Union، حالت شاید پوچ (maybe-null) است. این قاعده تحلیلی کامپایلر را قادر میسازد تا هشدارهای مربوط به Nullability را در موارد زیر بهدرستی صادر کند:OneOrMore) که عناصر اولیه آنها مقداردهی نشدهاند.default(OneOrMore).NullReferenceException):struct پیادهسازی شود، ایجاد ساختار با کلیدواژه default هیچکدام از شاخههای معتبر (T یا IEnumerable) را مقداردهی نمیکند. شاخه null => [] تضمین میکند که اجرای متد تحت هیچ شرایطی با شکست مواجه نشده و همواره یک دنباله خالی اما معتبر بازگردانده شود.| مشخصه | رویکرد سنتی (Overload Explosion) | رویکرد نوین (Unions with Bodies) |
| تعداد تعاریف متد | حداقل ۲ الی ۳ متد برای هر عملیات | یک متد متمرکز |
| ایمنی نوع داده (Type Safety) | تضمینشده، اما با افزونگی امضا | تضمینشده در سطح زمان کامپایل |
| امکان گسترش (Extensibility) | نیاز به تغییر در چندین متد در صورت نیاز به لاجیک جدید | افزودن اعضای کمکی به بدنه Union بدون تغییر امضاها |
| سربار نگهداری | بالا (بهویژه با اضافه شدن متدهای جدید) | بسیار پایین |
| مدیریت مقدار خالی (Empty/Null) | نیازمند اعتبارسنجی در تکتک متدها | مدیریت متمرکز در الگوهای درونی نوع داده |
T single => [single]، استفاده از سینتکس Collection Expressions در صورت هدفگیری آرایهها ممکن است برای مقادیر منفرد تخصیص حافظه کوچک ایجاد کند. در سناریوهای با کارایی بسیار بالا، پیادهسازی یک Enumerator سفارشی بر پایه struct برای پیمایش بدون تخصیص حافظه (Zero-allocation) گزینهای ایدهآل است.T یک ساختار (struct) باشد، تفاوت رفتار بین مقدار پیشفرض default(T) و null باید در منطق دامنه در نظر گرفته شود.Unions with Bodies) نقطه عطفی در تکامل سیستم انواع داده زبان #C است. این ویژگی فاصله میان مدلهای جبری داده (Algebraic Data Types) و الگوهای شیءگرا را پر میکند. با بهکارگیری ساختارهایی نظیر OneOrMore، توسعهدهندگان داتنت قادرند APIهایی صریحتر، خلاصهتر و مقاومتر در برابر خطاهای زمان اجرا طراحی کنند و چالشهای تاریخی نظیر انفجار اورلودها را برای همیشه برطرف سازند.