Union Type در C# 15 چیست؟ راهنمای Result، خطا و Pattern Matching کامل
راهنمای عملی Union Typeهای پیشنمایش C# 15؛ از مدلکردن نتیجه موفق یا خطا و Pattern Matching کامل تا مقایسه با object، inheritance و exception و تصمیم درباره آزمایش یا استفاده در Production.
فرض کنید متدی دارید که یا سفارش ثبتشده را برمیگرداند، یا یک خطای اعتبارسنجی، یا نتیجه «یافت نشد». بازگرداندن object قرارداد را مبهم میکند؛ پرتاب Exception برای هر نتیجه مورد انتظار نیز مسیر عادی کسبوکار را با شکست غیرمنتظره مخلوط میکند. Union Type در C# 15 برای بیان همین جمله طراحی شده است: «مقدار دقیقاً یکی از این چند نوع است.»
طبق راهنمای رسمی قابلیتهای C# 15، Union Typeها در نسخه Preview معرفی شدهاند و با SDK پیشنمایش .NET 11 قابل آزمایشاند. این نکته مهم است: هدف این مقاله یادگیری و ارزیابی است، نه توصیه بیقیدوشرط برای انتقال فوری کد Production به یک قابلیت Preview.
Union Type دقیقاً چه مسئلهای را حل میکند؟
یک Union مجموعه بستهای از case typeها تعریف میکند. هر مقدار Union یکی از همان caseها است و کامپایلر میداند چه حالتهایی مجازند. وقتی روی Union یک switch مینویسید، کامپایلر میتواند بررسی کند که همه caseها پوشش داده شدهاند. این ویژگی «Exhaustiveness» نام دارد.
public record OrderCreated(Guid Id);public record ValidationFailed(string Message);public record CustomerNotFound(Guid CustomerId); public union CreateOrderResult( OrderCreated, ValidationFailed, CustomerNotFound);اکنون امضای متد اطلاعات بیشتری از Task<object> منتقل میکند. مصرفکننده میداند فقط سه خروجی معتبر وجود دارد و افزودن case تازه میتواند محلهای ناقص Pattern Matching را آشکار کند.
CreateOrderResult result = await CreateOrderAsync(request); string message = result switch{ OrderCreated created => $"سفارش {created.Id} ثبت شد", ValidationFailed failed => failed.Message, CustomerNotFound missing => $"مشتری {missing.CustomerId} پیدا نشد",};مرجع رسمی 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های اعلامشده با Pattern Matching کامل.
Union با interface و inheritance چه تفاوتی دارد؟
interface و base class برای اشتراک رفتار و Polymorphism عالیاند. اگر همه پرداختها متد AuthorizeAsync دارند، interface قرارداد طبیعی است. اما یک interface معمولی مجموعه بستهای از پیادهسازیها نیست؛ ممکن است Assembly دیگری type تازهای بسازد و compiler نتواند فرض کند Switch شما همه حالتهای آینده را پوشش میدهد.
Union زمانی مناسبتر است که caseها لزوماً نسبت ارثبری ندارند و میخواهید مجموعه خروجی بسته باشد. C# 15 همچنین Closed Hierarchy را معرفی میکند که برای خانواده typeهای مرتبط از طریق inheritance طراحی شده است. در معرفی رسمی قابلیتهای C# 15 در .NET Blog این دو ایده کنار هم توضیح داده شدهاند: Union برای ترکیب typeها و Closed Hierarchy برای محدودکردن سلسلهمراتب.
قاعده انتخاب سریع
- اگر رفتار مشترک هسته طراحی است، interface یا base class را بررسی کنید.
- اگر مقدار «یکی از چند خروجی محدود» است، Union گزینه طبیعیتری است.
- اگر caseها یک سلسلهمراتب دامنه میسازند، Closed Hierarchy را مطالعه کنید.
- اگر فقط دو مقدار ساده را برمیگردانید و حالت نامعتبر ندارید، Tuple هنوز میتواند کافی باشد.
مثال عملی Result یا Error در یک API
فرض کنید Endpoint ثبتنام دوره باید میان موفقیت، ظرفیت تکمیل و ورودی نامعتبر تفاوت بگذارد. اینها خطاهای غیرمنتظره نیستند؛ بخشی از قرارداد کسبوکارند.
public record EnrollmentCreated(Guid EnrollmentId);public record CourseCapacityReached(Guid CourseId);public record InvalidEnrollment(string Field, string Reason); public union EnrollmentResult( EnrollmentCreated, CourseCapacityReached, InvalidEnrollment);Application Service میتواند هر case را مستقیم برگرداند و لایه HTTP آن را به Status Code مناسب نگاشت کند.
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] }),};مزیت اصلی فقط کوتاهشدن کد نیست. اگر بعدها case «نیازمند تأیید پرداخت» اضافه شود، محلهای Pattern Matching باید بهروزرسانی شوند. این فشار compiler کمک میکند قرارداد جدید بیصدا از لایهها عبور نکند.
تست هر case را جدا بنویسید
Exhaustive Switch اثبات نمیکند منطق تولید case درست است. برای هر خروجی تست مستقل لازم است: ورودی معتبر به EnrollmentCreated برسد، ظرفیت صفر به CourseCapacityReached و داده ناقص به InvalidEnrollment. سپس تست Endpoint بررسی کند که هر case به پاسخ HTTP مورد انتظار نگاشت میشود.
Union چه زمانی جای Exception را نمیگیرد؟
Exception برای وضعیتهایی مناسب است که مسیر عادی نمیتواند ادامه یابد یا Call Site انتظار مدیریت محلی ندارد: قطع ناگهانی پایگاهداده، نقض invariant داخلی، خرابی فایل یا Timeout زیرساخت. در مقابل، موجودنبودن محصول یا رد اعتبارسنجی معمولاً نتیجه قابل انتظار دامنه است.
| وضعیت | Union/Result | Exception |
|---|---|---|
| ورودی نامعتبر | معمولاً مناسب | فقط در مرزهای خاص |
| رکورد پیدا نشد | اغلب مناسب | وقتی واقعاً invariant شکسته |
| ظرفیت تکمیل | مناسب؛ نتیجه کسبوکار | نامناسب برای کنترل جریان |
| قطع SQL | نباید جزئیات زیرساخت را عادی کند | مناسب و قابل Log/Retry |
| Cancellation | طبق قرارداد async | OperationCanceledException طبیعی است |
مرز را بر اساس «مورد انتظار بودن» و «امکان واکنش محلی» انتخاب کنید. تبدیل همه Exceptionها به caseهای Union میتواند شکست زیرساخت را پنهان کند و Observability را ضعیف سازد.
Nullability و حالت Null را جدی بگیرید
Union Type به معنی حذف خودکار Null نیست. مستندات رسمی درباره Nullability توضیح میدهند که محتوای Union میتواند نیازمند مدیریت Null باشد. قرارداد متد باید روشن کند آیا Null اصلاً مجاز است. اگر «عدم وجود» یک وضعیت دامنه است، case نامدار مثل NotFound از Null مبهم خواناتر است.
public record FoundCustomer(Customer Value);public record CustomerMissing(Guid Id); public union FindCustomerResult( FoundCustomer, CustomerMissing);این مدل به مصرفکننده میگوید نبود مشتری یک خروجی تعریفشده است. در مقابل، 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 مشکل را نشان دهد؛ پیچیدگی زودهنگام ارزش قرارداد خوانا را کم میکند.
- Baseline را با BenchmarkDotNet یا ابزار مناسب ثبت کنید.
- Allocation و Throughput را در داده شبیه Production بسنجید.
- نسخه ساده Union را با Result type فعلی مقایسه کنید.
- فقط در Hot Path به custom storage فکر کنید.
چطور Union Type را در پروژه آزمایش کنیم؟
C# 15 در زمان نگارش Preview است. برای آزمایش طبق مستندات باید SDK پیشنمایش .NET 11 را نصب و Language Version را روی Preview قرار دهید.
net11.0 preview این تنظیم را در Branch یا نمونه جداگانه انجام دهید. اگر تیم تازه مسیر برنامهنویسی را شروع کرده، ابتدا تسلط بر Type، Method، Exception، Generic و Pattern Matching ارزش بیشتری از تعقیب هر Syntax تازه دارد؛ راهنمای نقشه راه برنامهنویسی از صفر تا پروژه ترتیب مهارتها را توضیح میدهد.
Sandbox قابل تکرار بسازید
SDK Preview را روی سیستم اصلی همه اعضای تیم تحمیل نکنید. فایل global.json، CI جدا و در صورت نیاز Container آزمایشی بسازید تا نسخه compiler ثابت باشد. برای آشنایی با تکرارپذیری محیط، مقاله Docker چیست و چرا توسعهدهنده باید Container را یاد بگیرد مفید است.
مقایسه کوتاه با Union در TypeScript
توسعهدهنده Front-End ممکن است Discriminated Union در TypeScript را بشناسد. TypeScript معمولاً Union را با ترکیب typeها و یک property تمایزبخش مدل میکند؛ C# 15 typeهای Runtime و Pattern Matching زبان 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ها نتیجه مورد انتظارند یا شکست غیرمنتظره زیرساخت؟
- آیا Pattern Matching در همه لایههای مصرفکننده قابل نگهداری است؟
- آیا Null به case نامدار تبدیل شده یا قرارداد Nullability روشن است؟
- آیا value type پرتکرار و ریسک Boxing وجود دارد؟
- آیا قابلیت Preview در Branch، CI و SDK قفلشده آزمایش میشود؟
- آیا راه بازگشت به Result type فعلی وجود دارد؟
- آیا تصمیم Production تا پایدارشدن نسخه و سیاست تیم مستند شده است؟
جمعبندی
Union Type در C# 15 راهی صریح برای مدلکردن مقدارهایی است که یکی از چند case محدود هستند. تبدیل ضمنی caseها، Pattern Matching کامل و فشار compiler برای مدیریت حالت تازه، بهخصوص در Result/Error و پیامهای دامنه ارزشمند است. در عین حال Preview بودن قابلیت، Nullability، Boxing و مرز Exception باید آگاهانه بررسی شوند.
بهترین شروع، یک نمونه کوچک خارج از Production است: یک Result واقعی را مدل کنید، تست همه caseها را بنویسید و خوانایی Call Site را با راهحل فعلی مقایسه کنید. اگر قرارداد روشنتر شد و هزینه قابل قبول بود، تجربه مستندشده برای تصمیم نسخه پایدار خواهید داشت.
اگر میخواهید C# و طراحی API را با پروژه واقعی، تست و تصمیمهای معماری یاد بگیرید، مسیر آموزشی خانه برنامهنویسان البرز را بررسی کنید.