عنوان:

‫ایندکسرهای الحاقی (Extension Indexers) در C# 15


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۵/۲۰ ۰۹:۳۵
آدرس: www.dntips.ir
چکیده: با تکامل روزافزون زبان سی‌شارپ (#C)، ویژگی اعضای الحاقی (Extension Members) گام‌های بزرگی به سمت بهبود قابلیت خوانایی و بیان‌گری کد برداشته است. پس از ارتقای اعضای الحاقی در نسخه‌های اخیر، نسخه C# 15 قابلیت جدیدی تحت عنوان ایندکسرهای الحاقی (Extension Indexers) را معرفی کرده است. این ویژگی به توسعه‌دهندگان اجازه می‌دهد بدون دستکاری سورس‌کد اصلی یا دسترسی به تعریف یک نوع (Type)، نحوه دسترسی شاخصی (Syntax []) را برای انواع موجود فراهم کنند. این مقاله به بررسی ساختار سینتکسی، نحوه اعلان، تحلیل فنی زیرساختی، ملاحظات عملکردیو بهترین الگوی مصرف این قابلیت جدید در اکوسیستم دات‌نت می‌پردازد.

۱. مقدمه
از زمان معرفی Extension Methodها در C# 3.0، امکان افزودن رفتارهای متدی به انواعی که به سورس‌کد آن‌ها دسترسی نداشتیم (مانند اینترفیس‌ها یا کلاس‌های کتابخانه‌های شخص ثالث) یکی از محبوب‌ترین ویژگی‌های این زبان بوده است. اما این قابلیت سال‌ها به متدها محدود بود. با معرفی بلوک‌های الحاقی جدید (Extension Blocks)، سی‌شارپ به سمت بستری یکپارچه‌تر حرکت کرده‌است که در آن هدف این است: «چگونه می‌توان طوری با یک نوع موجود رفتار کرد که گویی اعضای بیشتری به صورت ذاتی درون خود دارد؟»

دستیابی به این هدف با پشتیبانی C# 15 از Extension Indexers کاملاً برجسته می‌شود. این ویژگی امکان استفاده از سینتکس [index] را روی انواعی که فاقد ایندکسر داخلی هستند (مانند IEnumerable) میسر می‌سازد. با این حال، این زیباسازی سینتکسی (Syntactic Sugar) خطرات ناخواسته‌ای مانند پنهان‌سازی پیچیدگی الگوریتمی دارد که آگاهی عمیق توسعه‌دهنده از زیرساخت اجرایی را می‌طلبد.

۲. ساختار نحوی و نحوه اعلان (Syntax & Declaration)
ایندکسرها همواره اعضای مبتنی بر نمونه (Instance Members) هستند؛ بنابراین برای تعریف یک ایندکسر توسعه، باید یک بلوک توسعه (Extension Block) به همراه نام پارامتر دریافت‌کننده (Receiver Parameter) تعریف شود.

نمونه کد اعلان و نحوه تعریف:
using System;
using System.Collections.Generic;
using System.Linq;

namespace CSharp15Features
{
    public static class SequenceIndexerExtensions
    {
        // تعریف بلوک توسعه روی IEnumerable<int> به همراه نام پارامتر receiver (sequence)
        extension(IEnumerable<int> sequence)
        {
            // تعریف ایندکسر فقط-خواندنی (Get-only Indexer)
            public int this[int index] => sequence.ElementAt(index);
        }
    }
}

نحوه فراخوانی و استفاده:
پس از تعریف بلوک فوق و using کردن فضای نام مربوطه، توسعه‌دهنده می‌تواند بدون فراخوانی مستقیم متدهای LINQ، از سینتکس بومی ایندکسر استفاده کند:
IEnumerable<int> numbers = Enumerable.Range(1, 10);

// فراخوانی ایندکسر توسعه دقیقا مشابه یک ایندکسر ذاتی روی نوع
int thirdElement = numbers[2]; 

Console.WriteLine(thirdElement); // خروجی: 3

۳. تحلیل فنی
اگرچه Extension Indexerها کد را تمیزتر و خواناتر می‌سازند، اما توجه به جزئیات زیر برای توسعه‌دهندگان دات‌نت حیاتی است:

۳.۱. عدم تغییر در پیچیدگی زمانی (Time Complexity)
یکی از مهم‌ترین نکات فنی که باید مد نظر قرار گیرد این است که تغییر سینتکس به هیچ عنوان ساختار داده و رفتار کارایی زیرین را تغییر نمی‌دهد.
در یک ساختار برپایه IList یا آرایه، دسترسی به یک عنصر از طریق ایندکسر دارای پیچیدگی زمانی O(1) است. اما در IEnumerable که فاقد ایندکس داخلی است، کد بالا درون خود متد LINQ ElementAt را صدا می‌زند.
  • اگر دنباله ورودی یک لیست نباشد، ElementAt باید دنباله را پیمایش کند تا به عنصر مورد نظر برسد؛ یعنی پیچیدگی اجرای numbers[900000] برابر با O(N) خواهد بود.
  • هشدار: ظاهر کد این احساس اشتباه را به توسعه‌دهنده می‌دهد که با یک عملیات فوق‌العاده سریع O(1) روبرو است، در حالیکه در باطن ممکن است یک پیمایش سنگین سنگین O(N) رخ دهد.

۳.۲. بهینه‌سازی هوشمندانه درون ایندکسر
برای جلوگیری از افول کارایی، پیشنهاد می‌شود هنگام پیاده‌سازی ایندکسرهای توسعه روی انواعی مثل IEnumerable، بررسی‌های نوعی (Pattern Matching یا Type Casting) صورت گیرد تا در صورت پشتیبانی ساختار زیرین از IReadOnlyList یا IList، دسترسی مستقیم O(1) اعمال شود:
public static class EnumerableExtensions
{
    extension<T>(IEnumerable<T> sequence)
    {
        public T this[int index]
        {
            get
            {
                // بهینه‌سازی: اگر ساختار داده از قبل Indexable است، سریعاً دسترسی مستقیم داده شود
                if (sequence is IReadOnlyList<T> readOnlyList)
                    return readOnlyList[index];

                if (sequence is IList<T> list)
                    return list[index];

                // در غیر این صورت، پیمایش LINQ انجام می‌شود (O(N))
                return sequence.ElementAt(index);
            }
        }
    }
}

۳.۳. اولویت فراخوانی (Resolution Rules)
در کامپایلر #C، اولویت فراخوانی اعضا همواره با اعضای ذاتی نوع (Instance Members) است. اگر نوعی خودش شامل یک ایندکسر باشد، کامپایلر همیشه ایندکسر داخلی خود کلاس را فراخوانی کرده و Extension Indexer را نادیده می‌گیرد. این مسئله مانع برهم خوردن رفتار کد موجود (Breaking Changes) می‌شود.

۴. نتیجه‌گیری
ویژگی Extension Indexers در C# 15 گام باشکوه دیگری در راستای افزایش قدرت بیان‌گری (Expressiveness) و یکپارچه‌سازی اعضای توسعه در زبان سی‌شارپ است. این ویژگی به ما اجازه می‌دهد سینتکس شفاف‌تر و زیباتری بنویسیم و انواعی را که صاحب سورس‌کد آن‌ها نیستیم، با قابلیت‌های مدرن بازطراحی کنیم. با این حال، به عنوان توسعه‌دهنده دات‌نت باید همواره مراقب «انتزاع‌های پنهان‌کننده کارایی» (Leaky Abstractions) باشیم. زیبایی سینتکس نباید باعث فراموشی ماهیت ساختار داده‌ها شود. استفاده آگاهانه از این ویژگی در کنار اعمال بهینه‌سازی‌های لازم درون کد توسعه، کدی خوانا، قدرتمند و بهینه را به ارمغان خواهد آورد.