عنوان:

‫بررسی ویژگی OverloadResolutionPriority در C# 13 و دات‌نت 9


نویسنده: وحید نصیری
تاریخ: ۱۴۰۳/۰۹/۰۲ ۲۱:۱۰
آدرس: www.dntips.ir
دات‌نت 9 به همراه ویژگی جدیدی به نام [OverloadResolutionPriority] است که ... بیشتر توسط نگهدارندگان کتابخانه‌ها در موارد خاصی استفاده خواهد شد و کاربرد عمومی ... ندارد. برای نمونه مثال زیر را درنظر بگیرید:
static class Sample
{
    public static void Test() => Console.WriteLine("Test 1");
}
مدتی بعد، نگه‌دارنده‌ی این کتابخانه تصمیم می‌گیرد تا امضای این متد Test را با یک پارامتر اختیاری (optional)، به‌روز رسانی کرده و تغییر دهد:
static class Sample
{
    public static void Test([CallerMemberName]string name = "") => Console.WriteLine("Test");
}
مشکلی که این به‌روز رسانی ایجاد می‌کند،‌ اصطلاحا یک «Binary breaking change» نامیده می‌شود؛ چون متد قبلی بدون پارامتر Test، دیگر وجود خارجی ندارد.
راه‌حل بهتر مواجه شدن با این مساله، افزودن یک overload جدید است:
static class Sample
{
    public static void Test() => Console.WriteLine("Test 1");

    public static void Test([CallerMemberName]string name = "")
        => Console.WriteLine("Test 2");
}
در این حالت اگر متد ()Sample.Test را فراخوانی و اجرا کنیم، خروجی آن، Test 1 است و متد اضافه شده‌ی جدید، اصلا فراخوانی نمی‌شود! دلیل آن هم این‌است که با این روش فراخوانی بدون پارامتر، کامپایلر همیشه overload بدون پارامتر را بر نمونه‌های دارای پارامتر، ترجیح می‌دهد تا باخواست فراخوان، انطباق بیشتری پیدا کند.
بنابراین اگر قصد داریم که واقعا با فراخوانی متد ()Sample.Test، خروجی جدید Test 2 ظاهر شود، باید متد قبلی Test را حذف کنیم که به‌علت بروز یک «Binary breaking change»، مطلوب توسعه‌ی کتابخانه‌های عمومی نیست.


معرفی ویژگی جدید OverloadResolutionPriority در C# 13 و دات‌نت 9

با استفاده از ویژگی جدید OverloadResolutionPriority، می‌توان حق تقدم فراخوانی oveloadها را توسط کامپایلر، سفارشی سازی کرد. در این حالت، کامپایلر با overload ای شروع می‌کند که بیشترین حق تقدم آن، توسط ما مشخص شده‌است. در اینجا عدد بیشتر، یعنی حق تقدم بیشتر و اگر متد overload ای به همراه OverloadResolutionPriority نباشد، دارای عدد حق تقدم «صفر» است:
static class Sample
{
    [OverloadResolutionPriority(-1)]
    public static void Test() => Console.WriteLine("Test 1");

    public static void Test([CallerMemberName]string name = "") => Console.WriteLine("Test 2");
}
با این تغییرات، خروجی فراخوانی متد ()Sample.Test، مساوی Test 2 است.

مثال دیگری از این دست، باز هم با افزودن یک overload ای جدید، با پارامتری دارای مقدار پیش‌فرض رخ می‌دهد که آن‌را اختیاری هم می‌کند و حق تقدم فراخوانی آن‌ها به نحو زیر قابل سفارشی سازی است:
static class Demo
{
    [OverloadResolutionPriority(2)]
    public static void DisplayInvoice(string invoice )
    {
    }

    [OverloadResolutionPriority(-1)]
    public static void DisplayInvoice(string invoice, int count = 3)
    {
    }
}


سفارشی سازی حق تقدم فراخوانی overloadهایی که دارای implicit conversion هستند

یک کارکرد دیگر این ویژگی، مشخص کردن حق تقدم فراخوانی overload ای است که دارای یک «تبدیل ضمنی» است:
static class Sample
{
    public static void Test(int a) => Console.WriteLine("int");

    [OverloadResolutionPriority(1)]
    public static void Test(byte  a) => Console.WriteLine("byte");
}
برای مثال اگر این متد Test جدید را با عدد 1 فراخوانی کنیم، عدد یک، هم در بازه‌ی byte قرار دارد و هم در بازه‌ی int. با استفاده از OverloadResolutionPriority تعریف شده، متد دارای پارامتر از نوع byte، در مواردی که هم پوشانی نوعی وجود دارد و این فراخوانی «ambiguous» و گنگ است، حق تقدم بیشتری پیدا می‌کند:
Sample.Test(1);    // Print: byte
Sample.Test(1000); // Print: int

یا نمونه‌ی دیگر آن،‌ تغییر حق تقدم فراخوانی overload دارای نوع <ReadOnlySpan<char، بر overload تعریف شده‌ی با نوع string‌ است:
public class MyService
{
    [OverloadResolutionPriority(1)]
    public void Display(string chars) => Console.WriteLine(chars);

    [OverloadResolutionPriority(2)]
    public void Display(ReadOnlySpan<char> chars) => Console.WriteLine(chars.ToArray());
}


امکان مشخص کردن حق تقدم فراخوانی overloadهای Params Collections در C# 13

اگر خاطرتان باشد، در مطلب «بررسی ویژگی Params Collections در C# 13» عنوان شد که اگر چندین overload از نوع params را تعریف کردیم «... اگر فراخوانی آن متد به صورت ذکر آرگومان‌های جدا شده‌ی با کاما باشد، کامپایلر راسا بهترین انتخاب را انجام خواهد داد که در اینجا همان متد دارای آرگومان params ReadOnlySpan<string> names است ...». این مورد «انتخاب توسط کامپایلر» را هم می‌توان با استفاده از ویژگی OverloadResolutionPriority، سفارشی سازی کرد و تغییر داد:
[OverloadResolutionPriority(1)]
void Test(params Span<int> values) 
{
    // ....
}

void Test(params ReadOnlySpan<int> values)
{
    // ....
}


محدودیت ویژگی OverloadResolutionPriority در C# 13.0

اگر overloadهای تعریف شده در کلاس‌های مختلفی باشند (مانند نحوه‌ی تعریف متدهای الحاقی/extension methods)، حق تقدم‌های تعریف شده، ندید گرفته می‌شوند و این روش فقط برای متدهای موجود در یک کلاس، کاربرد دارد.