delegateها و Eventها در C#: درک برنامه‌نویسی رویدادمحور

فهرست مطالب

مقدمه: دروازه‌ای به دنیای برنامه‌نویسی رویدادمحور در C#

در دنیای پویای توسعه نرم‌افزار، توانایی واکنش به رویدادها و تعاملات کاربران یا سیستم‌ها، نقشی حیاتی در ساخت برنامه‌های پاسخگو و کاربرپسند ایفا می‌کند. این پارادایم، که به “برنامه‌نویسی رویدادمحور” (Event-Driven Programming) مشهور است، ستون فقرات بسیاری از برنامه‌های مدرن، از رابط‌های کاربری گرافیکی (GUIs) گرفته تا سیستم‌های توزیع‌شده و میکروسرویس‌ها، را تشکیل می‌دهد. در قلب برنامه‌نویسی رویدادمحور در C#، دو مفهوم کلیدی قرار دارند: Delegateها و Eventها.

اگرچه Delegateها و Eventها ممکن است در نگاه اول پیچیده به نظر برسند، اما با درک صحیح آن‌ها، می‌توانید کدی انعطاف‌پذیرتر، قابل نگهداری‌تر و مقیاس‌پذیرتر بنویسید. Delegateها به ما امکان می‌دهند ارجاعی به متدها داشته باشیم و آن‌ها را مانند اشیا منتقل کنیم، در حالی که Eventها مکانیزمی امن و استاندارد برای اطلاع‌رسانی از وقوع یک اتفاق به سایر اجزای برنامه فراهم می‌آورند. این ترکیب قدرتمند، به ما اجازه می‌دهد تا منطق برنامه را از طریق یک مدل “ناشر-مشترک” (Publisher-Subscriber) سازماندهی کنیم، جایی که یک جزء (ناشر) از وقوع یک رویداد خبر می‌دهد و اجزای دیگر (مشترکین) می‌توانند بدون نیاز به دانستن جزئیات پیاده‌سازی ناشر، به آن واکنش نشان دهند.

در این مقاله جامع و تخصصی، ما به اعماق مفهوم Delegateها و Eventها در C# شیرجه خواهیم زد. از اصول بنیادین و نحوه‌ی تعریف و استفاده از آن‌ها گرفته تا مباحث پیشرفته‌تر مانند Delegateهای Multicast، استفاده از Action و Func، Lambda Expressionها، مدیریت رویدادهای سفارشی، ملاحظات Thread Safety، و جلوگیری از Memory Leakها با Weak Events، همه و همه را به تفصیل مورد بررسی قرار خواهیم داد. هدف ما این است که شما نه تنها با نحوه‌ی کارکرد این مفاهیم آشنا شوید، بلکه درک عمیقی از چرایی و چگونگی استفاده مؤثر از آن‌ها در طراحی معماری‌های نرم‌افزاری مدرن به دست آورید. آماده شوید تا نگاهی دقیق‌تر به مکانیزم‌هایی داشته باشیم که C# را به زبانی قدرتمند برای ساخت برنامه‌های رویدادمحور تبدیل کرده‌اند.

1. بنیادها: درک Delegateها در C#

برای درک کامل Eventها، ابتدا باید با مفهوم Delegateها آشنا شویم. Delegate در C# را می‌توان به عنوان یک “اشاره‌گر تابع امن نوع” (Type-Safe Function Pointer) در نظر گرفت. این مفهوم ریشه در زبان‌های برنامه‌نویسی سطح پایین‌تر مانند C/C++ دارد که در آن‌ها اشاره‌گرهای تابع امکان ارسال یک متد به عنوان آرگومان به متد دیگر را فراهم می‌آوردند. با این حال، Delegateها در C# بسیار قدرتمندتر و ایمن‌تر هستند؛ آن‌ها نه تنها امکان ارجاع به یک متد را می‌دهند، بلکه می‌توانند به متدهای استاتیک یا متدهای نمونه (instance methods) ارجاع دهند و کامپایلر اطمینان حاصل می‌کند که امضای متد ارجاع داده شده (شامل تعداد و نوع پارامترها و نوع بازگشتی) با امضای Delegate سازگار است.

تعریف و هدف Delegate

هدف اصلی Delegateها ایجاد یک مکانیزم CallBack است. به عبارت دیگر، آن‌ها به شما اجازه می‌دهند یک متد را به عنوان یک آرگومان به متد دیگر پاس دهید تا آن متد بتواند در زمان مناسب، متد پاس داده شده را فراخوانی کند. این قابلیت در سناریوهای مختلفی بسیار مفید است، مانند:

  • پردازش رویدادها: جایی که یک جزء (مانند یک دکمه UI) نیاز دارد به اجزای دیگر اطلاع دهد که یک رویداد خاص (مانند کلیک) رخ داده است.
  • Custom Callbacks: هنگامی که می‌خواهید رفتار یک متد را بدون تغییر کد اصلی آن، از بیرون تزریق کنید.
  • Threading: استفاده در متدهای آغازگر Threadها (مانند `ThreadStart` Delegate).
  • LINQ: استفاده در بسیاری از عملیات‌های LINQ برای تعریف Predicateها و Transformations.

ساختار Delegateها: اعلان، نمونه‌سازی و فراخوانی

برای استفاده از یک Delegate، سه گام اصلی وجود دارد:

  1. اعلان (Declaration) Delegate: مانند تعریف یک کلاس یا اینترفیس، Delegate نیز یک نوع (Type) است که امضای متدهایی را که می‌تواند به آن‌ها ارجاع دهد، مشخص می‌کند. امضا شامل نوع بازگشتی و پارامترها است.
  2. نمونه‌سازی (Instantiation) Delegate: پس از اعلان، باید یک نمونه از Delegate ایجاد کنید و آن را به یک متد سازگار ارجاع دهید.
  3. فراخوانی (Invocation) Delegate: پس از نمونه‌سازی، می‌توانید Delegate را مانند یک متد فراخوانی کنید.

مثال کد ساده: یک Delegate برای عملیات ریاضی


using System;

// 1. اعلان یک Delegate
// این Delegate می تواند به هر متدی که دو int می گیرد و یک int برمی گرداند، ارجاع دهد.
public delegate int MathOperation(int a, int b);

public class Calculator
{
    public int Add(int x, int y)
    {
        return x + y;
    }

    public int Subtract(int x, int y)
    {
        return x - y;
    }

    public static void PerformOperation(MathOperation op, int val1, int val2)
    {
        int result = op(val1, val2); // 3. فراخوانی Delegate
        Console.WriteLine($"Result: {result}");
    }

    public static void Main(string[] args)
    {
        Calculator calc = new Calculator();

        // 2. نمونه سازی Delegate و ارجاع به متد Add
        MathOperation addDelegate = new MathOperation(calc.Add);
        PerformOperation(addDelegate, 10, 5); // خروجی: Result: 15

        // 2. نمونه سازی Delegate و ارجاع به متد Subtract
        MathOperation subDelegate = new MathOperation(calc.Subtract);
        PerformOperation(subDelegate, 10, 5); // خروجی: Result: 5

        // سینتکس کوتاه تر برای نمونه سازی
        MathOperation mulDelegate = (x, y) => x * y; // ارجاع به یک Lambda Expression
        PerformOperation(mulDelegate, 10, 5); // خروجی: Result: 50
    }
}

Delegateهای Multicast

یکی از ویژگی‌های قدرتمند Delegateها در C#، قابلیت Multicast بودن آن‌ها است. این بدان معناست که یک نمونه از Delegate می‌تواند به چندین متد ارجاع دهد. وقتی یک Delegate از نوع Multicast فراخوانی می‌شود، تمامی متدهایی که به آن اضافه شده‌اند، به ترتیب اضافه شدن، فراخوانی می‌شوند.

برای اضافه کردن متدها به یک Delegate Multicast، از عملگر `+=` و برای حذف متدها از عملگر `-=` استفاده می‌شود. این عملگرها در پشت صحنه از متدهای `Delegate.Combine` و `Delegate.Remove` استفاده می‌کنند.

مثال کد Multicast Delegate


using System;

public delegate void MessageDelegate(string message);

public class Notifier
{
    public void SendEmail(string msg)
    {
        Console.WriteLine($"Sending Email: {msg}");
    }

    public void SendSms(string msg)
    {
        Console.WriteLine($"Sending SMS: {msg}");
    }

