مقالات › C#

Delegate، Event و Lambda در C#؛ از مفاهیم پایه تا الگوهای واقعی

Delegate، Func و Action، عبارت‌های Lambda، Closure، Delegateهای چندپخشی، Event و الگوی EventHandler را در C# بشناسید و بدانید کِی اینترفیس یا IObservable را ترجیح دهید.

Delegateها روشی هستند که C# با آن رفتار را مثل داده در نظر می‌گیرد: می‌توانید یک متد را در یک متغیر نگه دارید، به متد دیگری بدهید و بعداً اجرایش کنید. LINQ، Minimal API در ASP.NET Core، Event Handlerهای WPF، سیاست‌های تلاش مجدد و میان‌افزارها همگی روی Delegate ساخته شده‌اند. این مقاله Delegate، Lambda و Event را از پایه توضیح می‌دهد و الگوهایی را نشان می‌دهد که در کدهای واقعی می‌بینید.

Delegate چیست؟

Delegate یک ارجاع امن از نظر نوع به متدی با امضای مشخص است.

public delegate decimal DiscountRule(decimal price);

decimal TenPercent(decimal price) => price * 0.9m;
decimal FlatFive(decimal price) => Math.Max(0, price - 5);

DiscountRule rule = TenPercent;
Console.WriteLine(rule(100));   // 90

rule = FlatFive;
Console.WriteLine(rule(100));   // 95

متغیر rule نگه می‌دارد کدام متد اجرا شود. کد فراخواننده اهمیتی نمی‌دهد کدام تخفیف اعمال می‌شود؛ فقط امضا را می‌شناسد.

Delegateهای آماده: Func، Action و Predicate

امروزه به‌ندرت Delegate سفارشی تعریف می‌کنید. دات‌نت نسخه‌های جنریک فراهم کرده است:

نوع امضا مثال
Action بدون مقدار برگشتی Action<string> log = msg => Console.WriteLine(msg);
Func<…, TResult> مقدار برمی‌گرداند (آخرین آرگومان نوع) Func<int, int, int> add = (a, b) => a + b;
Predicate<T> bool برمی‌گرداند Predicate<int> isEven = n => n % 2 == 0;
Func<decimal, decimal> rule = price => price * 0.9m;

decimal ApplyRules(decimal price, IEnumerable<Func<decimal, decimal>> rules) =>
    rules.Aggregate(price, (current, apply) => apply(current));

var final = ApplyRules(1_000_000, [p => p * 0.9m, p => p - 50_000]);

Delegate سفارشی هنوز وقتی معنا دارد که نام به آن مفهوم اضافه کند (DiscountRule) یا به پارامتر ref/out نیاز داشته باشید.

عبارت‌های Lambda

Lambda یک تابع بی‌نام درون‌خطی است:

Func<int, int> square = x => x * x;                 // Lambda عبارتی
Func<int, int, int> max = (a, b) => a > b ? a : b;
Action<string> greet = name =>
{                                                    // Lambda دستوری
    var message = $"Hello, {name}!";
    Console.WriteLine(message);
};

var adults = people.Where(p => p.Age >= 18);         // Lambda همه‌جای LINQ

از C# 10 به بعد، Lambdaها می‌توانند نوع طبیعی، نوع برگشتی صریح و Attribute داشته باشند:

var parse = (string s) => int.Parse(s);              // به‌صورت Func<string, int> استنتاج می‌شود
var choose = object (bool b) => b ? 1 : "one";       // نوع برگشتی صریح

Closure؛ Lambda متغیرها را Capture می‌کند

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

int counter = 0;
Action increment = () => counter++;

increment();
increment();
Console.WriteLine(counter);  // 2

متغیرهای Capture‌شده تا وقتی Lambda زنده است زنده می‌مانند. مراقب Lambdaهای بلندعمر (Event Handler، کش‌های استاتیک) باشید که اشیای بزرگ را Capture می‌کنند؛ آن‌ها را در حافظه نگه می‌دارند.

وقتی قصد Capture ندارید از Lambdaهای static استفاده کنید تا کامپایلر جلوی Capture تصادفی را بگیرد:

Func<int, int> doubler = static x => x * 2;

متدهایی که رفتار می‌گیرند

پذیرفتن Delegate متدها را بدون نیاز به وراثت انعطاف‌پذیر می‌کند:

public static async Task<T> RetryAsync<T>(
    Func<Task<T>> operation, int attempts = 3, TimeSpan? delay = null)
{
    for (var i = 1; ; i++)
    {
        try
        {
            return await operation();
        }
        catch (HttpRequestException) when (i < attempts)
        {
            await Task.Delay(delay ?? TimeSpan.FromMilliseconds(200 * i));
        }
    }
}

var rates = await RetryAsync(() => http.GetFromJsonAsync<Rates>("/api/rates"));

Minimal APIهای ASP.NET Core همین ایده در سطح فریم‌ورک هستند؛ هر Endpoint یک Delegate است:

app.MapGet("/articles/{slug}", async (string slug, IArticleService service) =>
    await service.GetAsync(slug) is { } article ? Results.Ok(article) : Results.NotFound());

Delegateهای چندپخشی (Multicast)

Delegate می‌تواند چند متد نگه دارد. با فراخوانی آن، متدها به ترتیب اجرا می‌شوند:

