وبلاگ / راهنمای مسیر یادگیری و فناوری
راهنمای مسیر یادگیری و فناوری

Union Type در C# 15 چیست؟ راهنمای Result، خطا و Pattern Matching کامل

راهنمای عملی Union Typeهای پیش‌نمایش C# 15؛ از مدل‌کردن نتیجه موفق یا خطا و Pattern Matching کامل تا مقایسه با object، inheritance و exception و تصمیم درباره آزمایش یا استفاده در Production.

· ۱۷ بازدید

فرض کنید متدی دارید که یا سفارش ثبت‌شده را برمی‌گرداند، یا یک خطای اعتبارسنجی، یا نتیجه «یافت نشد». بازگرداندن object قرارداد را مبهم می‌کند؛ پرتاب Exception برای هر نتیجه مورد انتظار نیز مسیر عادی کسب‌وکار را با شکست غیرمنتظره مخلوط می‌کند. در برای بیان همین جمله طراحی شده است: «مقدار دقیقاً یکی از این چند نوع است.»

طبق راهنمای رسمی قابلیت‌های C# 15، Union Typeها در نسخه Preview معرفی شده‌اند و با SDK پیش‌نمایش قابل آزمایش‌اند. این نکته مهم است: هدف این مقاله یادگیری و ارزیابی است، نه توصیه بی‌قیدوشرط برای انتقال فوری کد Production به یک قابلیت Preview.

دقیقاً چه مسئله‌ای را حل می‌کند؟

یک Union مجموعه بسته‌ای از case typeها تعریف می‌کند. هر مقدار Union یکی از همان caseها است و کامپایلر می‌داند چه حالت‌هایی مجازند. وقتی روی Union یک switch می‌نویسید، کامپایلر می‌تواند بررسی کند که همه caseها پوشش داده شده‌اند. این ویژگی «Exhaustiveness» نام دارد.

code.txtText
public record OrderCreated(Guid Id);public record ValidationFailed(string Message);public record CustomerNotFound(Guid CustomerId); public union CreateOrderResult(    OrderCreated,    ValidationFailed,    CustomerNotFound);
UTF-8Text

اکنون امضای متد اطلاعات بیشتری از Task<object> منتقل می‌کند. مصرف‌کننده می‌داند فقط سه خروجی معتبر وجود دارد و افزودن case تازه می‌تواند محل‌های ناقص را آشکار کند.

code.txtText
CreateOrderResult result = await CreateOrderAsync(request); string message = result switch{    OrderCreated created => $"سفارش {created.Id} ثبت شد",    ValidationFailed failed => failed.Message,    CustomerNotFound missing => $"مشتری {missing.CustomerId} پیدا نشد",};
UTF-8Text

مرجع رسمی Union Type در Microsoft Learn توضیح می‌دهد که caseها تبدیل ضمنی به Union دارند و compiler کامل‌بودن switch را کنترل می‌کند.

چرا object یا Tuple جایگزین مناسبی نیست؟

object فقط می‌گوید «هر چیزی ممکن است». Call Site باید با حدس، Cast و شاخه پیش‌فرض از خود دفاع کند. اگر نوع تازه‌ای برگردد، قرارداد در سطح کامپایلر شکسته نمی‌شود و خطا ممکن است در Runtime دیده شود.

Tuple می‌تواند چند مقدار را هم‌زمان حمل کند، اما برای «یکی از چند حالت» طراحی نشده است. الگویی مثل (Order? order, string? error) حالت‌های نامعتبر می‌سازد: هر دو Null، هر دو مقدار‌دار یا Error بدون نوع مشخص. Union حالت‌های مجاز را محدود می‌کند و فضای وضعیت نامعتبر را کوچک‌تر می‌سازد.

  • object: انعطاف زیاد، قرارداد ضعیف و Cast دفاعی.
  • Tuple: مناسب چند خروجی هم‌زمان، نه حالت‌های mutually exclusive.
  • Union: دقیقاً یکی از caseهای اعلام‌شده با کامل.

Union با interface و inheritance چه تفاوتی دارد؟

interface و base class برای اشتراک رفتار و Polymorphism عالی‌اند. اگر همه پرداخت‌ها متد AuthorizeAsync دارند، interface قرارداد طبیعی است. اما یک interface معمولی مجموعه بسته‌ای از پیاده‌سازی‌ها نیست؛ ممکن است Assembly دیگری type تازه‌ای بسازد و compiler نتواند فرض کند Switch شما همه حالت‌های آینده را پوشش می‌دهد.

Union زمانی مناسب‌تر است که caseها لزوماً نسبت ارث‌بری ندارند و می‌خواهید مجموعه خروجی بسته باشد. همچنین Closed Hierarchy را معرفی می‌کند که برای خانواده typeهای مرتبط از طریق inheritance طراحی شده است. در معرفی رسمی قابلیت‌های C# 15 در .NET Blog این دو ایده کنار هم توضیح داده شده‌اند: Union برای ترکیب typeها و Closed Hierarchy برای محدودکردن سلسله‌مراتب.

