عنوان:

‫بررسی عمیق اعضای الحاقی در C# 14


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۲/۱۹ ۰۷:۵۵
آدرس: www.dntips.ir
زبان برنامه‌نویسی سی‌شارپ از زمان پیدایش خود، همواره در حال تکامل و افزودن ویژگی‌های جدید برای تسهیل و بهبود فرآیند توسعه نرم‌افزار بوده است. یکی از قابلیت‌های قدرتمند و پرکاربرد این زبان، امکان تعریف متدهای الحاقی (Extension Methods) بوده که به برنامه‌نویسان اجازه می‌دهد تا متدهای جدیدی را به انواع موجود اضافه کنند، بدون آنکه نیازی به تغییر در تعریف اصلی آن نوع باشد. در نسخه 14 سی‌شارپ، این مفهوم با معرفی اعضای الحاقی (Extension Members) گسترش یافته است. این ویژگی جدید، علاوه بر متدهای الحاقی سنتی، امکان تعریف خصوصیات الحاقی (Extension Properties) را نیز فراهم می‌کند و یک نحو (Syntax) جایگزین و منسجم‌تر برای تعریف متدهای الحاقی ارائه می‌دهد. این مقاله به بررسی دقیق مفهوم اعضای الحاقی در سی‌شارپ 14، مزایا، چالش‌های طراحی و نحوه استفاده از آن‌ها می‌پردازد و نشان می‌دهد که چگونه این قابلیت جدید می‌تواند به افزایش خوانایی، سازماندهی و قابلیت نگهداری کد کمک کند.

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

نحو سنتی (This-parameter Extension Methods):
در این روش که از نسخه‌های قبلی سی‌شارپ وجود داشته است، یک متد استاتیک (Static Method) در یک کلاس استاتیک تعریف می‌شود. اولین پارامتر این متد با کلمه کلیدی this پیشوند می‌گیرد و نوع این پارامتر، نوعی است که متد الحاقی به آن اضافه می‌شود. برای مثال، متد ValuesLessThan که عناصر کوچکتر از یک مقدار مشخص را از یک دنباله (Sequence) از اعداد صحیح برمی‌گرداند، به صورت زیر تعریف می‌شود:
public static class MyExtensions
{
    public static IEnumerable<int> ValuesLessThan(this IEnumerable<int> source, int threshold)
            => source.Where(x => x < threshold);
}
در این مثال، this IEnumerable<int> source نشان می‌دهد که این متد الحاقی به نوع IEnumerable<int> اضافه شده است و به عنوان پارامتر source در بدنه متد قابل دسترسی است.

نحو جدید (Extension Members):
سی‌شارپ 14 یک نحو جدید و اختیاری را برای تعریف اعضای الحاقی معرفی می‌کند. در این نحو، از یک بلاک extension برای تعریف اعضایی که به یک نوع خاص تعلق دارند، استفاده می‌شود. نوع گیرنده (Receiver) در داخل پرانتز بعد از کلمه کلیدی extension مشخص می‌شود. متد ValuesGreaterThan که عناصر بزرگتر از یک مقدار مشخص را برمی‌گرداند و خصوصیت ValuesGreaterThanZero که عناصر بزرگتر از صفر را برمی‌گرداند، با استفاده از این نحو به صورت زیر تعریف می‌شوند:
public static class MyExtensions
{
    extension(IEnumerable<int> source)
    {
        public IEnumerable<int> ValuesGreaterThan(int threshold)
            => source.Where(x => x > threshold);

        public IEnumerable<int> ValuesGreaterThanZero
            => source.ValuesGreaterThan(0);
    }
}
همانطور که مشاهده می‌شود، در این نحو، نوع گیرنده (IEnumerable<int> source) یک بار در بلاک extension تعریف شده و سپس متدها و خصوصیات الحاقی برای این نوع بدون نیاز به تکرار کلمه کلیدی this تعریف می‌شوند. این امر می‌تواند به خوانایی و سازماندهی بهتر کد کمک کند، به ویژه زمانی که چندین عضو الحاقی برای یک نوع خاص وجود داشته باشد.

