مقدمه

بارها پیش می‌آید که آرزو می‌کنیم کاش یک نوع داده‌ی موجود در فریم‌ورک .NET (مانند string یا DateTime) یک متد خاص دیگر را هم داشت. برای مثال، یک متد برای شمارش تعداد کلمات یک رشته. از آنجایی که ما به کد منبع این کلاس‌ها دسترسی نداریم، نمی‌توانیم آن‌ها را مستقیماً تغییر دهیم. راه حل سنتی، ایجاد یک کلاس کمکی استاتیک و فراخوانی متدی از آن است. اما C# یک راهکار بسیار زیباتر و خواناتر ارائه می‌دهد: متدهای بسطی (Extension Methods). این ویژگی به ما اجازه می‌دهد تا به انواع داده‌ی موجود (حتی انواعی که خودمان ننوشته‌ایم) متدهای جدیدی "اضافه" کنیم، بدون اینکه نیازی به تغییر کد منبع اصلی، ارث‌بری یا کامپایل مجدد آن‌ها داشته باشیم.

متد بسطی چیست؟

یک متد بسطی در واقع یک متد static است که در یک کلاس static تعریف می‌شود. "جادوی" آن در پارامتر اولش نهفته است. پارامتر اول یک متد بسطی با کلمه‌ی کلیدی this مشخص می‌شود و نوعی را که قرار است بسط داده شود، تعیین می‌کند. اگرچه این متدها به صورت استاتیک تعریف می‌شوند، اما کامپایلر به ما اجازه می‌دهد تا آن‌ها را طوری فراخوانی کنیم که گویی یک متد نمونه (instance method) بر روی خود آن نوع هستند. این ویژگی، خوانایی کد را به شدت افزایش می‌دهد.

ساخت و استفاده از یک متد بسطی

برای تعریف یک متد بسطی، باید سه قانون را رعایت کنیم:

  1. کلاسی که متد بسطی را در خود جای می‌دهد، باید public static باشد.
  2. خود متد بسطی نیز باید public static باشد.
  3. پارامتر اول متد باید با کلمه‌ی کلیدی this مشخص شده و نوعی را که بسط می‌دهد، تعیین کند.

بیایید یک متد بسطی برای کلاس string بنویسیم که تعداد کلمات یک رشته را بشمارد.

Copy Icon StringExtensions.cs
// Extension methods must be defined in a static class.
public static class StringExtensions
{
    // The 'this' keyword before the first parameter makes this an extension method.
    // It "extends" the string type.
    public static int WordCount(this string str)
    {
        if (string.IsNullOrWhiteSpace(str))
        {
            return 0;
        }
        return str.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

فراخوانی متد بسطی

پس از تعریف، می‌توانیم این متد را مستقیماً روی هر نمونه‌ای از نوع string فراخوانی کنیم، گویی که از ابتدا بخشی از آن بوده است.

Copy Icon Program.cs
string myQuote = "The quick brown fox jumps over the lazy dog.";

// Call the extension method as if it were an instance method.
int count = myQuote.WordCount();

Console.WriteLine($"The sentence has {count} words."); // Output: 9

// Behind the scenes, the compiler translates the call to this:
// int countStaticCall = StringExtensions.WordCount(myQuote);

همانطور که در کامنت آخر کد مشخص است، کامپایلر فراخوانی myQuote.WordCount() را به فراخوانی متد استاتیک StringExtensions.WordCount(myQuote) ترجمه می‌کند. این یک ساده‌سازی بسیار مفید است که به ما اجازه می‌دهد زنجیره‌ای از فراخوانی متدها را به شکلی روان و خوانا بنویسیم.

بسط دادن اینترفیس‌ها

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

مشهورترین و بزرگ‌ترین مثال این الگو، LINQ (Language-Integrated Query) است. متدهایی مانند Where، Select، OrderBy و... همگی متدهای بسطی هستند که برای اینترفیس IEnumerable<T> تعریف شده‌اند. به همین دلیل است که شما می‌توانید از این متدها بر روی آرایه‌ها، List<T> و هر کالکشن دیگری که این اینترفیس را پیاده‌سازی کرده، استفاده کنید.

Copy Icon EnumerableExtensions.cs
public static class EnumerableExtensions
{
    // This method extends any type that implements IEnumerable.
    public static void PrintToConsole<T>(this IEnumerable<T> collection)
    {
        foreach (var item in collection)
        {
            Console.Write($"{item} ");
        }
        Console.WriteLine();
    }
}

// --- Usage ---
List<int> numbers = new() { 1, 2, 3 };
string[] names = { "A", "B", "C" };

numbers.PrintToConsole(); // Works on a List
names.PrintToConsole();   // Works on a string[]

نکات و بهترین رویه‌ها

  • استفاده از فضای نام (Namespace): بهترین کار این است که متدهای بسطی خود را در یک فضای نام جداگانه قرار دهید. این کار به کاربران کد شما اجازه می‌دهد تا با افزودن یک using، این قابلیت‌ها را به صورت آگاهانه به محدوده‌ی کد خود اضافه کنند و از "آلوده شدن" فضای نام عمومی جلوگیری می‌کند.
  • اولویت با متدهای نمونه: اگر یک کلاس هم یک متد نمونه و هم یک متد بسطی با نام و امضای یکسان داشته باشد، کامپایلر همیشه متد نمونه را انتخاب می‌کند. متدهای بسطی هرگز نمی‌توانند یک متد موجود را بازنویسی (override) کنند.
  • هدف، افزایش خوانایی است: از متدهای بسطی برای افزودن قابلیت‌های کمکی و مرتبط به یک نوع استفاده کنید. از آن‌ها برای تغییر رفتار اصلی یک نوع یا ایجاد قابلیت‌های غیرمنتظره که خوانایی را کاهش می‌دهند، خودداری کنید.