قاعده انتخاب سریع

  • اگر رفتار مشترک هسته طراحی است، interface یا base class را بررسی کنید.
  • اگر مقدار «یکی از چند خروجی محدود» است، Union گزینه طبیعی‌تری است.
  • اگر caseها یک سلسله‌مراتب دامنه می‌سازند، Closed Hierarchy را مطالعه کنید.
  • اگر فقط دو مقدار ساده را برمی‌گردانید و حالت نامعتبر ندارید، Tuple هنوز می‌تواند کافی باشد.

مثال عملی Result یا Error در یک API

فرض کنید Endpoint ثبت‌نام دوره باید میان موفقیت، ظرفیت تکمیل و ورودی نامعتبر تفاوت بگذارد. این‌ها خطاهای غیرمنتظره نیستند؛ بخشی از قرارداد کسب‌وکارند.

code.txtText
public record EnrollmentCreated(Guid EnrollmentId);public record CourseCapacityReached(Guid CourseId);public record InvalidEnrollment(string Field, string Reason); public union EnrollmentResult(    EnrollmentCreated,    CourseCapacityReached,    InvalidEnrollment);
UTF-8Text

Application Service می‌تواند هر case را مستقیم برگرداند و لایه HTTP آن را به Status Code مناسب نگاشت کند.

code.txtText
return result switch{    EnrollmentCreated value => Results.Created(        $"/enrollments/{value.EnrollmentId}", value),    CourseCapacityReached value => Results.Conflict(value),    InvalidEnrollment value => Results.ValidationProblem(        new Dictionary        {            [value.Field] = [value.Reason]        }),};
UTF-8Text

مزیت اصلی فقط کوتاه‌شدن کد نیست. اگر بعدها case «نیازمند تأیید پرداخت» اضافه شود، محل‌های باید به‌روزرسانی شوند. این فشار compiler کمک می‌کند قرارداد جدید بی‌صدا از لایه‌ها عبور نکند.

تست هر case را جدا بنویسید

Exhaustive Switch اثبات نمی‌کند منطق تولید case درست است. برای هر خروجی تست مستقل لازم است: ورودی معتبر به EnrollmentCreated برسد، ظرفیت صفر به CourseCapacityReached و داده ناقص به InvalidEnrollment. سپس تست Endpoint بررسی کند که هر case به پاسخ HTTP مورد انتظار نگاشت می‌شود.

Union چه زمانی جای Exception را نمی‌گیرد؟

Exception برای وضعیت‌هایی مناسب است که مسیر عادی نمی‌تواند ادامه یابد یا Call Site انتظار مدیریت محلی ندارد: قطع ناگهانی پایگاه‌داده، نقض invariant داخلی، خرابی فایل یا Timeout زیرساخت. در مقابل، موجودنبودن محصول یا رد اعتبارسنجی معمولاً نتیجه قابل انتظار دامنه است.

وضعیتUnion/ResultException
ورودی نامعتبرمعمولاً مناسبفقط در مرزهای خاص
رکورد پیدا نشداغلب مناسبوقتی واقعاً invariant شکسته
ظرفیت تکمیلمناسب؛ نتیجه کسب‌وکارنامناسب برای کنترل جریان
قطع SQLنباید جزئیات زیرساخت را عادی کندمناسب و قابل Log/Retry
Cancellationطبق قرارداد asyncOperationCanceledException طبیعی است

مرز را بر اساس «مورد انتظار بودن» و «امکان واکنش محلی» انتخاب کنید. تبدیل همه Exceptionها به caseهای Union می‌تواند شکست زیرساخت را پنهان کند و Observability را ضعیف سازد.

Nullability و حالت Null را جدی بگیرید

به معنی حذف خودکار Null نیست. مستندات رسمی درباره Nullability توضیح می‌دهند که محتوای Union می‌تواند نیازمند مدیریت Null باشد. قرارداد متد باید روشن کند آیا Null اصلاً مجاز است. اگر «عدم وجود» یک وضعیت دامنه است، case نام‌دار مثل NotFound از Null مبهم خواناتر است.

code.txtText
public record FoundCustomer(Customer Value);public record CustomerMissing(Guid Id); public union FindCustomerResult(    FoundCustomer,    CustomerMissing);
UTF-8Text

این مدل به مصرف‌کننده می‌گوید نبود مشتری یک خروجی تعریف‌شده است. در مقابل، Customer? ممکن است مشخص نکند Null به دلیل نبود داده است یا شکست نگاشت.

هزینه Runtime و Boxing را اندازه بگیرید

طبق مرجع Microsoft Learn، Union تولیدشده یک struct است که مقدار case را در property از نوع object? نگه می‌دارد. اگر case یک value type مثل int باشد، Boxing می‌تواند رخ دهد. برای بیشتر جریان‌های API این هزینه ممکن است تعیین‌کننده نباشد، اما در حلقه پرتکرار، پردازش Telemetry یا مسیر کم‌تاخیر باید Benchmark بگیرید.

