مقدمه

در درس‌های گذشته، یاد گرفتیم که چگونه با استفاده از کلمه‌ی کلیدی delegate، انواع نماینده‌ی سفارشی خود را تعریف کنیم. این روش کاملاً کارآمد است، اما اغلب متوجه می‌شویم که در حال تعریف نماینده‌هایی با الگوهای بسیار مشابه و تکراری هستیم؛ برای مثال، نماینده‌ای که هیچ پارامتری نمی‌گیرد و void برمی‌گرداند، یا نماینده‌ای که یک string می‌گیرد و یک bool برمی‌گرداند. برای جلوگیری از این تکرار و استانداردسازی کد، فریم‌ورک .NET مجموعه‌ای از نمایندگان جنریک آماده را در فضای نام System فراهم کرده است. این نمایندگان جنریک، نیاز ما به تعریف نماینده‌ی سفارشی را در اکثر قریب به اتفاق موارد از بین می‌برند.

Action<T>: برای متدهایی که void برمی‌گردانند

نماینده‌ی جنریک Action برای کپسوله کردن متدهایی استفاده می‌شود که هیچ مقداری را برنمی‌گردانند (یعنی void هستند). این نماینده چندین نسخه‌ی سربارگذاری شده دارد:

  • Action: به متدی اشاره دارد که هیچ پارامتری ندارد.
  • Action<T>: به متدی اشاره دارد که یک پارامتر از نوع `T` دارد.
  • Action<T1, T2>: به متدی اشاره دارد که دو پارامتر از انواع T1 و T2 دارد (و به همین ترتیب تا ۱۶ پارامتر).

بیایید مثال ProgressReporter از درس قبل را با استفاده از Action<int> بازنویسی کنیم.

Copy Icon FileDownloader.cs
// No need to define a custom delegate anymore!
// public delegate void ProgressReporter(int percentage);

public class FileDownloader
{
    // We use the built-in generic Action delegate.
    public Action<int> OnProgress;

    public void DownloadFile()
    {
        for (int i = 0; i <= 100; i += 25)
        {
            // The rest of the logic remains exactly the same.
            OnProgress?.Invoke(i);
        }
    }
}

با این تغییر، ما از تعریف یک نوع نماینده‌ی اضافی خلاص شدیم و کد ما استانداردتر و خواناتر شد. هر برنامه‌نویس C# با دیدن Action<int> فوراً متوجه می‌شود که با یک نماینده برای متدی که یک int می‌گیرد و void برمی‌گرداند، سروکار دارد.

Func<T, TResult>: برای متدهایی که مقدار برمی‌گردانند

نماینده‌ی جنریک Func برای کپسوله کردن متدهایی استفاده می‌شود که یک مقدار را برمی‌گردانند. این نماینده نیز چندین نسخه‌ی سربارگذاری شده دارد.

قانون مهم: در تعریف Func، آخرین پارامتر نوع، همیشه نوع بازگشتی (return type) متد است.

  • Func<TResult>: به متدی اشاره دارد که هیچ پارامتری ندارد و مقداری از نوع TResult برمی‌گرداند.
  • Func<T, TResult>: به متدی اشاره دارد که یک پارامتر از نوع T دارد و مقداری از نوع TResult برمی‌گرداند.
  • Func<T1, T2, TResult>: به متدی اشاره دارد که دو پارامتر از انواع T1 و T2 دارد و مقداری از نوع TResult برمی‌گرداند (و به همین ترتیب تا ۱۶ پارامتر ورودی).

بیایید مثال BinaryOperation را با استفاده از Func<int, int, int> بازنویسی کنیم.

Copy Icon Program.cs
public class Calculator
{
    public static int Add(int a, int b) => a + b;
}

// No need to define: public delegate int BinaryOperation(int x, int y);

// Use Func. The last type is the return type.
Func<int, int, int> addOperation = Calculator.Add;

int result = addOperation(20, 22);
Console.WriteLine(result); // Output: 42

Predicate<T>: حالت خاصی از Func

یک سناریوی بسیار رایج، نوشتن متدی است که یک مقدار را تست کرده و true یا false برمی‌گرداند. برای این کار می‌توانستیم از Func<T, bool> استفاده کنیم. اما فریم‌ورک .NET یک نماینده‌ی خاص و گویاتر برای این منظور فراهم کرده است: Predicate .

Predicate دقیقاً معادل Func<T, bool> است. این نماینده متدی را کپسوله می‌کند که یک پارامتر از نوع T گرفته و یک مقدار bool برمی‌گرداند. استفاده از آن کد را خواناتر می‌کند، زیرا نام Predicate (به معنای گزاره) به وضوح هدف متد را مشخص می‌کند. این نماینده اغلب در متدهای فیلتر کردن کالکشن‌ها به کار می‌رود.

Copy Icon Program.cs
public static bool IsEven(int number)
{
    return number % 2 == 0;
}

var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };

// Create an instance of the Predicate delegate.
Predicate<int> evenNumberPredicate = IsEven;

// Use the predicate with the FindAll method of the list.
List<int> evenNumbers = numbers.FindAll(evenNumberPredicate);

// Print the result
foreach (var num in evenNumbers)
{
    Console.Write($"{num} "); // Output: 2 4 6
}

چه زمانی از کدام نماینده استفاده کنیم؟

  • اگر متد شما مقداری برنمی‌گرداند (void)، از Action استفاده کنید.
  • اگر متد شما مقداری برمی‌گرداند، از Func استفاده کنید.
  • اگر متد شما یک مقدار را برای برگرداندن true یا false بررسی می‌کند، از Predicate استفاده کنید تا هدف کد شما واضح‌تر باشد.

قانون کلی: در برنامه‌نویسی مدرن C#، تقریباً هرگز نیازی به تعریف یک نوع delegate سفارشی نخواهید داشت. همیشه ابتدا بررسی کنید که آیا یکی از نمایندگان جنریک Action، Func یا Predicate نیاز شما را برآورده می‌کند یا خیر.