چالش‌های طراحی اعضای الحاقی
تیم طراحی سی‌شارپ در طول فرآیند توسعه اعضای الحاقی، با چالش‌های متعددی روبرو بوده است. یکی از مهم‌ترین این چالش‌ها، حفظ سازگاری با اکوسیستم گسترده متدهای الحاقی موجود بود. از آنجایی که متدهای الحاقی به طور گسترده در پروژه‌های .NET استفاده می‌شوند، هرگونه تغییر در نحو یا رفتار آن‌ها باید به گونه‌ای باشد که کمترین تأثیر را بر کدهای موجود داشته باشد و برای توسعه‌دهندگانی که از آن‌ها استفاده می‌کنند، طبیعی و بدون ابهام به نظر برسد.
چالش دیگر، نحوه تعریف گیرنده (Receiver) برای انواع مختلف اعضا بود. در متدهای الحاقی سنتی، گیرنده به عنوان اولین پارامتر با پیشوند this مشخص می‌شد. اما خصوصیات و سایر انواع اعضا پارامتر ندارند. نحو جدید اعضای الحاقی با جدا کردن تعریف گیرنده در بلاک extension، این مشکل را حل کرده است و یک مکان مشخص برای تعریف نوعی که اعضا به آن اضافه می‌شوند، فراهم می‌کند.
نحوه رفع ابهام (Disambiguation) در صورت وجود چندین عضو الحاقی با نام و امضای مشابه نیز یکی دیگر از چالش‌های مهم بود. رویکرد فعلی برای رفع ابهام در متدهای الحاقی سنتی، استفاده از نام کلاس استاتیک حاوی متد است. تیم طراحی سی‌شارپ تلاش کرده است تا این رویکرد را در اعضای الحاقی جدید نیز حفظ کند تا توسعه‌دهندگان با یک مکانیسم آشنا برای رفع ابهام روبرو باشند.

گیرنده‌ها (Receivers)
هنگامیکه یک متد الحاقی فراخوانی می‌شود، کامپایلر سی‌شارپ فراخوانی را به یک متد استاتیک با گیرنده به عنوان اولین پارامتر تبدیل می‌کند که به این فرآیند پایین‌آوردن (Lowering) گفته می‌شود. برای مثال، قطعه کد زیر:
int[] list = [ 1, 3, 5, 7, 9 ];
var x1 = list.ValuesLessThan(3);
var x2 = MyExtensions.ValuesLessThan(list, 3);
در اینجا، فراخوانی list.ValuesLessThan(3) توسط کامپایلر به MyExtensions.ValuesLessThan(list, 3) تبدیل می‌شود.
در تعریف یک متد الحاقی سنتی، گیرنده اولین پارامتر است و با this پیشوند می‌گیرد. در نحو جدید اعضای الحاقی، گیرنده در بلاک extension تعریف می‌شود:
public static class MyExtensions
{
    extension(IEnumerable<int> source)
    {
        public IEnumerable<int> ValuesLessThan(int threshold) ...
    }
}
کامپایلر این نحو جدید را نیز به یک متد استاتیک مشابه با نحو سنتی پایین می‌آورد. این امر تضمین می‌کند که متدهای الحاقی تعریف شده با هر دو نحو، در سطح کد منبع و کد باینری به یک شکل عمل می‌کنند.
خصوصیات الحاقی نیز به متدهای get و set پایین آورده می‌شوند. برای مثال، خصوصیت الحاقی ValuesGreaterThanZero:
public static class MyExtensions
{
   extension(IEnumerable<int> source)
    {
        public IEnumerable<int> ValuesGreaterThanZero
        {
             get
             { ... }
        }
    }
}
به یک متد get استاتیک با نوع گیرنده به عنوان تنها پارامتر پایین آورده می‌شود:
public static class MyExtensions
{
    public static IEnumerable<int> get_ValuesGreaterThanZero(this IEnumerable<int> source) ...
}
در هر دو حالت متد و خصوصیت، نوع، نام، پارامترهای نوع عمومی (Generic Type Parameters)، محدودیت‌های نوع پارامتر (Type Parameter Constraints)، صفات (Attributes) و اصلاح‌کننده‌ها (Modifiers) از بلاک extension به پارامتر گیرنده کپی می‌شوند.