    public static void Main(string[] args)
    {
        MessageDelegate notifier = null;

        notifier += new MessageDelegate(new Notifier().SendEmail);
        notifier += new MessageDelegate(new Notifier().SendSms);
        notifier += msg => Console.WriteLine($"Logging Message: {msg}"); // اضافه کردن یک Lambda

        if (notifier != null)
        {
            notifier("Hello from Multicast Delegate!");
        }

        Console.WriteLine("\nRemoving SMS notifier...");
        notifier -= new MessageDelegate(new Notifier().SendSms); // این خط در اینجا عمل نمی کند زیرا نمونه های جدیدی ساخته شده اند!

        // برای حذف صحیح، باید ارجاع به همان نمونه Delegate نگهداری شود.
        Notifier myNotifierInstance = new Notifier();
        MessageDelegate emailNotifier = myNotifierInstance.SendEmail;
        MessageDelegate smsNotifier = myNotifierInstance.SendSms;

        MessageDelegate properNotifier = emailNotifier;
        properNotifier += smsNotifier;
        properNotifier += msg => Console.WriteLine($"Logging Message: {msg}");

        Console.WriteLine("\nProper Multicast Call:");
        if (properNotifier != null)
        {
            properNotifier("Proper setup test.");
        }

        Console.WriteLine("\nRemoving SMS notifier (properly)...");
        properNotifier -= smsNotifier;
        if (properNotifier != null)
        {
            properNotifier("After removing SMS.");
        }
    }
}

نکته مهم در Multicast Delegateها: اگر Delegate دارای مقدار بازگشتی باشد، تنها مقدار بازگشتی از آخرین متدی که فراخوانی می‌شود، برگردانده خواهد شد. همچنین، اگر یکی از متدهای فراخوانی شده یک Exception پرتاب کند، زنجیره فراخوانی متوقف خواهد شد، مگر اینکه Exceptionها به طور خاص مدیریت شوند.

Action و Func: Delegateهای Built-in

از C# 3.0 به بعد، چارچوب .NET مجموعه‌ای از Delegateهای عمومی (Generic Delegates) پرکاربرد را معرفی کرد که نیاز به تعریف Delegateهای سفارشی برای سناریوهای رایج را به شدت کاهش می‌دهد. این Delegateها `Action` و `Func` نام دارند.

  • `Action` Delegate: برای ارجاع به متدهایی استفاده می‌شود که هیچ مقداری را برنمی‌گردانند (void). `Action` می‌تواند تا ۱۶ پارامتر ورودی داشته باشد.
  • `Func` Delegate: برای ارجاع به متدهایی استفاده می‌شود که یک مقدار را برمی‌گردانند. `Func` می‌تواند تا ۱۶ پارامتر ورودی داشته باشد و آخرین پارامتر همواره نوع بازگشتی متد را مشخص می‌کند.

استفاده از `Action` و `Func` نه تنها کد را کوتاه‌تر و خواناتر می‌کند، بلکه قابلیت استفاده مجدد را نیز افزایش می‌دهد، زیرا نیازی به تعریف `delegate` جدید برای هر امضای متد ندارید.

مثال کد با Action و Func


using System;

public class DelegateExamples
{
    public static void PrintMessage(string msg)
    {
        Console.WriteLine($"Message: {msg}");
    }

    public static int Multiply(int a, int b)
    {
        return a * b;
    }

    public static void Main(string[] args)
    {
        // استفاده از Action
        // ارجاع به متدی که یک string می گیرد و هیچ مقداری برنمی گرداند.
        Action<string> printAction = PrintMessage;
        printAction("Hello from Action!");

        // استفاده از Action با Lambda Expression
        Action<int, int> sumAction = (x, y) => Console.WriteLine($"Sum: {x + y}");
        sumAction(10, 20);

        // استفاده از Func
        // ارجاع به متدی که دو int می گیرد و یک int برمی گرداند.
        Func<int, int, int> multiplyFunc = Multiply;
        int result = multiplyFunc(7, 8);
        Console.WriteLine($"Multiplication Result: {result}");

        // استفاده از Func با Lambda Expression
        Func<double, double, double> divideFunc = (a, b) => a / b;
        double divisionResult = divideFunc(100.0, 4.0);
        Console.WriteLine($"Division Result: {divisionResult}");

        // Func بدون پارامتر ورودی
        Func<string> getRandomString = () => Guid.NewGuid().ToString();
        Console.WriteLine($"Random String: {getRandomString()}");
    }
}

متدهای ناشناس و Lambda Expressionها

با معرفی C# 2.0، مفهوم “متدهای ناشناس” (Anonymous Methods) به زبان اضافه شد که امکان تعریف یک بلاک کد را مستقیماً در جایی که Delegate مورد نیاز است، فراهم می‌کرد، بدون نیاز به تعریف یک متد جداگانه. این امر کد را کوتاه‌تر و خواناتر می‌کرد، به خصوص برای Callbackهای ساده.

اما با C# 3.0 و معرفی LINQ، “Lambda Expressionها” (Lambda Expressions) وارد صحنه شدند. Lambdaها سینتکسی بسیار کوتاه‌تر و قدرتمندتر برای تعریف متدهای ناشناس هستند و به طور گسترده‌ای در C# مدرن برای کار با Delegateها، LINQ و Eventها استفاده می‌شوند.

سینتکس یک Lambda Expression به صورت `(parameters) => expression or statement block` است. اپراتور `=>` (به نام “go-to” یا “lambda” اپراتور) پارامترهای ورودی را از بدنه Lambda جدا می‌کند.

مثال کد با متدهای ناشناس و Lambda


using System;

public class LambdaExamples
{
    public static void ProcessList(List<int> numbers, Func<int, bool> filter)
    {
        Console.WriteLine("Filtered Numbers:");
        foreach (var num in numbers)
        {
            if (filter(num))
            {
                Console.WriteLine(num);
            }
        }
    }

    public static void Main(string[] args)
    {
        List<int> myNumbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

        // استفاده از متد ناشناس برای Func
        Console.WriteLine("--- Using Anonymous Method ---");
        ProcessList(myNumbers, delegate(int number)
        {
            return number % 2 == 0; // اعداد زوج
        });

        // استفاده از Lambda Expression برای Func
        Console.WriteLine("--- Using Lambda Expression (Even Numbers) ---");
        ProcessList(myNumbers, number => number % 2 == 0); // بسیار کوتاه‌تر

        Console.WriteLine("--- Using Lambda Expression (Greater than 5) ---");
        ProcessList(myNumbers, n => n > 5); // اعداد بزرگتر از 5

        // Lambda Expression با بدنه Statement block
        Action<string> greet = name =>
        {
            string greeting = $"Hello, {name}!";
            Console.WriteLine(greeting);
        };
        greet("Alice");

        // Lambda Expression بدون پارامتر
        Func<int> getRandomNumber = () => new Random().Next(1, 100);
        Console.WriteLine($"Random Number: {getRandomNumber()}");
    }
}

Lambda Expressionها نه تنها کد را تمیزتر می‌کنند، بلکه به دلیل قابلیت Closures (بستارها) – یعنی توانایی دسترسی به متغیرهای محلی حوزه پیرامونی خود حتی پس از اتمام آن حوزه – در سناریوهای پیچیده‌تر نیز بسیار مفید هستند. این ویژگی، Lambdaها را به ابزاری بی‌نظیر در برنامه‌نویسی Functional-style در C# تبدیل کرده است.

2. ستون فقرات برنامه‌نویسی رویدادمحور: Eventها در C#

پس از درک عمیق Delegateها، نوبت به Eventها می‌رسد. Eventها در C# یک مفهوم سطح بالاتر هستند که بر پایه Delegateها بنا شده‌اند و مکانیزمی امن و استاندارد برای پیاده‌سازی الگوی طراحی “ناشر-مشترک” (Publisher-Subscriber) فراهم می‌کنند. یک Event به طور اساسی یک Delegate کپسوله‌شده است که به یک شیء اجازه می‌دهد تا به سایر اشیاء (که به آن‌ها “مشترک” یا “سخنران” گفته می‌شود) اطلاع دهد که یک اتفاق خاص رخ داده است.

تعریف و هدف Event

یک Event، در واقع، یک Delegate خصوصی است که فقط می‌تواند توسط کلاسی که آن را تعریف کرده است (ناشر) فراخوانی شود. مشترکین فقط می‌توانند به آن Event مشترک شوند (با `+=`) یا اشتراک خود را لغو کنند (با `-=`). این کپسوله‌سازی، کنترل دسترسی به Delegate را تضمین می‌کند و از دستکاری ناخواسته لیست مشترکین توسط کد خارجی جلوگیری می‌کند. Eventها در واقع یک “قرارداد” (Contract) بین ناشر و مشترکین هستند.

چرا Events به جای Delegateهای Public؟

