عنوان:

‫نوع‌های اتحادیه (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 ایجاد کرده است.