رفع ابهام (Disambiguation)
به طور کلی، اعضای الحاقی مانند سایر اعضای نوع گیرنده، از طریق آن قابل دسترسی هستند (مانند list.ValuesGreaterThan(3)). با این حال، در موارد نادر، ممکن است ابهام به وجود آید اگر چندین عضو الحاقی با امضای معتبر برای یک نوع وجود داشته باشد. برای رفع ابهام در متدهای الحاقی سنتی، می‌توان متد استاتیک الحاقی را مستقیماً فراخوانی کرد (مانند MyExtensions.ValuesLessThan(list, 3)). برای اعضای الحاقی با نحو جدید، رفع ابهام از طریق فراخوانی متدهای پایین آورده شده توسط کامپایلر انجام می‌شود:
var list = new List<int> { 1, 2, 3, 4, 5 };
var x1 = MyExtensions.ValuesLessThan(list, 3);
var x2 = MyExtensions.ValuesGreaterThan(list, 3);
var x3 = MyExtensions.get_ValuesGreaterThanZero(list);
استفاده از نام کلاس استاتیک برای رفع ابهام، با رویکرد متدهای الحاقی سنتی سازگار است و انتظار می‌رود که برای توسعه‌دهندگان آشنا باشد. هنگام وارد کردن نام کلاس استاتیک در محیط توسعه یکپارچه، قابلیت تکمیل خودکار (IntelliSense) اعضای پایین آورده شده را نیز نمایش می‌دهد (مانند get_ValuesGreaterThanZero).

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

متدهای و خصوصیات استاتیک (Static Methods and Properties)
اعضای الحاقی استاتیک نیز پشتیبانی می‌شوند و کمی متفاوت هستند زیرا گیرنده‌ای وجود ندارد. پارامتر در بلاک extension برای اعضای استاتیک نیازی به نام ندارد و اگر نامی برای آن تعیین شود، نادیده گرفته می‌شود:
public static class MyExtensions
{
    extension<T>(List<T>)
    {
        public static List<T> Create()
            => [];
    }
}

نوع‌های عمومی (Generics)
مانند متدهای الحاقی سنتی، اعضای الحاقی نیز می‌توانند دارای نوع‌های عمومی باز (Open Generics) یا بسته (Concrete Generics)، با یا بدون محدودیت (Constraints) باشند:
public static class MyExtensions
{
    extension<T>(IEnumerable<T> source)
        where T : IEquatable<T>
    {
        public IEnumerable<T> ValuesEqualTo(T threshold)
            => source.Where(x => x.Equals(threshold));
    }
}
محدودیت‌های نوع عمومی به مثال‌های قبلی اجازه می‌دهند تا برای هر نوع عددی که از رابط INumber<T> پشتیبانی می‌کند، به‌روزرسانی شوند:
public static class MyExtensions
{
    extension<T>(IEnumerable<T> source)
        where T : INumber<T>
    {
        public IEnumerable<T> ValuesGreaterThan(T threshold)
            => source.Where(x => x > threshold);

        public IEnumerable<T> ValuesGreaterThanZero
            => source.ValuesGreaterThan(T.Zero);

        public IEnumerable<TResult> SelectGreaterThan<TResult>(
                T threshold,
                Func<T, TResult> select)
            => source
                 .ValuesGreaterThan(x => x > threshold)
                 .Select(select);
    }
}
پارامتر نوع در بلاک extension (<T>)، پارامتر نوع عمومی است که از نوع گیرنده استنباط می‌شود. متد SelectGreaterThan همچنین دارای یک پارامتر نوع عمومی دیگر (<TResult>) است که از نوع نماینده (Delegate) پاس داده شده به پارامتر select استنباط می‌شود.