ممکن است این سوال پیش بیاید که چرا از یک Event استفاده کنیم وقتی می‌توانیم یک Delegate را به صورت Public در کلاس تعریف کنیم؟ دلایل اصلی برای استفاده از Eventها به جای Delegateهای Public عبارتند از:

  1. کپسوله‌سازی (Encapsulation): با Eventها، لیست مشترکین (invocation list) فقط توسط کلاسی که Event را تعریف کرده قابل مدیریت است. کدهای خارجی نمی‌توانند لیست را به طور مستقیم پاک کنند یا متدهای خاصی را از لیست حذف کنند مگر اینکه خودشان آن را اضافه کرده باشند. این امر ثبات و امنیت را افزایش می‌دهد.
  2. جلوگیری از فراخوانی مستقیم: کدهای خارجی نمی‌توانند Event را مستقیماً “فراخوانی” (raise) کنند. فقط کلاسی که Event را اعلام کرده، می‌تواند آن را فراخوانی کند. این تضمین می‌کند که Eventها فقط در شرایط مناسبی که توسط ناشر تعیین شده‌اند، آتش می‌شوند.
  3. وضوح و خوانایی: استفاده از `event` keyword به وضوح نشان می‌دهد که هدف این عضو کلاس، یک مکانیزم اطلاع‌رسانی به سبک Publisher-Subscriber است.

معماری Eventها: EventHandler و EventHandler<TEventArgs>

در C#، Eventها به طور معمول از الگوهای خاصی پیروی می‌کنند:

  • `event` keyword: برای اعلان یک Event در یک کلاس استفاده می‌شود.
  • `EventHandler` Delegate: یک Delegate استاندارد در .NET Framework است که برای اکثر Eventها استفاده می‌شود. امضای آن به صورت `public delegate void EventHandler(object sender, EventArgs e);` است.
    • `sender`: شیئی است که Event را ایجاد کرده است.
    • `e`: حاوی اطلاعات مربوط به Event است. نوع پایه آن `EventArgs` است.
  • `EventHandler<TEventArgs>` Delegate: یک نسخه عمومی (Generic) از `EventHandler` است که به شما اجازه می‌دهد کلاس `EventArgs` سفارشی خود را برای ارسال اطلاعات خاص‌تر مربوط به Event استفاده کنید. امضای آن به صورت `public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e) where TEventArgs : EventArgs;` است. این بهترین روش برای تعریف اکثر Eventها است.
  • Convention `OnEventName` method: معمولاً یک متد `protected virtual void OnEventName(EventArgs e)` (یا `TEventArgs`) برای فراخوانی Event تعریف می‌شود. این متد به کلاس‌های مشتق شده اجازه می‌دهد رفتار Event را سفارشی کنند یا Event را در زمان‌های خاصی فراخوانی کنند.

مثال کد ساده: Button Click Event


using System;
using System.Collections.Generic;

// کلاس ناشر (Publisher)
public class Button
{
    // 1. اعلان Event با استفاده از EventHandler
    // این Event به یک Delegate از نوع EventHandler ارجاع می دهد.
    public event EventHandler Click;

    // متد برای "کلیک" کردن دکمه و فراخوانی Event
    protected virtual void OnClick()
    {
        // اطمینان حاصل می کنیم که مشترکانی وجود دارند قبل از فراخوانی
        // این کار به نام Null Check معروف است.
        // در C# 6.0 به بعد می توان از اپراتور null-conditional (?.Invoke) استفاده کرد.
        Click?.Invoke(this, EventArgs.Empty);
    }

    public void SimulateClick()
    {
        Console.WriteLine("Button clicked!");
        OnClick(); // فراخوانی متد OnClick برای آتش زدن Event
    }
}

// کلاس مشترک (Subscriber)
public class EventHandlerExample
{
    public static void Main(string[] args)
    {
        Button myButton = new Button();

        // 2. مشترک شدن در Event
        // ارجاع به یک متد (Event Handler) که امضای EventHandler را دارد.
        myButton.Click += MyButton_Click;
        myButton.Click += MyOtherButton_Click; // می توان چند متد را مشترک کرد (Multicast)

        // شبیه‌سازی کلیک
        myButton.SimulateClick();

        // لغو اشتراک
        Console.WriteLine("\nUnsubscribing MyOtherButton_Click...");
        myButton.Click -= MyOtherButton_Click;
        myButton.SimulateClick(); // MyOtherButton_Click دیگر فراخوانی نمی شود
    }

    // Event Handler (متدی که به Event مشترک می شود)
    private static void MyButton_Click(object sender, EventArgs e)
    {
        Console.WriteLine("Event handled by MyButton_Click!");
        // می توان از 'sender' برای دسترسی به شیء ناشر استفاده کرد.
        // if (sender is Button btn) { Console.WriteLine($"Sender Button: {btn.GetType().Name}"); }
    }

    private static void MyOtherButton_Click(object sender, EventArgs e)
    {
        Console.WriteLine("Event handled by MyOtherButton_Click!");
    }
}

قوانین طراحی Event (Best Practices)

هنگام کار با Eventها، رعایت چند قانون و بهترین روش رایج در .NET ضروری است:

  1. Encapsulation با `event` keyword: همیشه از `event` keyword برای اعلان Eventها استفاده کنید تا از مزایای کپسوله‌سازی بهره‌مند شوید و جلوی دستکاری مستقیم Delegate را بگیرید.
  2. استفاده از `EventHandler<TEventArgs>`: به جای تعریف Delegate سفارشی برای هر Event، تقریباً همیشه از `EventHandler<TEventArgs>` استفاده کنید. این کار به استانداردسازی کد و استفاده از قراردادهای رایج در .NET کمک می‌کند. `EventArgs.Empty` را برای Eventهایی استفاده کنید که نیازی به ارسال اطلاعات اضافی ندارند.
  3. پارامتر `sender` و `e`: همیشه دو پارامتر `object sender` و `EventArgs e` (یا `TEventArgs`) را در Event Handler خود بگنجانید. `sender` ارجاع به شیئی است که Event را ایجاد کرده است، و `e` شامل داده‌های خاص Event است.
  4. Null-checking قبل از فراخوانی: قبل از فراخوانی یک Event، همیشه بررسی کنید که آیا هیچ مشترکی به آن متصل شده است یا خیر (چک کردن برای null). اگر هیچ مشترکی وجود نداشته باشد و شما Event را فراخوانی کنید، یک `NullReferenceException` رخ خواهد داد. `EventName?.Invoke(this, e);` راه امن و مدرن انجام این کار است.
  5. متد `OnEventName`: یک متد `protected virtual void OnEventName(TEventArgs e)` برای فراخوانی Event تعریف کنید. این الگو به کلاس‌های مشتق شده اجازه می‌دهد تا Eventها را در چارچوب معماری چند لایه فراخوانی کنند و امکان سفارشی‌سازی رفتار Event را فراهم می‌آورد.
  6. Thread Safety: در محیط‌های چندنخی (Multi-threaded)، اضافه/حذف مشترکین از یک Event و فراخوانی آن نیاز به ملاحظات Thread Safety دارد. بعداً به این موضوع خواهیم پرداخت.
  7. کوتاه نگه داشتن Event Handlerها: Event Handlerها باید تا حد امکان سریع و سبک باشند. عملیات‌های طولانی‌مدت یا Block کننده باید به صورت ناهمگام (Asynchronously) انجام شوند تا رابط کاربری یا عملکرد کلی برنامه تحت تأثیر قرار نگیرد.

3. پیاده‌سازی رویدادهای سفارشی با EventArgs

در بسیاری از سناریوها، Eventها نیاز به ارسال اطلاعات بیشتری به مشترکین خود دارند تا صرفاً این که یک Event رخ داده است. برای این منظور، C# به شما اجازه می‌دهد کلاس‌های EventArgs سفارشی خود را تعریف کنید. این کلاس‌ها باید از کلاس پایه `System.EventArgs` ارث‌بری کنند.

هدف اصلی از `EventArgs` سفارشی، کپسوله‌سازی هرگونه داده‌ای است که مربوط به Event خاصی است و مشترکین برای واکنش مناسب به آن Event به آن نیاز دارند. به عنوان مثال، اگر یک Event “تغییر دما” (TemperatureChanged) دارید، ممکن است بخواهید دمای جدید، دمای قبلی، و حتی واحد دما را به مشترکین ارسال کنید.

مراحل پیاده‌سازی Custom EventArgs:

  1. تعریف کلاس `TEventArgs`: یک کلاس جدید تعریف کنید که از `System.EventArgs` ارث‌بری کند.
  2. افزودن خصوصیات (Properties): خصوصیات مورد نیاز برای انتقال اطلاعات مربوط به Event را به این کلاس اضافه کنید. این خصوصیات معمولاً از نوع فقط خواندنی (read-only) هستند و در سازنده (constructor) مقداردهی می‌شوند.
  3. استفاده از `EventHandler<TEventArgs>`: Event خود را با استفاده از `EventHandler<TEventArgs>` اعلام کنید.
  4. ایجاد نمونه `TEventArgs` و ارسال آن: در متد `OnEventName` (یا هر جایی که Event را فراخوانی می‌کنید)، یک نمونه از کلاس `TEventArgs` سفارشی خود را ایجاد کرده و آن را به متد `Invoke` (یا `OnEventName`) ارسال کنید.