Tutorial رسمی Union Type علاوه بر نمونه generic، الگوی custom union بدون Boxing را نیز پوشش می‌دهد. سراغ پیاده‌سازی سفارشی فقط وقتی بروید که Measurement مشکل را نشان دهد؛ پیچیدگی زودهنگام ارزش قرارداد خوانا را کم می‌کند.

  1. Baseline را با BenchmarkDotNet یا ابزار مناسب ثبت کنید.
  2. Allocation و Throughput را در داده شبیه Production بسنجید.
  3. نسخه ساده Union را با Result type فعلی مقایسه کنید.
  4. فقط در Hot Path به custom storage فکر کنید.

چطور را در پروژه آزمایش کنیم؟

در زمان نگارش Preview است. برای آزمایش طبق مستندات باید SDK پیش‌نمایش را نصب و Language Version را روی Preview قرار دهید.

code.txtText
   net11.0  preview 
UTF-8Text

این تنظیم را در Branch یا نمونه جداگانه انجام دهید. اگر تیم تازه مسیر برنامه‌نویسی را شروع کرده، ابتدا تسلط بر Type، Method، Exception، Generic و ارزش بیشتری از تعقیب هر Syntax تازه دارد؛ راهنمای نقشه راه برنامه‌نویسی از صفر تا پروژه ترتیب مهارت‌ها را توضیح می‌دهد.

Sandbox قابل تکرار بسازید

SDK Preview را روی سیستم اصلی همه اعضای تیم تحمیل نکنید. فایل global.json، CI جدا و در صورت نیاز Container آزمایشی بسازید تا نسخه compiler ثابت باشد. برای آشنایی با تکرارپذیری محیط، مقاله Docker چیست و چرا توسعه‌دهنده باید Container را یاد بگیرد مفید است.

مقایسه کوتاه با Union در TypeScript

توسعه‌دهنده Front-End ممکن است Discriminated Union در TypeScript را بشناسد. TypeScript معمولاً Union را با ترکیب typeها و یک property تمایزبخش مدل می‌کند؛ typeهای Runtime و زبان C# را در اختیار دارد. شباهت مفهومی مهم است، اما Syntax و رفتار Runtime یکسان نیستند.

اگر هنوز بین اکوسیستم‌ها تصمیم می‌گیرید، مقاله TypeScript یا JavaScript زمینه type safety در Front-End را توضیح می‌دهد. هدف Union در هر دو فضا شفاف‌کردن حالت‌های مجاز است، ولی طراحی قرارداد باید با ابزار و Runtime همان زبان انجام شود.

چه زمانی از Union استفاده نکنیم؟

  • وقتی caseها رفتار مشترک غنی دارند و Polymorphism طراحی را ساده‌تر می‌کند.
  • وقتی مجموعه typeها عمداً باز است و افزونه‌ها باید type تازه اضافه کنند.
  • وقتی کتابخانه عمومی باید با compiler و Runtime پایدار فعلی سازگار بماند.
  • وقتی یک Boolean یا enum ساده تمام حالت‌ها را بدون داده اضافی بیان می‌کند.
  • وقتی تیم بدون Benchmark برای هر مقدار ساده custom union پیچیده می‌سازد.

Union ابزار مدل‌سازی است، نه هدف معماری. اگر استفاده از آن قرارداد را طولانی‌تر و فهم را سخت‌تر می‌کند، نوع ساده‌تر انتخاب بهتری است.

چک‌لیست ارزیابی برای تیم

  • آیا مقدار واقعاً «دقیقاً یکی از چند حالت بسته» است؟
  • آیا caseها نتیجه مورد انتظارند یا شکست غیرمنتظره زیرساخت؟
  • آیا در همه لایه‌های مصرف‌کننده قابل نگهداری است؟
  • آیا Null به case نام‌دار تبدیل شده یا قرارداد Nullability روشن است؟
  • آیا value type پرتکرار و ریسک Boxing وجود دارد؟
  • آیا قابلیت Preview در Branch، CI و SDK قفل‌شده آزمایش می‌شود؟
  • آیا راه بازگشت به Result type فعلی وجود دارد؟
  • آیا تصمیم Production تا پایدارشدن نسخه و سیاست تیم مستند شده است؟

جمع‌بندی

در راهی صریح برای مدل‌کردن مقدارهایی است که یکی از چند case محدود هستند. تبدیل ضمنی caseها، کامل و فشار compiler برای مدیریت حالت تازه، به‌خصوص در Result/Error و پیام‌های دامنه ارزشمند است. در عین حال Preview بودن قابلیت، Nullability، Boxing و مرز Exception باید آگاهانه بررسی شوند.

بهترین شروع، یک نمونه کوچک خارج از Production است: یک Result واقعی را مدل کنید، تست همه caseها را بنویسید و خوانایی Call Site را با راه‌حل فعلی مقایسه کنید. اگر قرارداد روشن‌تر شد و هزینه قابل قبول بود، تجربه مستندشده برای تصمیم نسخه پایدار خواهید داشت.

اگر می‌خواهید C# و را با پروژه واقعی، تست و تصمیم‌های معماری یاد بگیرید، مسیر آموزشی خانه برنامه‌نویسان البرز را بررسی کنید.