برخی از متدهای الحاقی قابل انتقال نیستند
قواعد مربوط به نحو جدید اعضای الحاقی منجر به این خواهد شد که برخی از متدهای الحاقی سنتی همچنان نیاز به حفظ نحو فعلی خود داشته باشند. ترتیب پارامترهای نوع عمومی در فرم پایین آورده شده نحو جدید، ابتدا پارامترهای نوع گیرنده و سپس پارامترهای نوع متد است. برای مثال، متد SelectGreaterThan در بالا به صورت زیر پایین آورده می‌شود:
public static class MyExtensions
{
    public static IEnumerable<TResult> SelectGreaterThan<T, TResult>(
                this IEnumerable<T> source, T threshold, Func<T, TResult> select) ...
}
اگر پارامترهای نوع گیرنده در ابتدا ظاهر نشوند، متد قابل انتقال به نحو جدید نیست. مثال زیر یک متد الحاقی سنتی است که قابل انتقال نیست:
public static class MyExtensions
{
    public static IEnumerable<TResult> SelectLessThan<TResult, T>(
                this IEnumerable<T> source, T threshold, Func<T, TResult> select) ...
}
همچنین، در حال حاضر امکان انتقال به نحو جدید وجود ندارد اگر یک پارامتر نوع در گیرنده دارای محدودیتی باشد که به یک پارامتر نوع از عضو وابسته باشد. تیم طراحی در حال بررسی امکان حذف این محدودیت بر اساس بازخوردها است. انتظار می‌رود این موارد نادر باشند و متدهای الحاقی که با این محدودیت‌ها روبرو می‌شوند، همچنان با استفاده از نحو سنتی کار خواهند کرد.

یک مشکل کوچک در نامگذاری
تیم طراحی سی‌شارپ اغلب به اعضای الحاقی از نظر استاتیک و نمونه (Instance)، متد و خصوصیت اشاره می‌کند. منظور آن‌ها متدها و خصوصیت‌هایی است که به گونه‌ای رفتار می‌کنند که گویا متدها و خصوصیت‌های استاتیک یا نمونه در نوع زیربنایی هستند. بنابراین، آن‌ها متدهای الحاقی سنتی را به عنوان متدهای الحاقی نمونه در نظر می‌گیرند. اما این ممکن است گیج‌کننده باشد زیرا آن‌ها به عنوان متدهای استاتیک در یک کلاس استاتیک تعریف می‌شوند. آگاهی از این ابهام احتمالی می‌تواند به درک بهتر پیشنهاد، مقالات و ارائه‌ها کمک کند.

نتیجه‌گیری
ایجاد اعضای الحاقی یک مسیر طولانی بوده و طرح‌های بسیاری مورد بررسی قرار گرفته‌اند. برخی از طرح‌ها نیاز به تکرار گیرنده در هر عضو داشتند، برخی بر رفع ابهام تأثیر می‌گذاشتند، برخی محدودیت‌هایی را در نحوه سازماندهی اعضای الحاقی اعمال می‌کردند و برخی در صورت به‌روزرسانی به نحو جدید، تغییرات ناسازگار ایجاد می‌کردند. برخی پیاده‌سازی‌های پیچیده‌ای داشتند و برخی دیگر به سادگی حس و حال زبان سی‌شارپ را نداشتند.
نحو جدید اعضای الحاقی، بدنه عظیم متدهای الحاقی سنتی موجود را حفظ می‌کند و در عین حال انواع جدیدی از اعضای الحاقی را معرفی می‌کند. این نحو یک سینتکس جایگزین برای متدهای الحاقی ارائه می‌دهد که با انواع جدید اعضا سازگار است و کاملاً با نحو سنتی قابل تعویض است. این ویژگی جدید، گامی مهم در جهت افزایش قدرت و انعطاف‌پذیری زبان سی‌شارپ برای توسعه‌دهندگان .NET محسوب می‌شود و به آن‌ها امکان می‌دهد تا کدهای تمیزتر، خواناتر و قابل نگهداری‌تری را ایجاد کنند.


مشاهده مطلب اصلی