مثال کد: Custom Event برای یک دماسنج (TemperatureChangeEventArgs)


using System;
using System.Threading;

// 1. تعریف کلاس EventArgs سفارشی
public class TemperatureChangeEventArgs : EventArgs
{
    public double OldTemperature { get; }
    public double NewTemperature { get; }
    public DateTime Timestamp { get; }

    public TemperatureChangeEventArgs(double oldTemp, double newTemp)
    {
        OldTemperature = oldTemp;
        NewTemperature = newTemp;
        Timestamp = DateTime.Now;
    }
}

// کلاس ناشر: سنسور دما
public class TemperatureSensor
{
    private double _currentTemperature;

    public event EventHandler<TemperatureChangeEventArgs> TemperatureChanged;

    public TemperatureSensor(double initialTemp)
    {
        _currentTemperature = initialTemp;
        Console.WriteLine($"Sensor initialized with temperature: {_currentTemperature}°C");
    }

    // متد محافظت شده برای فراخوانی Event
    protected virtual void OnTemperatureChanged(TemperatureChangeEventArgs e)
    {
        // Null-conditional operator (?.Invoke) به طور خودکار null check را انجام می دهد
        TemperatureChanged?.Invoke(this, e);
    }

    public void SetTemperature(double newTemp)
    {
        if (newTemp != _currentTemperature)
        {
            Console.WriteLine($"\nTemperature change detected from {_currentTemperature}°C to {newTemp}°C.");
            TemperatureChangeEventArgs args = new TemperatureChangeEventArgs(_currentTemperature, newTemp);
            _currentTemperature = newTemp;
            OnTemperatureChanged(args); // فراخوانی Event
        }
    }
}

// کلاس مشترک: کنترل کننده بخاری
public class HeaterController
{
    public void HandleTemperatureChange(object sender, TemperatureChangeEventArgs e)
    {
        Console.WriteLine($"Heater: Temperature changed at {e.Timestamp}. Old: {e.OldTemperature}°C, New: {e.NewTemperature}°C.");
        if (e.NewTemperature < 20)
        {
            Console.WriteLine("Heater: Temperature is low, turning on heater.");
        }
        else if (e.NewTemperature > 25)
        {
            Console.WriteLine("Heater: Temperature is high, turning off heater.");
        }
        else
        {
            Console.WriteLine("Heater: Temperature is optimal.");
        }
    }
}

// کلاس مشترک: لاگر دما
public class TemperatureLogger
{
    public void LogTemperature(object sender, TemperatureChangeEventArgs e)
    {
        Console.WriteLine($"Logger: Logging temperature change: {e.OldTemperature}°C -> {e.NewTemperature}°C at {e.Timestamp}.");
    }
}

public class CustomEventArgsExample
{
    public static void Main(string[] args)
    {
        TemperatureSensor sensor = new TemperatureSensor(22.0);
        HeaterController heater = new HeaterController();
        TemperatureLogger logger = new TemperatureLogger();

        // مشترک کردن کنترلر و لاگر در Event
        sensor.TemperatureChanged += heater.HandleTemperatureChange;
        sensor.TemperatureChanged += logger.LogTemperature;

        sensor.SetTemperature(18.5); // دما کم می شود
        Thread.Sleep(100);
        sensor.SetTemperature(24.0); // دما افزایش می یابد
        Thread.Sleep(100);
        sensor.SetTemperature(21.0); // دما به حالت بهینه برمی گردد
        Thread.Sleep(100);
        sensor.SetTemperature(28.0); // دما خیلی زیاد می شود

        // لغو اشتراک
        Console.WriteLine("\nUnsubscribing logger...");
        sensor.TemperatureChanged -= logger.LogTemperature;
        sensor.SetTemperature(19.0); // لاگر دیگر پیامی نمایش نمی دهد
    }
}

استفاده از `EventArgs` سفارشی، راهی تمیز و قوی برای انتقال داده‌های مرتبط با Event است. این کار به جداسازی نگرانی‌ها (Separation of Concerns) کمک می‌کند؛ کلاس ناشر فقط مسئول اطلاع‌رسانی است، و کلاس‌های مشترک مسئول واکنش به Event با توجه به داده‌های دریافتی هستند.

4. الگوهای پیشرفته در Eventها و Delegateها

با تسلط بر اصول بنیادین، می‌توانیم به بررسی سناریوهای پیشرفته‌تر و چالش‌هایی بپردازیم که در هنگام استفاده از Delegateها و Eventها در برنامه‌های پیچیده‌تر ممکن است با آن‌ها روبرو شوید.

Event Handling ناهمگام (Asynchronous Event Handling)

در برنامه‌های مدرن، به خصوص برنامه‌هایی با رابط کاربری گرافیکی (GUI) یا برنامه‌های Server-side، بسیار مهم است که Event Handlerها رابط کاربری را بلوکه نکنند یا عملکرد کلی سیستم را کاهش ندهند. اگر یک Event Handler عملیات طولانی‌مدتی (مانند دسترسی به دیسک، عملیات شبکه یا محاسبات سنگین) انجام دهد، می‌تواند باعث کندی یا عدم پاسخگویی برنامه شود.

برای حل این مشکل، می‌توان از `async/await` در Event Handlerها استفاده کرد. این کار اجازه می‌دهد که عملیات طولانی‌مدت به صورت ناهمگام اجرا شوند، بدون اینکه Thread اصلی را بلوکه کنند.

ملاحظات در Event Handling ناهمگام:

  • عدم بازگشت `void` در Event Handlerهای `async`: اکثر Event Handlerها دارای امضای `void` هستند، یعنی هیچ مقداری را برنمی‌گردانند. اگر یک Event Handler ناهمگام `async void` باشد و یک Exception در آن رخ دهد، آن Exception مستقیماً در Thread اصلی (که Event را فراخوانی کرده) پخش می‌شود و ممکن است برنامه را Crash کند. برای مدیریت بهتر خطاها، بهتر است از `async Task` برای متدهای ناهمگام استفاده کرد، اما Event Handlerها معمولاً باید `void` باشند. بنابراین، مدیریت استثناها درون خود Event Handler `async void` بسیار حیاتی است.
  • ترتیب اجرای Handlerها: در Delegateهای Multicast (که Eventها نیز هستند)، ترتیب فراخوانی Handlerها تضمین شده است (به ترتیب اضافه شدن). اما در Event Handlerهای ناهمگام، این تضمین فقط برای شروع متدهاست، نه برای تکمیل آن‌ها. یعنی، متد بعدی شروع به کار می‌کند در حالی که متدهای قبلی هنوز در حال تکمیل ناهمگام خود هستند.

مثال کد: async event handler


using System;
using System.Threading;
using System.Threading.Tasks;

public class DataProcessor
{
    public event EventHandler<string> DataReady;

    protected virtual void OnDataReady(string data)
    {
        DataReady?.Invoke(this, data);
    }

    public void SimulateDataFetch()
    {
        Console.WriteLine($"[Publisher] Data fetch started on Thread ID: {Thread.CurrentThread.ManagedThreadId}");
        // شبیه سازی عملیات طولانی مدت
        Task.Delay(100).Wait();
        string data = "Some important data";
        OnDataReady(data);
        Console.WriteLine($"[Publisher] Data fetch completed on Thread ID: {Thread.CurrentThread.ManagedThreadId}");
    }
}

