عنوان:

‫نوع‌های اتحادیه (Union Types) در C# 15 و NET 11.


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۳/۱۰ ۰۹:۳۵
آدرس: www.dntips.ir
پس از سال‌ها درخواست جامعه توسعه‌دهندگان، سرانجام در C# 15 (همراه با NET 11.) قابلیت Union Types یا انواع اتحادیه به زبان اضافه شده‌است. این ویژگی یکی از ابزارهای قدرتمند برنامه‌نویسی تابعی است که در زبان‌هایی مانند F#، Rust و TypeScript وجود داشته و حالا به اکوسیستم .NET نیز راه یافته است. انواع اتحادیه به شما اجازه می‌دهند نوعی تعریف کنید که بتواند یکی از چند نوع متفاوت (حتی نامرتبط) را در خود نگه دارد. این قابلیت، الگوهای رایجی مانند Result و Option را بسیار تمیزتر و ایمن‌تر می‌کند و گزینه‌ای قدرتمند برای مدیریت داده‌هایی است که ممکن است ساختارهای مختلفی داشته باشند. در این مقاله به بررسی syntax، نحوه استفاده، نحوه پیاده‌سازی و نکات پیشرفته این ویژگی می‌پردازیم.

انواع اتحادیه چیست؟
یک Union Type نوعی است که می‌تواند یکی از چندین حالت (case) مختلف را نمایندگی کند. مثال کلاسیک آن Result است:
  • حالت Success: شامل مقدار موفقیت‌آمیز از نوع TSuccess
  • حالت Error: شامل خطا از نوع TError
به‌جای برگرداندن مقدار یا پرتاب Exception، متد، یک Result برمی‌گرداند و فراخواننده مجبور است هر دو حالت را به صورت صریح مدیریت کند. این الگو (Result Pattern) خطاهای زمان اجرا را کاهش می‌دهد و کد را قابل پیش‌بینی‌تر می‌کند.
قبل از C# 15، پیاده‌سازی این الگوها نیاز به کد boilerplate قابل توجه یا استفاده از کتابخانه‌هایی مانند OneOf داشت. حالا این کار به صورت بومی و با پشتیبانی کامل کامپایلر انجام می‌شود.

تعریف Union در C# 15
سینتکس جدید بسیار ساده و خواناست:
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);
سپس Union را تعریف می‌کنیم. دو روش برای ایجاد نمونه وجود دارد:
SupportedOS os1 = new SupportedOS(new MacOS("Tahoe", 25));

// تبدیل ضمنی (Implicit Conversion)
SupportedOS os2 = new MacOS("Tahoe", 25);

کار با Unionها: Switch Expression
بهترین و توصیه‌شده‌ترین روش کار با Unionها، استفاده از عبارت 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})"
    // نیازی به _ => نیست! کامپایلر بررسی می‌کند همه حالات پوشش داده شده باشند
};
یکی از نقاط قوت این پیاده‌سازی، Exhaustiveness Checking است. اگر یکی از حالات را فراموش کنید، کامپایلر هشدار می‌دهد. این ویژگی کد را بسیار امن‌تر می‌کند.
اگر یکی از انواع nullable باشد (مثلاً MacOS?)، باید حالت null را هم مدیریت کنید.

پیاده‌سازی‌های کاربردی
با این ویژگی می‌توان الگوهای کلاسیک را بسیار تمیز نوشت:
// Result
public union Result<T>(T, Exception);

// Option
public record class None;
public union Option<T>(None, T);

نحوه استفاده در پروژه
برای استفاده از این ویژگی در حال حاضر:
  • SDK .NET 11 Preview 4 یا بالاتر را نصب کنید.
  • در فایل .csproj گزینه زیر را اضافه کنید:
<LangVersion>preview</LangVersion>
توجه کنید که حتی اگر روی فریم‌ورک‌های قدیمی‌تر (مانند net8.0) تارگت کنید، این ویژگی به عنوان قابلیت کامپایلر کار می‌کند.

پشت صحنه: نحوه پیاده‌سازی
Unionهای تولیدشده توسط کامپایلر، ساختار (struct) ساده‌ای هستند که اینترفیس 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;
}

جلوگیری از Boxing با پیاده‌سازی سفارشی
پیاده‌سازی پیش‌فرض همه مقادیر را به 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 جلوگیری می‌شود.

ویژگی‌های آینده
در پیشنهاد زبان، ویژگی‌های بیشتری مانند Union Member Providers، Closed Enums و Closed Hierarchies در نظر گرفته شده که مدیریت حالات و exhaustiveness را حتی قدرتمندتر خواهد کرد.

نتیجه‌گیری
اضافه شدن Union Types به C# 15 یکی از مهم‌ترین پیشرفت‌های زبان در سال‌های اخیر است. این ویژگی پلی بین برنامه‌نویسی شیءگرا و تابعی ایجاد می‌کند و امکان نوشتن کد تمیزتر، ایمن‌تر و expressiveتر را فراهم می‌آورد. با پشتیبانی عالی از switch expression و exhaustiveness checking، Unionها نه تنها جایگزین خوبی برای الگوهای Result و Option هستند، بلکه ابزاری برای مدل‌سازی داده‌های متغیر به شمار می‌روند. توسعه‌دهندگانی که با F# یا زبان‌های تابعی کار کرده‌اند، این ویژگی را بسیار آشنا و دلنشین خواهند یافت.
Union Types، گام مهمی در جهت بالغ‌تر شدن #C به عنوان زبانی مدرن و چندپارادایم است.

نظرات

  • وحید نصیری در ۱۴۰۵/۰۳/۱۲ ۰۹:۱۹
    بازنویسی الگوی Result با استفاده از Union Types (نوع‌های اتحادی) در C# 15

    در دنیای توسعه نرم‌افزار، به‌ویژه در برنامه‌نویسی سمت سرور (Backend)، مدیریت صحیح و پیش‌بینی‌پذیر جریان داده‌ها و خطاهای احتمالی از اهمیت بالایی برخوردار است. همواره با متدهایی مواجه می‌شویم که خروجی آن‌ها چندوجهی است؛ به عنوان مثال، عملیات تحلیل (Parse) یک رشته که ممکن است به یک عدد صحیح یا یک خطای قالب‌بندی منجر شود، یا جستجوی یک رکورد در پایگاه داده که حاصل آن یا داده مورد نظر است و یا وضعیت «یافت نشد» (Not Found).
    تا پیش از این، زبان #C راهکار بومی و مستقیمی برای مدل‌سازی مفهوم «این نوع یا آن نوع» نداشت. توسعه‌دهندگان برای حل این چالش به الگوهای مختلفی پناه می‌بردند؛ از پرتاب استثناها (Exceptions) گرفته تا استفاده از مقادیر تهی‌پذیر (Nullable)، ساختارهای چندتایی (Tuples)، اینترفیس‌های نشانگر (Marker Interfaces) یا کتابخانه‌های شخص ثالث مانند 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 است، اما در واقعیت این یک ادعای ناقص است. متد فوق در صورت عدم یافتن کاربر، استثنا پرتاب می‌کند. این رویکرد دو ایراد ساختاری عمده دارد:
    • پنهان‌کاری در امضای متد (Method Signature): توسعه‌دهنده‌ای که این متد را فراخوانی می‌کند، بدون مطالعه بدنه متد یا مستندات، متوجه رفتار ناهمگام یا خطاهای احتمالی آن نخواهد شد.
    • کاهش عملکرد و انحراف از مفهوم استثنا: استفاده از استثناها برای کنترل جریان عادی برنامه (Control Flow via Exceptions) هزینه پردازشی بالایی دارد، زیرا فرآیند پشته‌گشایی (Stack Unwinding) سنگین است. استثناها باید صرفاً برای شرایط غیرمنتظره و بحرانی سیستم استفاده شوند، نه برای سناریوهای منطقی کسب‌وکار مانند یافت نشدن یک رکورد.

    راهکارهای جایگزین سنتی و محدودیت‌های آن‌ها
    روش جایگزینمزیتمحدودیت اصلی
    الگوی Try-Parse (خروجی bool و پارامتر out)عدم پرتاب استثناعدم پشتیبانی از برنامه‌نویسی ناهمگام (Async) و پیچیدگی ترکیب کدهای چندگانه
    کلاس سفارشی Result با فیلدهای Nullableامضای صریح متدافزایش کدهای کلیشه‌ای (Ceremony) و عدم اجبار کامپایلر به بررسی خطا
    کتابخانه OneOfشبیه‌سازی مناسب نوع‌های اتحادیوابستگی به کتابخانه خارجی و عدم پشتیبانی بومی در سطح زبان
    ظهور Union Types در C# 15
    یک Union Type یا «نوع اتحادی»، مجموعه‌ای بسته (Closed Set) از چند نوع داده‌ای متمایز است. بر خلاف وراثت، این نوع‌ها نیازی به داشتن جد مشترک یا ارتباط معنایی با یکدیگر ندارند.

    ۱. نحوه اعلان (Declaration)
    نحو (Syntax) معرفی شده در C# 15 بسیار موجز و کارآمد است. شما می‌توانید یک مجموعه بسته از انواع مختلف را در یک خط تعریف کنید:
    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);

    ۲. مقداردهی و تخصیص (Creating Values)
    به دلیل تعبیه تبدیل‌های ضمنی (Implicit Conversions) در لایه کامپایلر، نیازی به کدهای اضافه برای بسته‌بندی مقادیر نیست. شما می‌توانید به طور مستقیم مقدار مورد نظر را به متغیر اتحادی انتساب دهید:
    PaymentMethod method = new CreditCard("4242", "Visa");
    مجموعه انواع معتبر در اتحادیه، کاملاً بسته است؛ بنابراین تلاش برای انتساب هر نوع داده‌ای خارج از موارد ذکر شده در تعریف، منجر به خطای زمان کامپایل (Compile-time Error) خواهد شد.

    ۳. مصرف اتحادیه و قدرت انطباق الگو (Pattern Matching)
    نقطه اوج کارایی Union Types در زمان مصرف و بازخوانی داده‌ها با استفاده از عبارت 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.
    این ویژگی امنیت کد را به شدت افزایش می‌دهد؛ چرا که اگر در آینده نوع جدیدی به اتحادیه اضافه شود، کامپایلر تمام بخش‌هایی از نرم‌افزار را که نیاز به به‌روزرسانی دارند، به صورت هوشمند به توسعه‌دهنده نشان می‌دهد.

    بازنویسی الگوی Result و اصلاح متد نمونه
    حال با تکیه بر این قابلیت جدید، متد پنهان‌کار 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}"),
    };

    ملاحظات فنی و زیرساختی (Under the Hood)
    هر چند این قابلیت در فاز پیش‌نمایش (Preview) قرار دارد، اما درک رفتار زیرساختی آن برای توسعه‌دهندگان ارشد ضروری است:
    • پیاده‌سازی در سطح ساختار (Struct-based Compiler Representation): در پشت صحنه، کامپایلر یک Union را به یک struct تبدیل می‌کند. این ساختار حاوی یک فیلد متمایزکننده (Discriminator/Tag) برای تشخیص نوع فعلی و یک ارجاع تخصیص‌یافته به داده است.
    • مدیریت حافظه و Boxing: در حالت پیش‌فرض، مقادیرِ نوعِ مرجع (Reference Types) و نوع‌های مقدار (Value Types) ممکن است درون یک فیلد مشترک نظیر object? ذخیره شوند که این امر برای انواع ساختاری منجر به بروز پدیده Boxing می‌شود. با این حال، تیم توسعه دات‌نت در حال توسعه یک مسیر بهینه‌سازی بدون نیاز به Boxing (Non-boxing path) با استفاده از الگوهای پیشرفته‌تر چیدمان حافظه (LayoutKind.Explicit) برای کدهای با کارایی بالا (Performance-sensitive) است.
    • تفاوت با Discriminated Unions جامع: شایان ذکر است که ویژگی فعلی در C# 15 یک Type Union (اتحادیه‌ای از انواع موجود) است و هنوز به یک Discriminated Union کامل (همراه با برچسب‌های نام‌گذاری شده اختصاصی در یک ساختار درختی واحد، مشابه زبان F# یا Rust) تبدیل نشده‌است. با این حال، این پیاده‌سازی بیش از ۹۰ درصد نیازهای روزمره توسعه‌دهندگان را پوشش می‌دهد.

    نتیجه‌گیری
    معرفی Union Types در C# 15، یکی از بزرگترین خلاءهای این زبان را در زمینه برنامه‌نویسی ماژولار و ایمن برطرف می‌سازد. بازنویسی الگوی Result با این ابزار نوظهور، ما را از معماری‌های پیچیده قدیمی و پنهان‌کاری متدها نجات می‌دهد. به کمک این قابلیت، امضای متدها بازتاب‌دهنده واقعیت خروجی برنامه خواهد بود و سیستم کنترل خطای زمان کامپایل، ریسک بروز خطاهای پیش‌بینی‌نشده در زمان اجرا (Runtime) را به حداقل می‌رساند. این گامی بزرگ به سمت کدهای خواناتر، پایدارتر و با نگهداری آسان‌تر در اکوسیستم دات‌نت است.
  • وحید نصیری در ۱۴۰۵/۰۳/۱۴ ۰۸:۲۴
    اتحادیه‌های تفکیک‌شده در #C و پلی به دنیای واریانت‌های VB6

    توسعه‌دهندگانی که سال‌ها با کدبیس‌های VB6 کار کرده‌اند، به خوبی با نوع Variant آشنا هستند. این نوع داده انعطاف‌پذیر اجازه می‌داد یک تابع گاهی یک رشته، گاهی یک عدد، گاهی یک آرایه، و گاهی هم مقدار Nothing یا خطا برگرداند. این الگو، ساده، سریع و بسیار عملی بود، اما هنگام مدرن‌سازی این کدبیس‌ها به سمت #C، همیشه یکی از بزرگ‌ترین چالش‌ها بوده‌است.
    #C پیش از نسخه دات‌نت ۱۱، فاقد ابزاری تمیز و مستقیم، برای بیان مفهوم «این تابع دقیقاً یکی از چند چیز متفاوت را برمی‌گرداند» بود. اما با معرفی Union Types (اتحادیه‌های تفکیک‌شده) در .NET 11، این شکاف مهم پر شده‌است. این ویژگی که در F# با نام Discriminated Unions شناخته می‌شود، اکنون به #C نیز آمده و یکی از مهم‌ترین پیشرفت‌ها برای پروژه‌های مهاجرت از زبان‌های قدیمی محسوب می‌گردد.

    الگوی رایج واریانت در VB6
    در VB6، توابع دسترسی به داده بسیار رایج بودند که از نوع 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 Function
    فراخوانی‌کننده باید با IsEmpty، IsArray یا VarType بررسی می‌کرد که چه چیزی دریافت کرده است. این رویکرد هرچند غیررسمی بود، اما دقیقاً منظور برنامه‌نویس را بیان می‌کرد: تابع یکی از سه حالت ممکن را برمی‌گرداند.

    چالش ترجمه به #C پیش از اتحادیه‌ها
    پیش از .NET 11، توسعه‌دهندگان برای شبیه‌سازی این رفتار مجبور به یکی از راه‌حل‌های زیر بودند که هیچ‌کدام رضایت‌بخش نبودند:
    • پرتاب استثنا برای حالت «پیدا نشد»؛ که از نظر معنایی نادرست است.
    • استفاده از Tuple nullable یا چندین پارامتر خروجی.
    • ایجاد کلاس نتیجه سفارشی با یک enum تشخیص‌دهنده (Discriminator)؛ که بسیار پرحجم و فاقد پشتیبانی کامپایلری قوی بود.
    هیچ‌کدام از این راه‌حل‌ها به اندازه کد اصلی VB6 مستقیم و واضح نبودند.

    اتحادیه‌های تفکیک‌شده (Discriminated Unions) در .NET 11
    با سینتکس جدید #C در .NET 11، می‌توانیم همان هدف را به شکلی تمیز، ایمن و مدرن بیان کنیم:
    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}"
    };
    یکی از قدرتمندترین ویژگی‌های این رویکرد، بررسی کامل‌پوشانی (Exhaustiveness Checking) است. اگر توسعه‌دهنده‌ای بعداً یک حالت جدید (مثلاً Suspended) به اتحادیه اضافه کند، کامپایلر همه switchهای موجود را علامت‌گذاری می‌کند تا مطمئن شود همه حالت‌ها مدیریت شده‌اند. این چیزی است که نوع Variant در VB6 هرگز نداشت.

    اهمیت این ویژگی برای پروژه‌های مدرن‌سازی
    برای تیم‌هایی که در حال مهاجرت کدبیس‌های VB6، PowerBuilder (با نوع Any) یا Clarion هستند، اتحادیه‌ها تحول بزرگی ایجاد کرده‌اند. دیگر نیازی به استفاده از کتابخانه‌های شخص ثالث مانند OneOf یا ساختن boilerplate زیاد نیست. کد تولیدی هم وفادار به معنای اصلی است و هم ایدیوماتیک برای #C مدرن. این ویژگی بخشی از روند گسترده‌تری است که در سال‌های اخیر در .NET رخ داده: Records، Pattern Matching پیشرفته، Required Members و اکنون Union Types. هر کدام از این امکانات، هدف مهاجرت را جذاب‌تر و هزینه نگهداری روی پلتفرم قدیمی را بالاتر می‌برند.
    • وحید نصیری در ۱۴۰۵/۰۳/۱۴ ۰۹:۵۴
      توضیح دقیق‌تر دربارهٔ public union CustomerLookupResult
      این ساختار، یکی از زیباترین و کاربردی‌ترین کاربردهای Union Types (اتحادیه‌های تفکیک‌شده) در C# 15 / .NET 11 است. اجازه دهید به صورت کامل و گام‌به‌گام آن را بررسی کنیم.

      ۱. مفهوم کلی
      union نوعی از نوع داده است که می‌گوید: «مقدار این متغیر دقیقاً یکی از این حالت‌ها (cases) را دارد، نه بیشتر و نه کمتر.»
      این مفهوم در نظریهٔ انواع (Type Theory) به نام Sum Type یا Discriminated Union شناخته می‌شود. برخلاف class معمولی یا record، اینجا کامپایلر می‌داند که فقط یکی از حالت‌ها ممکن است وجود داشته باشد.

      ۲. تجزیهٔ سینتکس
      public union CustomerLookupResult
      {
          Customer(string Name, decimal CreditLimit, CustomerStatus Status);
          NotFound;
          Error(string Message);
      }
      public union CustomerLookupResult
      • نام اتحادیه را تعریف می‌کند. این نوع جدید، مثل یک record یا class عمل می‌کند اما رفتار متفاوتی دارد.
      Customer(...)
      • یک case (حالت) با داده‌های مرتبط. معادل یک record موقعیتی (positional record) است.
      • وقتی از این حالت استفاده می‌کنید، باید مقادیر Name، CreditLimit و Status را بدهید.
      NotFound;
      • یک case بدون داده (parameterless). مثل یک singleton است. معادل NotFound یک حالت خالی و ثابت.
      Error(string Message);
      • یک case که فقط یک پیام خطا دارد.
      هر کدام از این سه خط، در واقع یک زیرنوع (nested type) از CustomerLookupResult ایجاد می‌کنند.

      ۳. نحوهٔ ایجاد مقدار (Instantiation)
      // حالت موفق
      return new CustomerLookupResult.Customer(علی رضایی", 15000000", CustomerStatus.Active);
      
      // حالت پیدا نشد
      return new CustomerLookupResult.NotFound();
      
      // حالت خطا
      return new CustomerLookupResult.Error("اتصال به دیتابیس برقرار نشد");

      ۴. نحوهٔ مصرف (Pattern Matching)
      بهترین و ایمن‌ترین روش استفاده، switch expression است:
      var message = result switch
      {
          CustomerLookupResult.Customer c => 
              $"مشتری یافت شد: {c.Name} - اعتبار: {c.CreditLimit:C}",
      
          CustomerLookupResult.NotFound => 
              "مشتری با این شناسه وجود ندارد",
      
          CustomerLookupResult.Error e => 
              $"خطا در جستجو: {e.Message}",
      
          // اگر حالتی اضافه شود و اینجا پوشش داده نشود، کامپایلر هشدار می‌دهد
      };

      ۵. ویژگی‌های مهم و قدرتمند
      Exhaustiveness Checking (بررسی کامل‌پوشانی):
      • قوی‌ترین مزیت. کامپایلر تضمین می‌کند که همه حالت‌های ممکن را مدیریت کرده‌اید. اگر بعداً یک حالت جدید مثل Suspended اضافه کنید، تمام switchهای قبلی خطای کامپایل یا warning می‌گیرند.
      Type Safety کامل:
      • برخلاف Variant در VB6 یا object در C# قدیمی، اینجا اشتباه نوع (مثل فرض کردن string بودن وقتی NotFound است) در زمان کامپایل گرفته می‌شود.
      کم بودن Boilerplate:
      • قبلاً برای رسیدن به همین سطح ایمنی باید یک abstract class + recordهای مشتق‌شده + enum discriminator می‌ساختید که خیلی طولانی‌تر بود.
      ادغام عالی با Pattern Matching:
      • می‌توانید از when، property patterns و غیره هم استفاده کنید.

      ۶. مقایسه با VB6 Variant
      ویژگیVB6 Variant C# Union (.NET 11)
      انعطاف‌پذیریبسیار بالامحدود به موارد تعریف‌شده
      ایمنی نوعبسیار کمبسیار بالا (کامپایلر-محور)
      بررسی کامل حالت‌هادستی (IsEmpty, VarType)خودکار توسط کامپایلر
      خوانایی کدمتوسطبسیار بالا
      نگهداری در بلندمدتسختآسان
      نکته نهایی:
      این سینتکس دقیقاً همان چیزی است که توسعه‌دهندگان VB6 سال‌ها آرزویش را داشتند؛ توانایی بیان «یکی از این چند حالت» بدون از دست دادن ایمنی نوع. public union در .NET 11 پلی بسیار قوی بین تجربهٔ عملی زبان‌های قدیمی Basic و قدرت مدرن #C ایجاد کرده است.
  • وحید نصیری در ۱۴۰۵/۰۴/۲۸ ۱۰:۳۴
    مطلب تکمیلی: یکپارچگی Union Types با اکوسیستم ASP.NET Core و System.Text.Json

    در ادامه‌ی بررسی ویژگی جدید Union Types در C# 15 و .NET 11، یکی از مهم‌ترین دغدغه‌های توسعه‌دهندگان، نحوه رفتار این ابزار نوظهور در لایه‌های مختلف فریم‌ورک وب، به خصوص در بخش سریال‌سازی (Serialization) و ارتباطات شبکه است. خوشبختانه تیم دات‌نت این ویژگی را به صورت کاملاً بومی (Native) در سراسر پشته‌ی فناوری دات‌نت ادغام کرده است. در این مطلب تکمیلی، به بررسی رفتار اتحادیه‌ها در دات‌نت ۱۱ (از نسخه Preview 6 به بعد) می‌پردازیم.

    ۱. یکپارچگی بومی با ASP.NET Core
    از آنجا که هسته وب در دات‌نت از کتابخانه System.Text.Json برای مدیریت داده‌های JSON استفاده می‌کند، Union Typeها بدون نیاز به هیچ‌گونه پیکربندی خاص یا کدهای کلیشه‌ای اضافی (Zero-Configuration)، در تمامی بخش‌های ASP.NET Core به عنوان بدنه درخواست (Request Body) یا نوع خروجی (Return Type) پشتیبانی می‌شوند:
    • Minimal APIs: شما می‌توانید از نوع‌های اتحادی مستقیماً به عنوان پارامتر ورودی یا خروجی متدها استفاده کنید. این پشتیبانی هم در بخش زمان اجرا (RequestDelegateFactory) و هم در لایه‌ی سورس‌جنراتورها (Source-Generated Request Delegates) پیاده‌سازی شده است. همچنین ساختارهایی مثل Task، IAsyncEnumerable و Results به طور کامل پوشش داده شده‌اند.
    • MVC و Razor Pages: مدل‌سازی داده‌ها در بدنه درخواست‌ها و پاسخ‌های JSON در کنترلرهای سنتی به راحتی با Union Typeها ممکن است.
    • SignalR: در صورت استفاده از پروتکل JSON Hub، می‌توانید از اتحادیه‌ها به عنوان پارامتر متدهای هاب، مقادیر بازگشتی و آیتم‌های استریم (Stream Items) استفاده کنید.
    • Blazor: در دنیای فرانت‌اند دات‌نت، اتحادیه‌ها می‌توانند به عنوان پارامترهای کامپوننت، آرگومان‌ها و نتایج ارتباط با جاوااسکریپت (JS Interop) و همچنین برای ذخیره‌سازی وضعیت کامپوننت‌ها (Persisted Component State) به کار گرفته شوند.

    نمونه کد در Minimal APIs و نگاشت در OpenAPI
    زمانی که یک متد خروجی اتحادی دارد، کامپایلر دقیقاً مقدار حالت فعال (Active Case) را به بیرون سریالایز می‌کند و در سند OpenAPI (یا همان Swagger) این رفتار با استفاده از کلمات کلیدی استاندارد مانند 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));

    ۲. پشتیبانی بومی در لایه سریال‌سازی (System.Text.Json)
    در نسخه‌های ابتدایی پیش‌نمایش (مانند Preview 5)، توسعه‌دهندگان مجبور بودند انواع پشتیبان کامپایلر (UnionAttribute و IUnion) را به صورت دستی بازنویسی کنند. اما از نسخه Preview 6 به بعد، این انواع به صورت داخلی در فضای نام System.Runtime.CompilerServices قرار گرفته‌اند و پروژه بدون هیچ کد اضافه‌ای کامپایل می‌شود.
    مکانیزم سریال‌سازی System.Text.Json اکنون قرارداد جدیدی به نام JsonTypeInfoKind.Union را شناسایی می‌کند. این مکانیزم هم در حالت سریالایزر مبتنی بر رفلکشن (Reflection-based) و هم در سورس‌جنراتورها (Source Generators) کار می‌کند.

    نحوه خروجی JSON (Round-tripping)
    هنگام سریالایز کردن یک اتحادیه، فریم‌ورک مستقیماً مقدارِ حالتِ فعال را می‌نویسد. به عنوان مثال، اگر اتحادیه Pet در حال حاضر حاوی نمونه Dog("Rex") باشد، خروجی JSON دقیقاً به صورت زیر خواهد بود:
    {"Name":"Rex"}
    این یعنی یک اتحادیه ساده که مثلاً ترکیبی از int و string است، به شکل کاملاً تمیز و بدون آبجکت‌های پیچیده تودرتو منتقل می‌شود:
    • در حالت فعال بودن رشته: "hello"
    • در حالت فعال بودن عدد: 42
    توسعه‌دهندگان در صورت نیاز به شخصی‌سازی نام‌گذاری یا نحوه کشف حالت‌ها (Cases)، می‌توانند از APIهای جدیدی نظیر JsonUnionAttribute، JsonUnionCaseInfo و کلاس‌بندهای نوع (JsonTypeClassifier و JsonSerializerOptions.TypeClassifiers) استفاده کنند.

    ۳. اصلاح قوانین زبان و رفع باگ‌ها در کامپایلر (Preview 6)
    تیم توسعه دات‌نت در آخرین به‌روزرسانی‌ها، قوانین زبان C# را برای هندل کردن دقیق‌تر اتحادیه‌ها بهینه‌سازی کرده است:
    • سازنده‌های غیرعمومی (Non-public Constructors): اعلان‌های اتحادیه اکنون می‌توانند از سازنده‌های غیرعمومی (مانند private یا internal) که تنها یک پارامتر می‌گیرند استفاده کنند.
    • اصلاح الگوی Not (Not Pattern): الگوی not در انطباق الگو (Pattern Matching) اکنون به خودِ مقدار ورودی اتحادیه اعمال می‌شود، نه به ارزش درونی آن.
    • ارث‌بری متدهای ساخت: نوع‌های اتحادی سفارشی (Custom Union Types) اکنون از ارث‌بری متدهای تولید شده‌ی Create پشتیبانی می‌کنند.
    • خطاهای صریح زمان کامپایل: اگر در اعلان یک اتحادیه سفارشی، حداقل مجموعه APIهای مورد نیاز وجود نداشته باشد، کامپایلر خطاهای بسیار شفاف و دقیقی را گزارش می‌کند تا توسعه‌دهنده سردرگم نشود.

    خلاصه تکمیلی
    قابلیت Union Types دیگر یک ایده انتزاعی در سطح کامپایلر نیست؛ بلکه با ورود به زیرساخت‌های اصلی دات‌نت ۱۱ مانند System.Text.Json و ASP.NET Core تبدیل به ابزاری آماده برای تولید محصول (Production-ready) شده است. این ویژگی به ما اجازه می‌دهد APIهایی با خروجی‌های چندگانه و کاملاً مستند (از طریق OpenAPI بومی) بدون تحمیل بار پردازشی سنگین یا کدهای پیچیده، معماری کنیم.
  • وحید نصیری در ۱۴۰۵/۰۶/۱۱ ۰۸:۵۰
    چالش‌های سریال‌سازی و ابهام‌زدایی: رفتار پیشرفته System.Text.Json در مواجهه با Union Types و سلسه‌مراتب بسته

    در مباحث پیشین، به پشتیبانی بومی فریم‌ورک از Union Typeها در ASP.NET Core و نگاشت مستقیم آن‌ها در خروجی‌های وب پرداخته شد. با این حال، با انتشار پیش‌نمایش‌های جدیدتر دات‌نت ۱۱ (به‌ویژه Preview 7)، ابعاد عمیق‌تر و لبه‌های تیزی (Sharp Edges) از رفتار System.Text.Json نمایان شده است. برخلاف تصور اولیه، سریال‌سازی و بازخوانی (Deserialization) داده‌های چندشکلی به سادگی و بدون دردسر صورت نمی‌گیرد.
    در این بخش تکمیلی، با تمرکز بر چالش‌های سریال‌سازی اتحادیه‌ها، پدیده ابهام در توکن‌های JSON، تفاوت Union با «سلسله‌مراتب بسته» (Closed Class Hierarchies) و راهکارهای حل آن را بررسی می‌کنیم.

    ۱. تفاوت ساختاری: Union Types در برابر Closed Class Hierarchies
    در C# 15 علاوه بر نوع‌های اتحادی، مفهوم کلیدی دیگری به نام سلسله‌مراتب بسته با کلمه کلیدی closed معرفی شده است:
    • Union Types: نوعی پیوند بدون وراثت بین چند تایپ کاملاً بی‌ارتباط با یکدیگر است.
    • Closed Class: کلاسی پایه که ذاتاً شبیه یک کلاس انتزاعی (abstract) رفتار می‌کند، اما اجازه ارث‌بری از آن تنها در درون همان اسمبلی (Assembly) ممکن است. تلاش برای ارث‌بری از کلاسی با نشانگر closed خارج از اسمبلی منجر به خطای کامپایل (CS9382) خواهد شد.
    هر دو ساختار امکان بررسی جامعیت (Exhaustiveness) را در الگوهای switch به کامپایلر می‌دهند، اما نحوه برخورد سریالایزر با آن‌ها کاملاً متفاوت است.

    ۲. چالش سریال‌سازی سلسه‌مراتب بسته و ویژگی InferClosedTypePolymorphism
    در حالت پیش‌فرض، اگر نمونه‌ای از کلاس‌های مشتق‌شده را در قالبی با تایپ پایه closed سریالایز کنید، تنها فیلدهای کلاس پایه به خروجی منتقل شده و ویژگی‌های فرزند از دست می‌روند.
    برای رفع نیاز به نوشتن دستی اتریبیوت‌های متعدد [JsonDerivedType]، گزینه‌ای نوین در JsonSerializerOptions به نام InferClosedTypePolymorphism معرفی شده است تا سلسله‌مراتب کلاس‌های بسته را در زمان کامپایل/اجرا به صورت خودکار شناسایی کند. با این حال، در پیش‌نمایش‌های اولیه محدودیت‌های سخت‌گیرانه‌ای وجود دارد:
    • سطح دسترسی (Accessibility): تمام کلاس‌های فرزند استنتاج‌شده باید دست‌کم هم‌سطح کلاس پایه باشند (کلاس فرزند نمی‌تواند internal باشد اگر کلاس والد public است).
    • سلسله‌مراتب چندلایه‌ای (Nested Hierarchies): در صورتی که ساختار وراثت بیش از یک لایه عمق داشته باشد (مثلاً کلاس فرزند واسطه‌ای خودش closed یا abstract باشد)، استنتاج خودکار با خطا مواجه می‌شود و توسعه‌دهنده کماکان ناچار است از اتریبیوت‌های صریح [JsonDerivedType] به همراه تفکیک‌کننده $type استفاده کند.

    ۳. بحران بازخوانی اتحادیه‌ها (The Ambiguity Trap)
    رفتار پیش‌فرض System.Text.Json برای یک اتحادیه بر مبنای «انتقال تمیز» (Clean Payload) است؛ به این معنا که شیء را بدون هیچ کلید اضافی یا تفکیک‌کننده‌ای نظیر $type مستقیماً به مقدار نوع فعال سریالایز می‌کند. به عنوان مثال:
    • نوع IntOrString(42) دقیقاً به صورت عدد 42 نوشته می‌شود، نه { "Value": 42 }.
    • نوع Windows("11") به صورت شیء {"Version": "11"} ذخیره می‌گردد.

    مشکل کجاست؟ (عدم تقارن Round-trip)
    این رویکرد زمانی که قصد بازخوانی (Deserialization) داده را دارید، با بن‌بست روبه‌رو می‌شود. سریالایزر تنها در حالتی قادر است به صورت خودکار یک اتحادیه را بازیابی کند که انواع موجود در آن، متناظر با توکن‌های متفاوتی در استاندارد JSON باشند:
    // این اتحادیه‌ها به درستی Deserialized می‌شوند (عدم تداخل توکن)
    public union IntOrString(int, string);        // Number در برابر String
    public union DoubleOrGuid(double, Guid);      // Number در برابر String
    public union BoolOrDict(bool, Dictionary);     // Boolean در برابر Object
    اما اگر دو حالت در اتحادیه، توکن JSON یکسانی تولید کنند، عملیات بازخوانی با خطای 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 تولید می‌کنند

    ۴. راهکارهای ابهام‌زدایی در سریال‌سازی اشیاء هم‌توکن
    روش اول: پیاده‌سازی کلاس‌بند ساختاری دستی (Custom JsonTypeClassifierFactory)
    در غیاب تفکیک‌کننده‌های نوع صریح نظیر $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
    برای جلوگیری از کدنویسی‌های دستی خسته‌کننده بالا، کلاسی به نام JsonUnionTypeStructuralClassifier تعبیه شده است. این ابزار به شکل سراسری و خودکار فیلدهای اختصاصی اشیاء را برای تفکیک بررسی می‌کند.
    با این وجود، تطبیق ساختاری خودکار محدودیت‌ها و هزینه‌های خاص خود را دارد:
    • بار پردازشی (Performance Penalty): بررسی تفاوت نام فیلدها قبل از ساخت شیء، نیازمند اسکن بایت‌های اولیه JSON در حافظه است که عملکرد پردازشی را کاهش می‌دهد.
    • نیاز به فیلدهای متمایز و الزامی: اگر کلاس‌های موجود در اتحادیه هیچ فیلد انحصاری نداشته باشند، یا فیلدهای اختصاصی به عنوان 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);

    جمع‌بندی تکمیلی
    رویکرد دات‌نت ۱۱ در قبال سریال‌سازی Union Types، اولویت را به سازگاری با ساختارهای JSON دنیای وب و تایپ‌اسکریپت (Flat/Clean JSON) داده است، نه چرخه رفت‌وبرگشت بومی بدون دست‌کاری. بنابراین، اگر اتحادیه‌های شما شامل چند نوع شیء (Object) با خروجی‌های هم‌توکن باشند، تکیه بر تنظیمات پیش‌فرض کافی نخواهد بود؛ در این سناریوها باید ساختار داده‌ها را با استفاده از فیلدهای الزامی منحصربه‌فرد غنی‌سازی کرده و از کلاس‌بندهای ساختاری (JsonUnionTypeStructuralClassifier یا سفارشی) بهره بگیرید.
  • وحید نصیری در ۱۴۰۵/۰۶/۱۵ ۰۹:۱۱
    کاوش در انواع اجتماع (Union Types) در C# 15 و بازتاب حالت‌های ممکن در زمان اجرا

    چکیده: با معرفی اولیه و آزمایشی انواع اجتماع (Discriminated Unions / Union Types) در نسخه پیش‌نمایش #C 15 و پلتفرم NET 11.، زبان سی‌شارپ گام بلندی به‌سوی سیستم‌های نوع‌بندی جبری (Algebraic Data Types) و همگامی با الگوهای برنامه‌نویسی تابعی مدرن برداشته است. بااین‌حال، در نگارش‌های نخستین این ویژگی، قابلیت داخلی زبان یا API مستقیمی برای استخراج فهرستی از کلیه حالت‌های معتبر یک نوع اجتماع (Case Types) در زمان اجرا یا کامپایل بدون بررسی دستی پیش‌بینی نشده است. این مقاله با تکیه بر ساختار کامپایلر Roslyn و پیاده‌سازی زیربنایی رابط IUnion، متدهای توسعه‌یافته‌ای (Extension Methods) مبتنی بر Reflection ارائه می‌دهد که امکان کشف پویا و استاتیک حالت‌های تشکیل‌دهنده Union را فراهم می‌سازد. همچنین ضمن تحلیل چالش‌های عملکردی Reflection سنتی، راهکارهای بهینه‌سازی از جمله کش‌گذاری حافظه و استفاده از مولدهای سورس (Source Generators) به‌عنوان راهکار جایگزین مورد ارزیابی قرار می‌گیرند.

    ۱. مقدمه
    سال‌هاست توسعه‌دهندگان اکوسیستم دات‌نت در انتظار افزودن انواع داده جبری، به‌ویژه Discriminated Unions، به زبان #C هستند. پیش از این، پیاده‌سازی سناریوهایی نظیر «نتیجه یا خطا» (Result) یا دامنه‌های مقید چندحالته، مستلزم وراثت کلاس‌های مجرد، کتابخانه‌های جانبی (مانند OneOf) یا ساختارهای دستی بود که اغلب فاقد بررسی جامعیت الگوی تطبیق (Exhaustive Pattern Matching) در سطح کامپایلر بودند.
    با نزدیک شدن به انتشار پاییز ۲۰۲۶ پلتفرم NET 11. و معرفی پیش‌نمایش #C 15، مایکروسافت کلمه کلیدی union را به عنوان بخشی بومی از سیستم نوع‌بندی معرفی کرده است. با وجود این تکامل، در حوزه‌هایی مانند اعتبارسنجی خودکار، سریال‌سازی/دی‌سریال‌سازی پویا (Serialization)، ساخت اسناد OpenAPI و ابزارهای نگاشت شیء، نیاز به استخراج برنامه‌نویسی‌شده‌ی حالت‌های یک Union کماکان وجود دارد. از آنجا که API مستقیمی در زبان برای برشمردن این حالات وجود ندارد، شناخت زیرساخت کامپایلر و استفاده از متدهای کمکی Reflection نقشی کلیدی ایفا می‌کند.

    ۲. پیکربندی و الزامات محیط توسعه
    برای آزمایش زودهنگام قابلیت‌های پیش‌نمایش C# 15، پیکربندی فایل پروژه (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>
    ابزارهای مورد نیاز:
    • پیش‌نمایش SDK نسخه .NET 11
    • نسخه Visual Studio Code Insiders
    • آخرین ویرایش افزونه‌های C# Dev Kit و C# در حالت پیش‌نمایش

    ۳. مکانیزم زیربنایی Union در C# 15
    هنگام تعریف یک Union ساده در C# 15، کامپایلر ساختاری تولید می‌کند که به‌طور ضمنی واسط زیرساختی 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

    ۵. تحلیل معماری و نکات تکمیلی جهت بهینه‌سازی (Enrichment)
    هرچند روش مبتنی بر Reflection فوق نیازمندی‌های اولیه‌ی کاوش فراداده را مرتفع می‌سازد، اما در یک چارچوب استاندارد نرم‌افزاری باید به چند ملاحظه معماری توجه داشت:

    الف) کاهش هزینه Reflection با کش‌گذاری حافظه (Caching)
    عملیات 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());
    }

    ب) محدودیت در Manual Unions
    چنانچه توسعه‌دهنده از الگوهای دستی (Manual Union) استفاده کرده و کامپایلر را ملزم به نگاشت خودکار نکند، یا سازنده‌های چندعاملی دیگری به نوع افزوده شده باشند، فرض «سازنده تک‌عاملی نوع حالت است» ممکن است در موارد خاص استثنائاتی ایجاد کند. در خطوط تولید واقعی نرم‌افزار، تکیه بر اینترفیس‌ها یا ویژگی‌های متادیتایی (Attributes) اختصاصی، اطمینان بیشتری فراهم می‌کند.

    ج) رویکرد ایده‌آل: مولدهای سورس (C# Source Generators)
    به‌جای تحمیل بار محاسباتی به زمان اجرا (Runtime Reflection)، توسعه‌دهندگان می‌توانند با بهره‌گیری از Roslyn Source Generators در زمان کامپایل، ویژگی یا متد استاتیکی نظیر Pet.AllCases یا Pet.CaseTypes تولید کنند. این کار علاوه بر حفظ عملکرد در مقیاس بالا (AOT / Native AOT-friendly)، از خطاهای زمان اجرا جلوگیری می‌کند.

    ۶. نتیجه‌گیری
    ورود انواع اجتماع (Unions) به C# 15 و NET 11. یکی از برجسته‌ترین گام‌های مایکروسافت در غنی‌سازی سیستم نوع‌بندی این زبان است. تا زمان ارائه یک API جامع و بازتابی داخلی در سطوح بعدی پلتفرم دات‌نت، متدهای الحاقی مطرح‌شده روشی استاندارد، بدون شکست ساختار و تمیز برای پیمایش و اعتبارسنجی انواع حالت‌های Union در سناریوهای پویای نرم‌افزاری به‌شمار می‌روند. ترکیب این قابلیت با متدهای الگوی تطبیق (Pattern Matching)، خوانایی کد را ارتقا داده و خطاها را در زمان اجرا به حداقل می‌رساند.
  • وحید نصیری در ۱۴۰۵/۰۶/۱۹ ۰۹:۲۵
    بررسی قابلیت بدنه در انواع اجتماعی (Unions with Bodies) در سی‌شارپ: حل چالش یک یا چند مقدار

    چکیده: یکی از چالش‌های مکرر در طراحی رابط‌های برنامه‌نویسی (API Design) در زبان #C، نیاز به پشتیبانی هم‌زمان از یک شیء منفرد و مجموعه‌ای از اشیاء است. رویکرد مرسوم برای حل این مسئله، ارائه چندریختی یا اورلودهای متعدد متدها (Overloads Explosion) است که منجر به پیچیدگی و افزونگی کد می‌شود. با مطرح شدن و پیشرفت طرح انواع اجتماعی تفکیک‌شده (Discriminated Unions) در سی‌شارپ مدرن، قابلیت تعریف بدنه برای انواع اجتماعی (Unions with Bodies) امکان غنی‌سازی این ساختارها با متدهای کمکی و اعضای سفارشی را فراهم کرده است. در ادامه، ساختار اتحادیه‌های دارای بدنه، پیاده‌سازی الگوی OneOrMore، مدیریت حالت‌های پوچ‌پذیری (Nullability) و وضعیت مقدار پیش‌فرض (Default State) بررسی می‌شود؛ همچنین مقایسه‌ای فنی میان این رویکرد نوین و راهکارهای سنتی مبتنی بر اورلود متدها ارائه خواهد شد.

    ۱. مقدمه
    طراحی کتابخانه‌ها و سرویس‌های مقیاس‌پذیر در دات‌نت مستلزم ایجاد APIهایی است که هم انعطاف‌پذیر باشند و هم ایمنی نوع داده (Type Safety) را در زمان کامپایل تضمین کنند. سناریوی متداول زیر را در نظر بگیرید:
    // رویکرد سنتی: انفجار اورلودها
    public void ApplyTags(string tag) { /* ... */ }
    public void ApplyTags(IEnumerable<string> tags) { /* ... */ }
    public void ApplyTags(params string[] tags) { /* ... */ }
    این سناریو که به مسئله مقدار منفرد یا مجموعه (Single-Value-or-Collection Problem) معروف است، تیم‌های مهندسی را ناچار به نگهداری چندین متد موازی می‌کند که عموماً منطقی یکسان را اجرا می‌کنند. راهکارهای مبتنی بر ارسال object یا تبدیل دستی صریح نیز ایمنی انواع را کاهش داده و تخصیص حافظه غیرضروری (Boxing/Allocation) ایجاد می‌کنند.
    طرح پیشنهادی Unions در تکامل زبان #C ابزاری قدرتمند برای مدل‌سازی صریح مقادیر چندریختی به شمار می‌رود. یکی از ویژگی‌های کلیدی این طرح، امکان افزودن بدنه (Body) به تعریف Union است؛ این بدنه اجازه می‌دهد اعضا، مشخصه‌ها و متدهای کمکی درست مانند هر نوع داده معمولی (Class یا Struct) در کنار ماهیت Union تعریف شوند.

    ۲. ساختار Unions با بدنه: الگویOneOrMore
    زمانی که یک Union دارای بدنه باشد، می‌توان رفتارهای اشتراکی مورد نیاز مصرف‌کنندگان را به‌طور مستقیم درون آن کپسوله کرد. الگوی مرجع برای نمایش این قدرت، نوع داده‌ای با عنوان OneOrMore است:
    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[]) می‌شود و در عین حال خوانایی کد مبدا را به حداکثر می‌رساند.

    ۳. تحلیل دقیق وضعیت‌های Null و مقدار پیش‌فرض (Default State)
    در پیاده‌سازی متد AsEnumerable، مدیریت حالت null صرفاً یک احتیاط ساده نیست، بلکه یک ضرورت مهندسی در تطبیق الگو (Pattern Matching) است:
    null => []
    علت این الزام در دو جنبه نهفته است:
    وضعیت پوچ‌پذیری پیش‌فرض (Maybe-Null State):
    • وضعیت پیش‌فرض مشخصه درونی Value در Union، حالت شاید پوچ (maybe-null) است. این قاعده تحلیلی کامپایلر را قادر می‌سازد تا هشدارهای مربوط به Nullability را در موارد زیر به‌درستی صادر کند:
    • تعریف آرایه‌ای از Unionها (مانند OneOrMore) که عناصر اولیه آن‌ها مقداردهی نشده‌اند.
    • نمونه‌های پیش‌فرض ناشی از انواع مقداری (default(OneOrMore).

    پرهیز از استثناهای زمان اجرا (NullReferenceException):
    • اگر ساختار زیرین به شکل struct پیاده‌سازی شود، ایجاد ساختار با کلیدواژه default هیچ‌کدام از شاخه‌های معتبر (T یا IEnumerable) را مقداردهی نمی‌کند. شاخه null => [] تضمین می‌کند که اجرای متد تحت هیچ شرایطی با شکست مواجه نشده و همواره یک دنباله خالی اما معتبر بازگردانده شود.

    ۴. مقایسه فنی و مزایای معماری
    مشخصهرویکرد سنتی (Overload Explosion)رویکرد نوین (Unions with Bodies)
    تعداد تعاریف متدحداقل ۲ الی ۳ متد برای هر عملیاتیک متد متمرکز
    ایمنی نوع داده (Type Safety)تضمین‌شده، اما با افزونگی امضاتضمین‌شده در سطح زمان کامپایل
    امکان گسترش (Extensibility)نیاز به تغییر در چندین متد در صورت نیاز به لاجیک جدیدافزودن اعضای کمکی به بدنه Union بدون تغییر امضاها
    سربار نگهداریبالا (به‌ویژه با اضافه شدن متدهای جدید)بسیار پایین
    مدیریت مقدار خالی (Empty/Null)نیازمند اعتبارسنجی در تک‌تک متدهامدیریت متمرکز در الگوهای درونی نوع داده
    ۵. ملاحظات مهندسی و نکات تکمیلی
    برای بهینه‌سازی بیشتر این ساختار در محیط‌های Enterprise و کدهای حساس به کارایی، توجه به نکات زیر توصیه می‌شود:
    مدیریت تخصیص حافظه (Allocation & Performance):
    • در شاخه T single => [single]، استفاده از سینتکس Collection Expressions در صورت هدف‌گیری آرایه‌ها ممکن است برای مقادیر منفرد تخصیص حافظه کوچک ایجاد کند. در سناریوهای با کارایی بسیار بالا، پیاده‌سازی یک Enumerator سفارشی بر پایه struct برای پیمایش بدون تخصیص حافظه (Zero-allocation) گزینه‌ای ایده‌آل است.
    پوشش انواع مقداری تهی‌پذیر (Nullable Value Types):
    • چنانچه نوع پایه T یک ساختار (struct) باشد، تفاوت رفتار بین مقدار پیش‌فرض default(T) و null باید در منطق دامنه در نظر گرفته شود.

    ۶. نتیجه‌گیری (Conclusion)
    پشتیبانی از بدنه در انواع اجتماعی (Unions with Bodies) نقطه عطفی در تکامل سیستم انواع داده زبان #C است. این ویژگی فاصله میان مدل‌های جبری داده (Algebraic Data Types) و الگوهای شیء‌گرا را پر می‌کند. با به‌کارگیری ساختارهایی نظیر OneOrMore، توسعه‌دهندگان دات‌نت قادرند APIهایی صریح‌تر، خلاصه‌تر و مقاوم‌تر در برابر خطاهای زمان اجرا طراحی کنند و چالش‌های تاریخی نظیر انفجار اورلودها را برای همیشه برطرف سازند.