Action<string> notify = msg => Console.WriteLine($"Console: {msg}");
notify += msg => File.AppendAllText("log.txt", msg + Environment.NewLine);

notify("Order #42 shipped");  // هر دو اجرا می‌شوند
notify -= ...;                // حذف یک Handler (به همان نمونه نیاز دارد)

در Delegateهای چندپخشی Func فقط مقدار برگشتی آخرین متد حفظ می‌شود؛ Multicast برای اطلاع‌رسانی است، نه محاسبه.

Event؛ Delegate چندپخشی امن

Event یک فیلد Delegate با دسترسی محدود است: کد بیرونی فقط می‌تواند مشترک شود (+=) یا لغو اشتراک کند (-=)؛ فقط کلاس مالک می‌تواند آن را اجرا (Raise) کند.

public class OrderPlacedEventArgs(int orderId, decimal total) : EventArgs
{
    public int OrderId { get; } = orderId;
    public decimal Total { get; } = total;
}

public class OrderService
{
    public event EventHandler<OrderPlacedEventArgs>? OrderPlaced;

    public void Place(Order order)
    {
        // ... ذخیره
        OnOrderPlaced(new OrderPlacedEventArgs(order.Id, order.Total));
    }

    protected virtual void OnOrderPlaced(OrderPlacedEventArgs e) =>
        OrderPlaced?.Invoke(this, e);   // ?. در صورت نبود مشترک جلوی NullReferenceException را می‌گیرد
}

var service = new OrderService();
service.OrderPlaced += (sender, e) => Console.WriteLine($"Send receipt for {e.OrderId}");

قرارداد دات‌نت: EventHandler<TEventArgs> با امضای (object? sender, TEventArgs e)، یک متد protected virtual به نام On… برای Raise و کلاس آرگومانی که از EventArgs مشتق شده است.

جلوگیری از نشت حافظه با Event

ناشر (Publisher) از طریق Event به مشترک ارجاع دارد. اگر یک ناشر بلندعمر (یک سرویس استاتیک) Handlerهای مشترکان کوتاه‌عمر (پنجره‌ها، ViewModelها) را نگه دارد، آن مشترکان هرگز جمع‌آوری زباله نمی‌شوند. وقتی کار مشترک تمام شد، همیشه لغو اشتراک کنید:

public void Dispose() => _service.OrderPlaced -= HandleOrderPlaced;

Event در برابر جایگزین‌ها

نیاز گزینه‌ی مناسب
اطلاع‌رسانی رابط کاربری در WPF/WinForms Event، INotifyPropertyChanged
اطلاع‌رسانی دامنه داخل یک پروسه Domain Event + Dispatcher (مثل Notificationهای MediatR)
جریان مقادیر در طول زمان IObservable<T> / Reactive Extensions، IAsyncEnumerable<T>، Channel<T>
ارتباط بین سرویس‌ها Message Broker (RabbitMQ، Kafka)
استراتژی قابل تعویض با چند متد اینترفیس

Event برای رابط کاربری و اطلاع‌رسانی‌های ساده‌ی داخل پروسه عالی است. برای کارهای async توجه کنید که Event Handlerها ذاتاً همگام‌اند؛ یک Handler از نوع async void قابل await نیست و مدیریت Exceptionهایش سخت است.

Expression Tree؛ Lambda به‌عنوان داده

وقتی یک Lambda به‌جای Func<…> به Expression<Func<…>> نسبت داده شود، کامپایلر به‌جای کد اجرایی یک ساختار داده که کد را توصیف می‌کند تولید می‌کند. EF Core دقیقاً این‌طور LINQ شما را به SQL تبدیل می‌کند:

Expression<Func<Product, bool>> cheap = p => p.Price < 100;  // درخت قابل بررسی
Func<Product, bool> cheapFunc = cheap.Compile();              // Delegate قابل اجرا

جمع‌بندی

Delegateها اجازه می‌دهند رفتار را مثل یک مقدار جابه‌جا کنید؛ Func، Action و Predicate تقریباً همه‌ی نیازها را پوشش می‌دهند؛ Lambdaها آن‌ها را کوتاه می‌کنند و از طریق Closure متغیرهای اطراف را Capture می‌کنند. Eventها Delegateهای چندپخشی را در یک API امن انتشار/اشتراک می‌پیچند؛ فقط لغو اشتراک را فراموش نکنید. با این ابزارها APIهای انعطاف‌پذیر، توابع تلاش مجدد، Pipelineها و سیستم‌های اطلاع‌رسانی را بدون سلسله‌مراتب سنگین وراثت می‌سازید.

پرسش‌های متداول

تفاوت Delegate و اینترفیس چیست؟

Delegate یک امضای متد واحد را نمایش می‌دهد و اینترفیس قراردادی با احتمالاً اعضای متعدد. برای عملیات تکی و Callback از Delegate و برای انتزاع‌های غنی‌تر از اینترفیس استفاده کنید.

آیا Lambda از متد معمولی کندتر است؟

فراخوانی از طریق Delegate سربار ناچیزی دارد و Closureها حافظه تخصیص می‌دهند. فقط در مسیرهای بسیار داغ اهمیت دارد.

چرا Event با ?.Invoke اجرا می‌شود؟

اگر کسی مشترک نشده باشد، فیلد Event برابر null است. عملگر شرطی null جلوی NullReferenceException را می‌گیرد و بررسی را Thread-Safe انجام می‌دهد.