public class AsyncEventHandlerExample
{
    public static async void ProcessDataAsync(object sender, string data)
    {
        Console.WriteLine($"  [Handler 1] Processing data '{data}' started on Thread ID: {Thread.CurrentThread.ManagedThreadId}");
        try
        {
            await Task.Delay(2000); // شبیه سازی عملیات IO ناهمگام
            Console.WriteLine($"  [Handler 1] Data '{data}' processed successfully on Thread ID: {Thread.CurrentThread.ManagedThreadId}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"  [Handler 1] Error processing data: {ex.Message}");
        }
    }

    public static async void LogDataAsync(object sender, string data)
    {
        Console.WriteLine($"    [Handler 2] Logging data '{data}' started on Thread ID: {Thread.CurrentThread.ManagedThreadId}");
        try
        {
            await Task.Delay(1000); // شبیه سازی عملیات IO ناهمگام دیگر
            Console.WriteLine($"    [Handler 2] Data '{data}' logged successfully on Thread ID: {Thread.CurrentThread.ManagedThreadId}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"    [Handler 2] Error logging data: {ex.Message}");
        }
    }

    public static void Main(string[] args)
    {
        DataProcessor processor = new DataProcessor();
        processor.DataReady += ProcessDataAsync;
        processor.DataReady += LogDataAsync;

        Console.WriteLine("Main thread started.");
        processor.SimulateDataFetch(); // این متد Publisher بلاک نمی شود.
        Console.WriteLine("Main thread continues after data fetch simulation.");

        Console.WriteLine("Waiting for async handlers to complete...");
        // در یک برنامه واقعی UI/Service، این انتظار لازم نیست.
        // اینجا فقط برای اینکه Console قبل از اتمام Handlers بسته نشود.
        Console.ReadKey(); 
    }
}

در مثال بالا، `SimulateDataFetch` به سرعت برمی‌گردد، در حالی که `ProcessDataAsync` و `LogDataAsync` به صورت ناهمگام در پس‌زمینه کار می‌کنند. این به ناشر اجازه می‌دهد تا به سرعت به کار خود ادامه دهد و UI برنامه نیز پاسخگو بماند.

Weak Events: جلوگیری از Memory Leak

یکی از مشکلات رایج در برنامه‌نویسی رویدادمحور، به خصوص در برنامه‌هایی با چرخه حیات طولانی (مانند برنامه‌های دسکتاپ WPF/WinForms) یا اجزای متحرک، “Memory Leak” ناشی از Eventها است. هنگامی که یک مشترک به یک Event از یک ناشر مشترک می‌شود، ناشر یک ارجاع قوی به Event Handler مشترک نگه می‌دارد. اگر مشترک از بین برود (از Scope خارج شود یا دیگر مورد نیاز نباشد) اما اشتراک خود را از Event لغو نکرده باشد، ناشر همچنان یک ارجاع به آن مشترک نگه می‌دارد. این ارجاع قوی از جمع‌آوری زباله (Garbage Collection) مشترک جلوگیری می‌کند، حتی اگر دیگر به آن نیازی نباشد، و منجر به Memory Leak می‌شود.

برای حل این مشکل، می‌توان از الگوی “Weak Event” استفاده کرد. در یک Weak Event، ناشر به جای نگه داشتن یک ارجاع قوی به مشترک، یک ارجاع ضعیف (Weak Reference) نگه می‌دارد. این بدان معناست که اگر هیچ ارجاع قوی دیگری به مشترک وجود نداشته باشد، جمع‌آوری زباله می‌تواند آن را پاک کند، حتی اگر هنوز به Event مشترک باشد.

پیاده‌سازی Weak Events می‌تواند پیچیده باشد، اما برای فریم‌ورک‌هایی مانند WPF، مایکروسافت `WeakEventManager` را برای برخی از سناریوهای رایج فراهم کرده است. در غیر این صورت، ممکن است نیاز به پیاده‌سازی دستی مکانیزم Weak Event داشته باشید که شامل یک “Event Manager” واسطه‌ای می‌شود که ارجاع‌های ضعیف را مدیریت می‌کند.

مفهوم پیاده‌سازی دستی Weak Event (conceptual overview):


// Conceptual example of how a Weak Event might work internally
public class WeakEventManager<TEventArgs>
{
    private List<WeakReference<EventHandler<TEventArgs>>> _handlers = new List<WeakReference<EventHandler<TEventArgs>>>();

    public void AddHandler(EventHandler<TEventArgs> handler)
    {
        _handlers.Add(new WeakReference<EventHandler<TEventArgs>>(handler));
    }

    public void RemoveHandler(EventHandler<TEventArgs> handler)
    {
        // ... Logic to find and remove the specific handler
    }

    public void RaiseEvent(object sender, TEventArgs e)
    {
        // Create a temporary list to avoid issues if handlers are removed during iteration
        List<EventHandler<TEventArgs>> liveHandlers = new List<EventHandler<TEventArgs>>();
        
        // Clean up dead references and collect live ones
        _handlers.RemoveAll(wr => !wr.TryGetTarget(out var h));
        
        foreach (var weakRef in _handlers)
        {
            if (weakRef.TryGetTarget(out var handler))
            {
                liveHandlers.Add(handler);
            }
        }

        foreach (var handler in liveHandlers)
        {
            handler(sender, e);
        }
    }
}

در این الگو، به جای اینکه ناشر مستقیماً `EventHandler` را نگه دارد، یک `WeakReference` به آن نگه می‌دارد. این تضمین می‌کند که GC می‌تواند مشترک را جمع‌آوری کند اگر هیچ ارجاع قوی دیگری به آن وجود نداشته باشد. البته این پیاده‌سازی بسیار ساده‌شده است و در واقعیت پیچیدگی‌های بیشتری دارد.

Thread Safety در Eventها

Eventها، به دلیل ماهیت Multicast خود، می‌توانند در محیط‌های چندنخی مشکلات همزمانی (Concurrency Issues) ایجاد کنند. دو سناریوی اصلی وجود دارد که باید در نظر گرفته شود:

  1. اضافه/حذف مشترکین همزمان: اگر چندین Thread به طور همزمان سعی در اضافه کردن یا حذف کردن Handlers از یک Event داشته باشند، ممکن است لیست Handlers خراب شود یا نتایج غیرقابل پیش‌بینی رخ دهد.
  2. فراخوانی Event در حین تغییر لیست: اگر یک Thread در حال فراخوانی Event باشد (یعنی در حال پیمایش لیست Handlers باشد) و Thread دیگری همزمان یک Handler را اضافه یا حذف کند، این می‌تواند منجر به `NullReferenceException` (اگر یک Handler حذف شود در حالی که در حال فراخوانی است) یا `IndexOutOfRangeException` (اگر لیست تغییر کند) شود.

برای اطمینان از Thread Safety در Eventها، روش‌های مختلفی وجود دارد:

  • استفاده از `lock`: ساده‌ترین راه، قفل کردن (locking) روی یک شیء اختصاصی در هنگام اضافه/حذف Handlers و همچنین در هنگام فراخوانی Event است. این کار تضمین می‌کند که فقط یک Thread در هر زمان به لیست Handlers دسترسی دارد.
  • Snapshotting: یک روش بهتر و کارآمدتر، به خصوص برای فراخوانی Eventها، ایجاد یک کپی (snapshot) از لیست Handlers قبل از فراخوانی است. به این ترتیب، حتی اگر لیست اصلی در حین فراخوانی تغییر کند، Delegate Snapshot شده تحت تأثیر قرار نمی‌گیرد و Exception رخ نمی‌دهد.

مثال کد: Thread-safe event با Snapshotting


using System;
using System.Threading;
using System.Threading.Tasks;

public class SafePublisher
{
    private EventHandler _myEvent;

    // A private object for locking to protect the event field
    private readonly object _eventLock = new object();

    public event EventHandler MyEvent
    {
        add
        {
            lock (_eventLock)
            {
                _myEvent += value;
            }
        }
        remove
        {
            lock (_eventLock)
            {
                _myEvent -= value;
            }
        }
    }

    public void RaiseEvent()
    {
        EventHandler handler;
        lock (_eventLock)
        {
            // Take a snapshot of the event handlers
            handler = _myEvent;
        }

        // Invoke the snapshot outside the lock
        // This ensures the lock is not held during potentially long-running handler execution
        // And handles concurrent changes to _myEvent
        handler?.Invoke(this, EventArgs.Empty);
    }

    public static void Main(string[] args)
    {
        SafePublisher publisher = new SafePublisher();

        // Simulate multiple threads adding/removing handlers and raising event
        Task.Run(() =>
        {
            publisher.MyEvent += (s, e) => Console.WriteLine($"Handler 1 from Thread {Thread.CurrentThread.ManagedThreadId}");
            publisher.RaiseEvent();
        });

        Task.Run(() =>
        {
            publisher.MyEvent += (s, e) => Console.WriteLine($"Handler 2 from Thread {Thread.CurrentThread.ManagedThreadId}");
            publisher.RaiseEvent();
        });

        Task.Run(() =>
        {
            publisher.MyEvent += (s, e) => Console.WriteLine($"Handler 3 from Thread {Thread.CurrentThread.ManagedThreadId}");
            publisher.MyEvent -= (s, e) => Console.WriteLine($"Handler 1 from Thread {Thread.CurrentThread.ManagedThreadId}"); // This won't work for anonymous methods
            publisher.RaiseEvent();
        });

        // Keep console open
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}

نکته مهم: در مثال بالا، برای حذف کردن یک Delegate ناشناس (مثل Lambda)، باید یک ارجاع به همان Lambda را نگهداری کنید. یعنی برای `publisher.MyEvent -= (s, e) => Console.WriteLine(…)`، اگر `+=` با یک Lambda جدید بوده باشد، این `remove` کار نخواهد کرد زیرا هر Lambda یک نمونه Delegate جدید ایجاد می‌کند. برای حل این مشکل، باید Delegateها را به یک متد نامگذاری شده ارجاع دهید یا ارجاع به Lambda را ذخیره کنید.


// Proper way to add/remove lambda for thread safety example
EventHandler handler1 = (s, e) => Console.WriteLine($"Handler 1 from Thread {Thread.CurrentThread.ManagedThreadId}");
EventHandler handler2 = (s, e) => Console.WriteLine($"Handler 2 from Thread {Thread.CurrentThread.ManagedThreadId}");

publisher.MyEvent += handler1;
publisher.MyEvent += handler2;

// later
publisher.MyEvent -= handler1;

روش Snapshotting با `lock` بهترین روش برای مدیریت Thread Safety در Eventها است، زیرا هم از تداخل در لیست مشترکین جلوگیری می‌کند و هم تضمین می‌کند که لیست در حین فراخوانی پایدار است.

5. مقایسه و انتخاب: Delegateها در مقابل رابط‌ها (Interfaces) برای Callbacks

در برنامه‌نویسی C#، هم Delegateها و هم رابط‌ها (Interfaces) می‌توانند برای پیاده‌سازی مکانیزم CallBack استفاده شوند. هر دو ابزاری برای ایجاد انعطاف‌پذیری و جداسازی نگرانی‌ها در طراحی نرم‌افزار هستند، اما در سناریوهای مختلفی برتری دارند. انتخاب بین Delegate و Interface به نیازهای خاص معماری شما بستگی دارد.

شباهت‌ها:

  • Callback Mechanism: هر دو به یک شیء اجازه می‌دهند تا متدی را در شیء دیگر فراخوانی کند، بدون اینکه وابستگی مستقیمی به نوع آن شیء داشته باشد.
  • Decoupling: هر دو به کاهش وابستگی (coupling) بین اجزای نرم‌افزار کمک می‌کنند، زیرا ناشر نیازی به دانستن نوع دقیق مشترک ندارد.

تفاوت‌ها و سناریوهای مناسب:

Delegateها:

تعریف: یک اشاره‌گر تابع امن نوع که به متدها ارجاع می‌دهد.

ویژگی‌ها:

  • Single Method Callback: بهترین گزینه برای زمانی که نیاز به ارجاع به یک متد منفرد با امضای خاص دارید.
  • Multicast Capability: قابلیت Multicast دارند، یعنی یک Delegate می‌تواند به چندین متد ارجاع دهد. این ویژگی برای Eventها ایده‌آل است، زیرا چندین مشترک می‌توانند به یک Event واحد واکنش نشان دهند.
  • Flexibility (Runtime): می‌توانند در زمان اجرا (runtime) به متدهای مختلفی (چه استاتیک و چه نمونه) اشاره کنند.
  • Lambda Expressions & Anonymous Methods: به خوبی با Lambda Expressionها و متدهای ناشناس کار می‌کنند و امکان تعریف Callbackها را به صورت Inline و کوتاه فراهم می‌کنند.
  • Stateful Callbacks: با استفاده از Closures، Delegateها می‌توانند به داده‌های محیطی که در آن تعریف شده‌اند دسترسی داشته باشند (Stateful callbacks).
  • Type Safety (Signature): فقط بر اساس امضای متد (بازگشتی و پارامترها) Type-safe هستند.

سناریوهای کاربرد Delegateها:

  • Events: کاربرد اصلی و رایج‌ترین آن‌ها.
  • LINQ: برای تعریف Predicateها، Selectors و دیگر عملیات‌های Query.
  • Asynchronous Operations: برای تعریف Callbacks در عملیات‌های ناهمگام.
  • Custom Callbacks: زمانی که می‌خواهید رفتار خاصی را به یک متد تزریق کنید (Strategy Pattern ساده).

رابط‌ها (Interfaces):

تعریف: یک قرارداد برای یک مجموعه از متدها، خصوصیات یا رویدادها که یک کلاس باید آن‌ها را پیاده‌سازی کند.

ویژگی‌ها:

  • Contract for Multiple Methods: زمانی استفاده می‌شوند که می‌خواهید یک قرارداد برای مجموعه‌ای از متدها (نه فقط یک متد) تعریف کنید.
  • Compile-time Type Safety: Type-safety بسیار قوی در زمان کامپایل ارائه می‌دهند، زیرا کامپایلر اطمینان حاصل می‌کند که کلاس پیاده‌سازی کننده تمامی اعضای اینترفیس را پیاده‌سازی کرده است.
  • Polymorphism: امکان Polymorphism قوی را فراهم می‌کنند و پایه و اساس بسیاری از الگوهای طراحی شیءگرا هستند (Dependency Injection، Strategy Pattern پیچیده).
  • Explicit Implementation: امکان پیاده‌سازی صریح (Explicit) اعضای اینترفیس را فراهم می‌کنند تا از تداخل نام‌ها جلوگیری شود.

سناریوهای کاربرد Interfaceها:

  • Plugin Architectures: برای تعریف قراردادهایی که پلاگین‌ها باید رعایت کنند.
  • Dependency Inversion Principle: برای ایجاد وابستگی به انتزاعات به جای جزئیات (Dependency Injection).
  • Strategy Pattern (Complex): برای پیاده‌سازی رفتارهای مختلف که می‌توانند به صورت پویا تغییر کنند.
  • Design by Contract: برای تعریف قابلیت‌ها و رفتارهایی که کلاس‌ها باید داشته باشند.
  • Testing (Mocking/Stubbing): بسیار مفید برای Unit Testing از طریق Mocking و Stubbing.

جمع‌بندی انتخاب:

  • Delegateها را انتخاب کنید زمانی که:
    • نیاز به ارجاع به یک متد منفرد دارید.
    • نیاز به قابلیت Multicast (چندین مشترک برای یک رویداد) دارید.
    • متد Callback ساده و بدون نیاز به اطلاعات Context زیادی است.
    • از Lambda Expressions برای تعریف Callbackها به صورت Inline استفاده می‌کنید.
  • رابط‌ها را انتخاب کنید زمانی که:
    • نیاز به تعریف یک “قرارداد” (Contract) برای مجموعه‌ای از متدها، خصوصیات یا Eventها دارید.
    • می‌خواهید قابلیت‌های خاصی را به یک کلاس اضافه کنید که آن را از سایر کلاس‌ها متمایز کند.
    • به Polymorphism قوی و امکان تعویض پیاده‌سازی‌ها در زمان اجرا نیاز دارید.
    • در حال پیاده‌سازی الگوهای طراحی شیءگرای پیچیده (مانند Strategy، Observer، Dependency Injection) هستید.
    • قابلیت تست‌پذیری (Testability) و امکان Mocking کلاس‌ها برایتان مهم است.

در بسیاری از موارد، Delegateها و Interfaceها می‌توانند مکمل یکدیگر باشند. به عنوان مثال، یک Interface می‌تواند یک Event را تعریف کند (که خود از Delegateها استفاده می‌کند)، یا یک متد در یک Interface می‌تواند یک Delegate را به عنوان پارامتر بپذیرد. درک نقاط قوت هر کدام به شما کمک می‌کند تا معماری‌های نرم‌افزاری قوی‌تر و منعطف‌تری طراحی کنید.

6. الگوهای طراحی رویدادمحور و کاربردهای عملی

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

الگوهای طراحی مرتبط:

Observer Pattern (الگوی مشاهده‌گر)

Observer Pattern یک الگوی طراحی رفتاری است که در آن یک شیء (به نام Subject یا Observable) لیستی از وابستگان خود (به نام Observers) را نگهداری می‌کند و به طور خودکار به آن‌ها اطلاع می‌دهد، معمولاً با فراخوانی یکی از متدهایشان، هنگامی که حالت آن تغییر می‌کند. این الگو بر اساس اصل Publish-Subscribe کار می‌کند و Eventها در C# پیاده‌سازی بومی از این الگو را فراهم می‌کنند.

  • Subject/Observable: کلاسی است که Event را اعلام می‌کند (ناشر).
  • Observer: کلاسی است که به Event مشترک می‌شود (مشترک).

این الگو به جداسازی وابستگی بین ناشر و مشترکین کمک می‌کند و آن‌ها را قادر می‌سازد به طور مستقل از یکدیگر توسعه یابند.

Command Pattern (الگوی فرمان)

Command Pattern یک الگوی طراحی رفتاری است که هدف آن کپسوله‌سازی یک درخواست (Request) به عنوان یک شیء است. این کار به شما امکان می‌دهد پارامترهای مختلفی را به متدها پاس دهید، درخواست‌ها را در صف قرار دهید یا لاگ کنید، و عملیات را Undo کنید. Delegateها در C# می‌توانند به سادگی برای پیاده‌سازی Command Pattern استفاده شوند، جایی که هر فرمان به عنوان یک Delegate (معمولاً `Action` یا `Func`) نمایش داده می‌شود که عملیات خاصی را انجام می‌دهد.


// Simple Command Pattern with Delegates
public class CalculatorCommand
{
    private Action _execute;
    private Action _undo;

    public CalculatorCommand(Action execute, Action undo)
    {
        _execute = execute;
        _undo = undo;
    }

    public void Execute() => _execute?.Invoke();
    public void Undo() => _undo?.Invoke();
}

public class CommandExample
{
    private static int _currentValue = 0;

    public static void Main(string[] args)
    {
        List<CalculatorCommand> commands = new List<CalculatorCommand>();

        Action add10 = () => { _currentValue += 10; Console.WriteLine($"Added 10. Current: {_currentValue}"); };
        Action undoAdd10 = () => { _currentValue -= 10; Console.WriteLine($"Undo Add 10. Current: {_currentValue}"); };
        commands.Add(new CalculatorCommand(add10, undoAdd10));

        Action subtract5 = () => { _currentValue -= 5; Console.WriteLine($"Subtracted 5. Current: {_currentValue}"); };
        Action undoSubtract5 = () => { _currentValue += 5; Console.WriteLine($"Undo Subtract 5. Current: {_currentValue}"); };
        commands.Add(new CalculatorCommand(subtract5, undoSubtract5));

        foreach (var cmd in commands)
        {
            cmd.Execute();
        }

        Console.WriteLine("\nUndoing all commands...");
        commands.Reverse();
        foreach (var cmd in commands)
        {
            cmd.Undo();
        }
    }
}

Message Bus / Event Aggregator

در برنامه‌های بزرگتر و ماژولار، به خصوص در معماری‌های MVVM (Model-View-ViewModel)، اغلب نیاز به ارتباط بین ماژول‌ها یا ViewModelهای مختلف وجود دارد که مستقیماً به یکدیگر ارجاع نمی‌دهند. یک Message Bus یا Event Aggregator یک سرویس مرکزی را فراهم می‌کند که ماژول‌ها می‌توانند Eventها را در آن منتشر کنند و سایر ماژول‌ها به آن Eventها مشترک شوند. این الگو وابستگی‌ها را به شدت کاهش می‌دهد و به تست‌پذیری و نگهداری‌پذیری کد کمک می‌کند. در قلب این الگو، Delegateها و Eventها به عنوان مکانیزم‌های اصلی برای انتشار و اشتراک پیام‌ها استفاده می‌شوند.

کاربردهای عملی:

UI Programming (WPF, WinForms)

برنامه‌های رابط کاربری گرافیکی (GUI) نمونه‌های بارزی از برنامه‌نویسی رویدادمحور هستند. هر تعامل کاربر (کلیک دکمه، حرکت ماوس، تایپ کیبورد) یک Event را ایجاد می‌کند که توسط کنترل‌ها (مانند Button، TextBox) منتشر می‌شود. توسعه‌دهندگان به این Eventها مشترک می‌شوند و Event Handlerها را برای واکنش به تعاملات کاربر پیاده‌سازی می‌کنند.


// در WinForms یا WPF
// Button btn = new Button();
// btn.Click += new EventHandler(MyButton_Click);
// void MyButton_Click(object sender, EventArgs e) { /* reaction to click */ }

همچنین، در معماری MVVM، Eventها معمولاً برای ارتباط View با ViewModel استفاده می‌شوند (اگرچه “Commands” اغلب جایگزین مستقیم Eventها در View می‌شوند، اما در نهایت Commandها نیز از Delegateها برای اجرای عملیات استفاده می‌کنند).

Service Communication و Microservices

اگرچه Eventها در C# بیشتر برای ارتباطات درون‌فرآیندی (In-process communication) استفاده می‌شوند، اما مفهوم برنامه‌نویسی رویدادمحور به سیستم‌های توزیع‌شده و معماری Microservices نیز گسترش یافته است. در این زمینه، “Event” به معنای یک پیام (Message) است که توسط یک سرویس منتشر می‌شود و توسط سرویس‌های دیگر مصرف می‌شود (معمولاً از طریق Message Brokerها مانند Kafka، RabbitMQ). در حالی که مکانیزم‌های ارتباطی پروتکل‌های شبکه هستند، فلسفه Publish-Subscribe همچنان پابرجا است و مفاهیم مشابهی با Delegateها و Eventها در C# دارند.

Custom Event Sourcing / Change Data Capture

در معماری‌های Event Sourcing، تمام تغییرات وضعیت برنامه به عنوان دنباله‌ای از Eventها ذخیره می‌شوند. هر Event یک واقعیت (Fact) درباره چیزی است که در سیستم رخ داده است. این Eventها قابل پخش مجدد هستند و می‌توانند برای بازسازی وضعیت برنامه یا برای تحلیل‌های تاریخی استفاده شوند. اگرچه پیاده‌سازی یک سیستم Event Sourcing کامل پیچیده‌تر است، اما مفاهیم پایه Delegateها و Eventها در C# می‌توانند برای مدل‌سازی و انتشار این Eventهای داخلی در یک دامین خاص مورد استفاده قرار گیرند.

Callbacks در APIها

بسیاری از کتابخانه‌ها و فریم‌ورک‌ها از Delegateها به عنوان Callbacks برای فراهم کردن نقاط توسعه‌پذیری (Extension Points) استفاده می‌کنند. به عنوان مثال، در C#، متدهای `Array.Sort` یا `List.Sort` می‌توانند یک Delegate از نوع `Comparison` را بپذیرند تا منطق مرتب‌سازی سفارشی را فراهم کنند. این امکان می‌دهد تا رفتار متدها را بدون تغییر در کد منبع آن‌ها، سفارشی‌سازی کنید.


List<int> numbers = new List<int> { 5, 1, 4, 2, 8 };
numbers.Sort((x, y) => y.CompareTo(x)); // مرتب سازی نزولی با Lambda

همچنین، استفاده از Delegateها برای Hookهای (قلاب‌های) سفارشی در فریم‌ورک‌ها رایج است، جایی که کاربران می‌توانند کد خود را برای اجرا در مراحل خاصی از چرخه حیات فریم‌ورک تزریق کنند.

در مجموع، Delegateها و Eventها سنگ بنای برنامه‌نویسی رویدادمحور در C# هستند. درک این الگوها و کاربردهای عملی آن‌ها، به شما امکان می‌دهد نرم‌افزارهای مقیاس‌پذیر، قابل نگهداری و پاسخگوتر طراحی و پیاده‌سازی کنید.

7. چالش‌ها و بهترین روش‌ها در کار با Delegateها و Eventها

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

چالش‌ها:

Memory Leaks (نشت حافظه)

همانطور که در بخش Weak Events اشاره شد، اگر یک مشترک به یک Event مشترک شود و هرگز اشتراک خود را لغو نکند (unsubscribe)، ناشر یک ارجاع قوی به Event Handler مشترک نگه می‌دارد. این امر از جمع‌آوری زباله (Garbage Collection) مشترک جلوگیری می‌کند و منجر به Memory Leak می‌شود، به خصوص اگر مشترک یک شیء با عمر کوتاه باشد و ناشر یک شیء با عمر طولانی.

راه حل: همیشه اشتراک Eventها را لغو کنید، به خصوص برای اشیایی که عمر آن‌ها از ناشر کوتاه‌تر است. از الگوی `IDisposable` برای Clean-up کردن منابع و لغو اشتراک در متد `Dispose()` استفاده کنید. برای سناریوهای پیچیده، Weak Events را در نظر بگیرید.

Order of Execution in Multicast Delegates (ترتیب اجرای Delegateهای Multicast)

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

راه حل: از Delegateهای Multicast/Events انتظار نداشته باشید که ترتیب اجرای منطقی پیچیده‌ای را حفظ کنند. Event Handlerها باید تا حد امکان مستقل از یکدیگر باشند. اگر ترتیب اجرا حیاتی است، ممکن است الگوی طراحی دیگری (مانند Chain of Responsibility) یا مکانیزم فراخوانی ترتیبی صریح مورد نیاز باشد.

Exception Handling in Event Handlers (مدیریت استثناها در Event Handlerها)

اگر یک Event Handler یک Exception پرتاب کند، این Exception می‌تواند زنجیره فراخوانی سایر Handlerها را متوقف کند. به عبارت دیگر، Eventهای بعدی در لیست فراخوانی نخواهند شد، و این می‌تواند منجر به رفتار غیرمنتظره در برنامه شود.

راه حل: هر Event Handler باید Exceptionهای خود را مدیریت کند (با استفاده از بلوک `try-catch`) تا از تأثیرگذاری بر سایر Handlerها و ناشر جلوگیری کند. ناشر نیز می‌تواند یک بلوک `try-catch` در اطراف فراخوانی `handler?.Invoke()` داشته باشد تا Exceptionهای Handlers را در یک حلقه جداگانه مدیریت کند، یا از `GetInvocationList()` برای فراخوانی Handlerها به صورت جداگانه استفاده کند.


protected virtual void OnMyEvent(EventArgs e)
{
    EventHandler handler = _myEvent; // Snapshot
    if (handler != null)
    {
        foreach (EventHandler del in handler.GetInvocationList())
        {
            try
            {
                del.Invoke(this, e);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error in event handler: {ex.Message}");
                // Log the exception, but allow other handlers to run
            }
        }
    }
}

Thread Safety (امنیت نخ)

همانطور که قبلاً بحث شد، دستکاری لیست مشترکین Event (افزودن یا حذف) و فراخوانی آن در یک محیط چندنخی بدون محافظت مناسب می‌تواند منجر به Condition Raceها و Exceptionها شود.

راه حل: همیشه از مکانیزم‌های Thread Safety مانند `lock` یا Snapshotting در Event Accessorها (یعنی `add` و `remove` بخش‌های Event) و هنگام فراخوانی Event استفاده کنید. الگوی Snapshotting بهترین رویکرد برای اکثر سناریوها است.

Over-use of Events (استفاده بیش از حد از رویدادها)

استفاده بیش از حد از Eventها می‌تواند منجر به معماری‌های پیچیده و دشوار برای اشکال‌زدایی (Debugging) شود، جایی که جریان کنترل برنامه به سختی قابل پیگیری است (مشکلی که به “Spaghetti Code” یا “Callback Hell” معروف است). وابستگی‌های ضمنی بین ناشران و مشترکین می‌تواند پنهان بماند و درک رفتار کلی سیستم را دشوار کند.

راه حل: Eventها را با دقت و فقط در جایی که نیاز به یک مدل ارتباطی Publisher-Subscriber دارید، استفاده کنید. برای Callbacksهای ساده یا تزریق رفتار، Delegateهای مستقیم (مانند `Action` و `Func`) یا الگوهای طراحی دیگر (مانند Strategy Pattern) ممکن است مناسب‌تر باشند. همیشه به دنبال تعادل بین انعطاف‌پذیری و پیچیدگی باشید.

بهترین روش‌ها (Best Practices):

  1. Null-check events before raising (?.Invoke): همیشه قبل از فراخوانی یک Event، آن را برای `null` بررسی کنید. استفاده از اپراتور `?.Invoke()` بهترین روش برای انجام این کار است.
  2. Keep event handlers fast: Event Handlerها باید سریع و غیربلوک‌کننده باشند. عملیات‌های طولانی‌مدت را به صورت ناهمگام (Asynchronously) اجرا کنید تا UI یا عملکرد برنامه مختل نشود.
  3. Use `EventHandler<TEventArgs>`: برای تعریف Eventها از `EventHandler<TEventArgs>` استفاده کنید تا Eventها استاندارد و انعطاف‌پذیر باشند. برای Eventهایی بدون داده خاص، از `EventArgs.Empty` استفاده کنید.
  4. Consider Weak Events for long-lived subscribers: برای جلوگیری از Memory Leak در برنامه‌های طولانی‌مدت، به خصوص زمانی که ناشر عمر طولانی‌تری از مشترکین دارد، الگوی Weak Event را در نظر بگیرید.
  5. Document event behavior: به وضوح مستند کنید که Event چه زمانی فراخوانی می‌شود و چه اطلاعاتی از طریق `EventArgs` منتقل می‌کند. این به مشترکین کمک می‌کند تا به درستی به Event واکنش نشان دهند.
  6. Avoid exposing public delegates instead of events: هرگز Delegateها را به صورت `public` بدون استفاده از `event` keyword در معرض عموم قرار ندهید. استفاده از `event` کپسوله‌سازی و کنترل دسترسی را تضمین می‌کند.
  7. Subscription/Unsubscription management: به مدیریت چرخه عمر اشتراک‌ها و لغو اشتراک‌ها توجه ویژه داشته باشید. اگر یک کلاس به Eventهای شیء دیگری مشترک می‌شود، باید مسئول لغو اشتراک خود در زمان مناسب باشد (مثلاً در متد `Dispose()` یا در یک متد `Close()`).
  8. Pass `sender` correctly: همیشه شیء ناشر را به عنوان پارامتر `sender` ارسال کنید تا مشترکین بتوانند در صورت لزوم به منبع Event دسترسی پیدا کنند.

با رعایت این بهترین روش‌ها و آگاهی از چالش‌های رایج، می‌توانید از Delegateها و Eventها به طور مؤثرتری در پروژه‌های C# خود استفاده کنید و سیستم‌هایی قوی، پایدار و قابل نگهداری بسازید.

نتیجه‌گیری

Delegateها و Eventها دو مفهوم بنیادین و بی‌نهایت قدرتمند در زبان برنامه‌نویسی C# هستند که درک عمیق آن‌ها برای هر توسعه‌دهنده جدی این زبان ضروری است. همانطور که در این مقاله به تفصیل بررسی شد، Delegateها به عنوان اشاره‌گرهای تابع امن نوع عمل می‌کنند و امکان ارجاع به متدها و انتقال آن‌ها به عنوان آرگومان را فراهم می‌آورند، در حالی که Eventها بر پایه Delegateها بنا شده‌اند و مکانیزمی امن و استاندارد برای پیاده‌سازی الگوی طراحی Publisher-Subscriber (ناشر-مشترک) ارائه می‌دهند.

ما آموختیم که چگونه Delegateها را تعریف، نمونه‌سازی و فراخوانی کنیم، و با قدرت Multicast Delegateها و همچنین Delegateهای Built-in مانند `Action` و `Func` آشنا شدیم که کد را کوتاه‌تر و خواناتر می‌سازند. Lambda Expressionها نیز به عنوان ابزاری انقلابی در ساده‌سازی تعریف CallBackها و کار با Delegateها معرفی شدند.

در ادامه، به جزئیات Eventها پرداختیم، از `event` keyword و نقش `EventHandler` و `EventHandler<TEventArgs>` گرفته تا اهمیت `EventArgs` سفارشی برای انتقال اطلاعات خاص Event. در بخش‌های پیشرفته‌تر، چالش‌هایی مانند Event Handling ناهمگام، جلوگیری از Memory Leak با Weak Events، و اطمینان از Thread Safety در Eventها را مورد بررسی قرار دادیم و راه‌حل‌های عملی برای آن‌ها ارائه کردیم.

همچنین، با مقایسه Delegateها و رابط‌ها (Interfaces) برای سناریوهای Callback، دیدگاه جامع‌تری در مورد انتخاب ابزار مناسب در طراحی نرم‌افزار به دست آوردیم. نهایتاً، نگاهی به الگوهای طراحی رویدادمحور مانند Observer Pattern و کاربردهای عملی این مفاهیم در برنامه‌نویسی UI، ارتباطات سرویسی و APIهای توسعه‌پذیر داشتیم. در هر گام، بر بهترین روش‌ها و چالش‌های رایج تأکید شد تا شما بتوانید کدی قوی‌تر، ایمن‌تر و قابل نگهداری‌تر بنویسید.

برنامه‌نویسی رویدادمحور قلب تپنده بسیاری از سیستم‌های مدرن است. با تسلط بر Delegateها و Eventها، شما نه تنها می‌توانید به رویدادهای سیستمی و تعاملات کاربران به شیوه‌ای مؤثر واکنش نشان دهید، بلکه می‌توانید معماری‌هایی را طراحی کنید که انعطاف‌پذیر، قابل مقیاس‌بندی و بسیار پاسخگو هستند. این مفاهیم ابزارهای اساسی در جعبه ابزار هر برنامه‌نویس C# هستند که به شما امکان می‌دهند برنامه‌هایی بسازید که نه تنها عملکردی، بلکه زیبا، کارآمد و قابل توسعه باشند. به شما توصیه می‌شود که با مثال‌های عملی ارائه شده کار کنید و این مفاهیم را در پروژه‌های خود به کار گیرید تا درک خود را عمیق‌تر کنید.

“تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT”

قیمت اصلی 2.290.000 ریال بود.قیمت فعلی 1.590.000 ریال است.

"تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT"

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

ویژگی‌های کلیدی:

بدون نیاز به تجربه قبلی برنامه‌نویسی

زیرنویس فارسی با ترجمه حرفه‌ای

۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان