درس چهاردهم – رخدادها و delegate ها در C#

نکته مهم قبل از مطالعه این درس

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

طی درسهای گذشته، چگونگی ایجاد و پیادسازی انواع مرجعی (Reference Type) را با استفاده از ساختارهای زبان C#، یعنی کلاسها (Class) و واسطها (Interface)، فرا گرفتید. همچنین فرا گرفتید که با استفاده از این انواع مرجعی، میتوانید نمونه‌های جدیدی از اشیاء را ایجاد کرده و نیازهای توسعه نرم‌افزار خود را تامین نمایید. همانطور که تا کنون دیدید، با استفاده از کلاسها قادر به ساخت اشیائی هستید که دارای صفات (Attribute) و رفتارهای (Behavior) خاصی بودند. با استفاده از واسطها، یکسری از صفات و رفتارها را تعریف می‌کردیم تا فرم کلی داشته باشیم و تمام اشیاء خود به پیاده‌سازی این صفا و رفتارها می‌پرداختند. در این درس با یکی دیگر از انواع مرجعی (Reference Type) در زبان C# آشنا خواهید شد.

مقدمه‌ای بر رخداد‌ها و delegate ها

در گذشته، پس از اجرای یک برنامه، برنامه مراحل اجرای خود را مرحله به مرحله اجرا می‌نمود تا به پایان برسد. در صورتیکه نیاز به ارتباط و تراکنش با کاربر نیز وجود داشت، این امر محدود و بسیار کنترل شده صورت می‌گرفت و معمولاً ارتباط کاربر با برنامه تنها پر کردن و یا وارد کردن اطلاعات خاصی در فیلدهایی مشخص بود.

امروزه با پبشرفت کامپیوتر و گسترش تکنولوژیهای برنامه نویسی و با ظهور رابطهای کاربر گرافیکی (GUI) ارتباط بین کاربر و برنامه بسیار گسترش یافته و دیگر این ارتباط محدود به پر کردن یکسری فیلد نیست، بلکه انواع عملیات از سوی کاربر قابل انجام است. انتخاب گزینه‌ای خاص در یک منو، کلیک کردن بر روی دکمه‌ها برای انجام عملیاتی خاص و ... . رهیافتی که امروزه در برنامه‌نویسی مورد استفاده است، تحت عنوان "برنامه‌نویسی بر پایه رخدادها" (Event-Based Programming) شناخته می‌شود. در این رهیافت برنامه همواره منتظر انجام عملی از سوی کاربر می‌ماند و پس از انجام عملی خاص، رخداد مربوط به آن را اجرا می‌نماید. هر عمل کاربر باعث اجرای رخدادی می‌شود. در این میان برخی از رخدادها بدون انجام عملی خاص از سوی کاربر اجرا می‌شوند، همانند رخدادهای مربوط به ساعت سیستم که مرتباً در حال اجرا هستند.

رخدادها (Events) بیان این مفهوم هستند که در صورت اتفاق افتادن عملی در برنامه، کاری باید صورت گیرد. در زبان C# مفاهیم Event و Delegate دو مفهوم بسیار وابسته به یکدیگر هستند و با یکدیگر در تعامل می‌باشند. برای مثال، مواجهه با رخدادها و انجام عمل مورد نظر در هنگام اتفاق افتادن یک رخداد، نیاز به یک event handler دارد تا در زمان بروز رخداد، بتوان به آن مراجعه نمود. Event handler ها در C# معمولاً با delegate ها ساخته می‌شوند.

از delegate ، می‌توان به عنوان یک Callback یاد نمود، بدین معنا که یک کلاس می‌تواند به کلاسی دیگر بگوید : "این عمل خاص را انجام بده و هنگامیکه عملیات را انجام دادی منرا نیز مطلع کن". با استفاده از delegate ها، همچنین می‌توان متدهایی تعریف نمود که تنها در زمان اجرا قابل دسترسی باشند.

Delegate

Delegate ها، یکی دیگر از انواع مرجعی زبان C# هستند که با استفاده از آنها می‌توانید مرجعی به یک متد داشته باشید، بدین معنا که delegate ها، آدرس متدی خاص را در خود نگه میدارند. در صورتیکه قبلاً با زبان C برنامه‌نویسی کرده‌اید، حتماً با این مفهوم آشنایی دارید. در زبان C این مفهوم با اشاره‌گرها (pointer) بیان می‌شود. اما برای افرادی که با زبانهای دیگری برنامه‌نویسی می‌کرده‌اند و با این مفهوم مانوس نیستند، شاید این سوال مطرح شود که چه نیازی به داشتن آدرس یک متد وجود دارد. برای پاسخ به این سوال اندکی باید تامل نمایید.

بطور کلی می‌توان گفت که delegate نوعی است شبیه به متد و همانند آن نیز رفتار می‌کند. در حقیقت delegate انتزاعی (Abstraction) از یک متد است. در برنامه‌نویسی ممکن به شرایطی برخورد کرده باشید که در آنها می‌خواهید عمل خاصی را انجام دهید اما دقیقاً نمی‌دانید که باید چه متد یا شی‌ءای را برای انجام آن عمل خاص مورد استفاده قرار دهید. در برنامه‌های تحت ویندوز این گونه مسائل مشهودتر هستند. برای مثال تصور کنید در برنامه‌ شما، دکمه‌ای قرار دارد که پس از فشار دادن این دکمه توسط کاربر شیءای یا متدی باید فراخوانی شود تا عمل مورد نظر شما بر روی آن انجام گیرد. می‌توان بجای اتصال این دکمه به شیء یا متد خاص، آنرا به یک delegate مرتبط نمود و سپس آن delegate را به متد یا شیء خاصی در هنگام اجرای برنامه متصل نمود.

ابتدا، به نحوه استفاده از متدها توجه نمایید. معمولاً، برای حل مسایل خود الگوریتم‌هایی طراحی می‌نائیم که این الگوریتمهای کارهای خاصی را با استفاده از متدها انجام می‌دهد، ابتدا متغیرهایی مقدار دهی شده و سپس متدی جهت پردازش آنها فراخوانی می‌گردد. حال در نظر بگیرید که به الگوریتمی نیاز دارید که بسیار قابل انعطاف و قابل استفاده مجدد (reusable) باشد و همچنین در شرایط مختلف قابلیت‌های مورد نظر را در اختیار شما قرار دهد. تصور کنید، به الگوریتمی نیاز دارید که از نوعی از ساختمان داده پشتیبانی کند و همچنین می‌خواهید این ساختمان داده را در مواردی مرتب (sort) نمایید، بعلاوه میخواهید تا این ساختمان داده از انواع مختلفی تشکیل شده باشد. اگر انواع موجود در این ساختمان داده را ندانید، چکونه می‌خواهید الگوریتمی جهت مقایسه عناصر آن طراحی کنید؟‌ شاید از یک حلقه if/then/else و یا دستور switch برای این منظور استفاده کنید، اما استفاده از چنین الگوریتمی محدودیتی برای ما ایجاد خواهد کرد. روش دیگر، استفاده از یک واسط است که دارای متدی عمومی باشد تا الگوریتم شما بتواند آنرا فراخوانی نماید، این روش نیز مناسب است، اما چون مبحث ما در این درس delegate ها هستند، می‌خواهیم مسئله را از دیدگاه delegate ها مورد بررسی قرار دهیم. روش حل مسئله با استفاده از آنها اندکی متفاوت است.

روش دیگر حل مسئله آنست که،‌ می‌توان delegate ی را به الگوریتم مورد نظر ارسال نمود و اجازه داد تا متد موجود در آن،‌عمل مورد نظر ما را انجام دهد. چنین عملی در مثال 1-14 نشان داده شده است.

(به صورت مسئله توجه نمایید : میخواهیم مجموعه‌ای از اشیاء را که در یک ساختمان داده قرار گرفته‌اند را مرتب نمائیم. برای اینکار نیاز به مقایسه این اشیاء با یکدیگر داریم. از آنجائیکه این اشیاء از انواع (type) مختلف هستند به الگوریتمی نیاز داریم تا بتواند مقایسه بین اشیاء نظیر را انجام دهد. با استفاده از روشهای معمول این کار امکان پذیر نیست، چراکه نمی‌توان اشیائئ از انواع مختلف را با یکدیگر مقایسه کرد. برای مثال شما نمی‌توانید نوع عددی int را با نوع رشته‌ای string مقایسه نمایید. به همین دلیل با استفاده از delegate ها به حل مسئله پرداخته‌ایم. به مثال زیر به دقت توجه نمایید تا بتوانید به درستی مفهوم delegate را درک کنید.)

مثال 1-14 : اعلان و پیاده‌سازی یک delegate

using System;

// در اینجا اعلان می‌گردد. delegate

public delegate int Comparer(object obj1, object obj2);

public class Name

{

public string FirstName = null;

public string LastName = null;

public Name(string first, string last)

{

FirstName = first;

LastName = last;

}

// delegate method handler

public static int CompareFirstNames(object name1, object name2)

{

string n1 = ((Name)name1).FirstName;

string n2 = ((Name)name2).FirstName;

if (String.Compare(n1, n2) > 0)

{

return 1;

}

else if (String.Compare(n1, n2) < 0)

{

return -1;

}

else

{

return 0;

}

}

public override string ToString()

{

return FirstName + " " + LastName;

}

}

class SimpleDelegate

{

Name[] names = new Name[5];

public SimpleDelegate()

{

names[0] = new Name("Meysam", "Ghazvini");

names[1] = new Name("C#", "Persian");

names[2] = new Name("Csharp", "Persian");

names[3] = new Name("Xname", "Xfamily");

names[4] = new Name("Yname", "Yfamily");

}

static void Main(string[] args)

{

SimpleDelegate sd = new SimpleDelegate();

// delegate ساخت نمونه‌ای جدید از

Comparer cmp = new Comparer(Name.CompareFirstNames);

Console.WriteLine("\nBefore Sort: \n");

sd.PrintNames();

 

sd.Sort(cmp);

Console.WriteLine("\nAfter Sort: \n");

sd.PrintNames();

}

 

public void Sort(Comparer compare)

{

object temp;

for (int i=0; i < names.Length; i++)

{

for (int j=i; j < names.Length; j++)

{

//همانند یک متد استفاده می‌شود compare از

if ( compare(names[i], names[j]) > 0 )

{

temp = names[i];

names[i] = names[j];

names[j] = (Name)temp;

}

}

}

}

public void PrintNames()

{

Console.WriteLine("Names: \n");

foreach (Name name in names)

{

Console.WriteLine(name.ToString());

}

}

}

اولین اعلان در این برنامه، اعلان delegate است. اعلان delegate بسیا رشبیه به اعلان متد است، با این تفاوت که دارای کلمه کلیدی delegate در اعلان است و در انتهای اعلان آن ";" قرار می‌گیرد و نیز پیاده‌سازی ندارد. در زیر اعلان delegate که در مثال 1-14 آورده شده را مشاهده می‌نمایید :

public delegate int Comparer(object obj1, object obj2);

این اعلان، مدل متدی را که delegate می‌تواند به آن اشاره کند را تعریف می‌نماید. متدی که می‌توان از آن بعنوان delegate handler برای Comparer استفاده نمود، هر متدی می‌تواند باشد اما حتماً باید پارامتر اول و دوم آن از نوع object بوده و مقداری از نوع int بازگرداند. در زیر متدی که بعنوان delegate handler در مثال 1-14 مورد استفاده قرار گرفته است، نشان داده شده است :

public static int ComparerFirstNames(object name1, object name2)

{

  …

}

برای استفاده از delegate می‌بایست نمونه‌ای از آن ایجاد کنید. ایجاد نمونه جدید از delegate همانند ایجاد نمونه‌ای جدید از یک کلاس است که به همراه پارامتری جهت تعیین متد delegate handler ایجاد می‌شود :

Comparer cmp = new Comparer(Name.ComparerFirstName);

در مثال 1-14، cmp بعنوان پارامتری برای متد Sort() مورد استفاده قرار گرفته است. به روش ارسال delegate به متد Sort() توجه نمایید :

sd.Sort(cmp);

با استفاده از این تکنیک، هر متد delegate handler به سادگی در زمان اجرا به متد Sort() قابل ارسال است. برای مثال می‌توان handler دیگری با نام CompareLastNames() تعریف کنید، نمونه جدیدی از ‍Comparer را با این پارامتر ایجاد کرده و سپس آنرا به متد Sort() ارسال نمایید.

 

درک سودمندی delegate ها

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

حل مسئله بدون استفاده از delegate

فرض کنید، میخواهید برنامه بنویسید که عمل خاصی را هر یک ثانیه یکبار انجام دهد. یک روش برای انجام چنین عملی آنست که، کار مورد نظر را در یک متد پیاده‌سازی نمایید و سپس با استفاده از کلاسی دیگر، این متد را هر یک ثانیه یکبار فراخوانی نمائیم. به مثال زیر توجه کنید :

class Ticker
{
    
    public void Attach(Subscriber newSubscriber)
    {
        subscribers.Add(newSubscriber);
    }
    public void Detach(Subscriber exSubscriber)
    {
        subscribers.Remove(exSubscriber);
    }
    // هر ثانیه فراخوانی میگردد Notify 
    private void Notify()
    {
        foreach (Subscriber s in subscribers)
        {
            s.Tick();
        }
    }
    
    private ArrayList subscribers = new ArrayList();
}
class Subscriber
{
    public void Tick()
    {
        
    }
}
class ExampleUse
{
    static void Main()
    {
        Ticker pulsed = new Ticker();
        Subscriber worker = new Subscriber();
        pulsed.Attach(worker);
        
    }
}

این مثال مطمئناً کار خواهد کرد اما ایدآل و بهینه نیست. اولین مشکل آنست که کلاس Ticker بشدت وابسته به Subscriber است. به بیان دیگر تنها نمونه‌های جدید کلاس Subscriber می‌توانند از کلاس Ticker استفاده نمایند. اگر در برنامه کلاس دیگری  داشته باشید که بخواهید آن کلاس نیز هر یک ثانیه یکبار اجرا شود، می‌بایست کلاس جدیدی شبیه به Ticker ایجاد کنید. برای بهینه کردن این مسئله می‌توانید از یک واسط (Interface) نیز کمک بگیرید. برای این منظور می‌توان متد Tick را درون واسطی قرار داد و سپس کلاس Ticker را به این واسط مرتبط نمود.

interface Tickable
{
    void Tick();
}
 
class Ticker
{
    public void Attach(Tickable newSubscriber)
    {
        subscribers.Add(newSubscriber);
    }
    public void Detach(Tickable exSubscriber)
    {
        subscribers.Remove(exSubscriber);
    }
    // هر ثانیه فراخوانی میگردد Notify
    private void Notify()
    {
        foreach (Tickable t in subscribers)
        {
            t.Tick();
        }
    }
    
    private ArrayList subscribers = new ArrayList();
}

این راه حل این امکان را برای کلیه کلاسها فراهم می‌نماید تا واسط Tickable را پیاده‌سازی کنند.

class Clock : Tickable
{
    
    public void Tick()
    {
        
    }
    
}
class ExampleUse
{
    static void Main() 
    {
        Ticker pulsed = new Ticker();
        Clock wall = new Clock();
        pulsed.Attach(wall);
        
    }
}

حال به بررسی همین مثال با استفاده از delegate خواهیم پرداخت.

حل مسئله با استفاده از delegate

استفاده از واسطها در برنامه‌ها، مطمئناً روشی بسیار خوب است، اما کامل نبوده اشکالاتی دارد. مشکل اول آنست که این روش بسیار کلی و عمومی است. تصور نمایید می‌خواهید از تعداد زیادی از سرویسها استفاده نمایید(بعنوان مثال در برنامه‌های مبتنی بر GUI. در اینگونه برنامه‌ها هجم عظیمی از رخدادها وجود دارند که می‌بایست با تمامی آنها در ارتباط باشید.) مشکل دیگر آنست که استفاده از واسط، بدین معناست که متد Tick باید متدی public باشد، از اینرو هر کدی می‌تواند Clock.Tick را در هر زمانی فراخوانی نماید. روش مناسب تر آنست که مطمئن شویم تنها اعضایی خاص قادر به فراخوانی و دسترسی به Clock.Tick هستند. با استفاده از delegate تمامی این امکانات برای ما فراهم خواهد شد و برنامه‌هایی با ایمنی بالاتر و پایدارتر می‌توانیم داشته باشیم.

اعلان Delegate

در مثال ما، متد Tick از واسط Tickable از نوع void بود و هیچ پارامتری دریافت نمی‌کرد :

interface Tickable
{
    void Tick();
}

برای این متد می‌توان delegate ی تعریف نمود که ویژگیهای آنرا داشته باشد :

delegate void Tick();

 همانطور که قبلاً نیز گفته شد، این عمل نوع جدیدی را ایجاد می‌نماید که می‌توان از آن همانند سایر انواع استفاده نمود. مثلاً می‌توان آنرا بعنوان پارامتری برای یک متد در نظر گرفت :

void Example(Tick param)
{
    
}

فراخوانی delegate

قدرت و توانایی delegate زمانی مشهود می‌گردد که می‌خواهید از آن استفاده نمایید. برای مثال، با متغیر param در مثال قبل چکار می‌توانید انجام دهید؟ اگر param متغیری از نوع int بود، از مقدار آن استفاده می‌کردید و با استفاده از عملگرهایی نظیر +، - و یا عملگرهای مقایسه‌ای، عملی خاص را بر روی آن انجام می‌دادید. اما حال که param متغیری از نوع int نیست، چه می‌کنید؟ متغیر param یک delegate است و همانطور که گفته شد، delegate انتزاعی از یک متد است، پس هر عملی که متد انجام می‌دهد، delegate نیز می‌تواند انجام دهد. با استفاده از پرانتز، می‌توان از delegate استفاده نمود :

void Example(Tick param)
{
    param();
}

نکته : همانطور که اشاره شد، delegate یکی از انواع مرجعی است از اینرو مقدار آن می‌تواند برابر با Null باشد. در مثال فوق، اگر مقدار param برابر با Null باشد، کامپایلر خطای NullReferenceException را ایجاد می‌نماید.

همانند متدها، delegate ها باید بطور کامل و صحیح فراخوانی گردند. با توجه به اعلان Tick، در زمان فراخوانی  این delegate، مثلاً param، باید توجه داشت که هیچ پارامتری را نمی‌توان به آن ارسال نمود و نمی‌توان آنرا به متغیری نسبت داد چراکه این delegate بصورت void اعلان شده و مقدار بازگشتی ندارد.

void Example(Tick param)
{
    param(42);                  // خطای زمان کامپایل رخ می‌دهد
    int hhg = param();          // خطای زمان کامپایل رخ می‌دهد
    Console.WriteLine(param()); // خطای زمان کامپایل رخ می‌دهد
}

توجه نمایید که delegate را به هر نحوی می‌توانید اعلان نمایید. برای مثال به نسخة دیگری از Tick توجه کنید :

delegate void Tick(int hours, int minutes, int seconds);

اما به یاد داشته باشید که همانند متد، در هنگام استفاده از آن باید پارامترهای صحیح به آن ارسال نمایید :

void Example(Tick method)
{
    method(12, 29, 59);
}

با استفاده از delegate می‌توانید کلاس Ticker را پیاده‌سازی کنید :

delegate void Tick(int hours, int minutes, int seconds);

class Ticker

{

   

    public void Attach(Tick newSubscriber)

    {

        subscribers.Add(newSubscriber);

    }

    public void Detach(Tick exSubscriber)

    {

        subscribers.Remove(exSubscriber);

    }

 

    private void Notify(int hours, int minutes, int seconds)

    {

        foreach (Tick method in subscribers)

        {

            method(hours, minutes, seconds);

        }

    }

   

    private ArrayList subscribers = new ArrayList();

}

ساخت نمونه‌های جدید از یک delegate

آخرین کاری که باید انجام دهید، ایجاد نمونه‌های جدید از delegate ساخته شده است. یک نمونة جدید از یک delegate، تنها انتزاعی از یک متد است که با نامگذاری آن متد ایجاد می‌شود.

class Clock
{
    
    public void RefreshTime(int hours, int minutes, int seconds)
    {
        
    }
    
}

با توجه به ساختار Tick، ملاحظه می‌نمایید که متد RefreshTime کاملاً با این delegate همخوانی دارد :

delegate void Tick(int hours, int minutes, int seconds);

و این بدین معناست که می‌توان نمونة جدید از Tick ایجاد کرد که انتزاعی از فراخوانی RefreshTime در شیء خاصی از Clock است.

Clock wall = new Clock();
Tick m = new Tick(wall.RefreshTime);

حال که m، ایجاد شد، می‌توانید از آن بصورت زیر استفاده نمایید :

m(12, 29, 59);

این دستور در حقیقت کار دستور زیر را انجام می‌دهد (چون m دقیقاً انتزاع آن است) :

wall.RefreshTime(12, 29, 59);

همچنین می‌توانید m را بعنوان پارامتر به متدی ارسال نمایید. حال تمام چیزهایی را که برای حل مسئله با استفاده از delegate بدانها نیاز داشتیم را بررسی کردیم.  در زیر مثالی را مشاهده می‌کنید که کلاسهای Ticker و Clock را به یکدیگر مرتبط نموده است. در این مثال از واسط استفاده نشده و متد RefreshTime، متدی private است :

delegate void Tick(int hours, int minutes, int seconds);

class Clock

{

   

    public void Start()

    {

        ticking.Attach(new Tick(this.RefreshTime));

    }

    public void Stop()

    {

        ticking.Detach(new Tick(this.RefreshTime));

    }

    private void RefreshTime(int hours, int minutes, int seconds)

    {

        Console.WriteLine("{0}:{1}:{2}", hours, minutes, seconds);

    }

    private Ticker ticking = new Ticker();

}

با اندکی تامل و صرف وقت می‌توانید delegate  را بطور کامل درک نمایید.

رخدادها (Events)

در برنامه‌های Console ، برنامه منتظر ورود اطلاعات یا دستوراتی از سوی کاربر می‌ماند و با استفاده از این اطلاعات کار مورد نظر را انجام می‌دهند.  این روش برقراری ارتباط با کاربر، روشی ناپایدار و غیر قابل انعطاف است. در مقابل برنامه‌های Console، برنامه‌های مدرن وجود دارند که با استفاده از GUI با کاربر در ارتباطند و بر پایه رخدادها بنا شده‌اند (Event-Based)، بدین معنا که رخدادی (منظور از رخداد اتفاقی است که در سیستم یا محیط برنامه صورت میگیرد.) در سیستم روی می‌دهد و بر اساس این رخداد عملی در سیستم انجام می‌شود.  در برنامه‌های تحت ویندوز، نیازی به استفاده از حلقه‌های متعدد جهت منتظر ماندن برای ورودی از کاربر نیست، بلکه با استفاده از رخدادها، تراکنش بین سیستم و کاربر کنترل می‌شود.

یک event در زبان C#، عضوی از کلاس است، که در صورت بروز رخداد خاصی، فعال می‌شود و عملی را انجام می‌دهد. معمولاً برای فعال شده event از دو عبارت fires و raised استفاده می‌شود. هر متدی که بخواهد، میتواند در لیست رخداد ثبت شده و به محض اتفاق افتادن آن رخداد، از آن مطلع گردد.

بطور کلی می‌توان گفت که یک رخداد همانند یک فیلد اعلان می‌شود با این تفاوت مهم که نوع آنها حتماٌ باید یک delegate باشد.

Delegate و رخدادها در کنار یکدیگر کار می‌کنند تا قابلیت‌های یک برنامه را افزایش دهند. این پروسه با شروع یک کلاس که یک رخداد را تعریف می‌کند، آغاز می‌شود. هر کلاسی، که این رخداد را درون خود داشته باشد، در آن رخداد ثبت شده است و می‌تواند متدی را به آن رخداد تخصیص دهد. این عمل با استفاده از delegate ها صورت می‌پذیرد، بدین معنی که delegate متدی را که برای رخداد ثبت می‌شود را تعیین می‌نماید. Delegate ها می‌توانند هر یک از delegate های از پیش تعریف شدة .Net و یا هر delegate ی باشند که توسط کاربر تعریف شده است. بطور کلی، delegate ی را به رخدادی تخصیص می‌دهیم تا متدی را که بهنگام روی دادن رخداد فراخوانی می‌شود، معین گردد. مثال زیر روش تعریف رخداد را نشان می‌دهد.

مثال 2-14 : اعلان و پیاده‌سازی رخدادها

using System;

public delegate void MyDelegate();

class Listing14-2

{

   public static event MyDelegate MyEvent;

   static void Main()

   {

      MyEvent += new MyDelegate(CallbackMethod);

      // فراخوانی رخداد

      MyEvent();

      Console.ReadLine();

   }

   public static void CallbackMethod()

   {

      Console.WriteLine("CallbackMethod.");

   }

}

در این مثال، ابتدا اعلان یک delegate دیده می‌شود.  درون کلاس، رخدادی با نام MyEvent و از نوع MyDelegate تعریف شده است. در متد Main() نیز مرجع جدیدی به رخداد MyEvent افزوده شده است. همانطور که در این مثال نیز مشاهده می‌کنید، delegate ها تنها با استفاده از += می‌توانند به رخدادها افزوده شوند. در این مثال هر گاه MyEvent فراخوانی شود، متد CallbackMethod اجرا می‌شود چراکه با استفاده از مرجع delegate به رخداد مرتبط شده است. (یا در اصطلاح در رخداد ثبت شده است.)

مثال فوق را بدون استفاده از رخداد نیز می‌توان نوشت. این نسخه از مثال 2-14 که تنها در آن از delegate استفاده شده در زیر آورده شده است :

using System;

public delegate void MyDelegate();

class UsingDelegates

{

   static void Main()

   {

      MyDelegate del = new MyDelegate(CallbackMethod);

      // delegate فراخوانی

      del();

      Console.ReadLine();

   }

   public static void CallbackMethod()

   {

      Console.WriteLine("CallbackMethod.");

   }

}

 

باید توجه کنید که موارد کاربرد رخدادها بیشتر در برنامه‌های تحت ویندوز نمایان می‌شود و در اینجا شاید وجود آنها در برنامه برای شما مشهود نباشد. در آینده، به بررسی برنامه‌نویسی فرمهای ویندوز نیز خواهیم رسید و در آنجا به طور مفصل درباره event ها و delegate ها مجدداً بحث خواهیم نمود.

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

توضیحات پیشرفته :

در انتهای این درس می‌خواهم توضیحات پیشرفته تری را نیز در اختیار شما قرار دهم. در قسمت مربوط به delegate ها در همین درس، مثالی مطرح شد که در آن delegate ی با نام Tick وجود داشت. اعلان این delegate به صورت زیر بود :

delegate void Tick(int hours, int minutes, int seconds);

 

حال میخواهیم به این مثال یک رخداد نیز اضافه کنیم. در زیر رخداد tick از نوع Tick اعلان شده است :

class Ticker

{

    public event Tick tick;

   

}

باید توجه نمایید که یک رخداد بطور خودکار لیست اعضای خود را مدیریت می‌کند و نیازی به استفاده از یک مجموعه، مانند آرایه، برای مدیریت اعضای مرتبط با آن نیست.

نکته : یک رخداد بطور خودکار خود را تخصیص دهی می‌کند و نیازی به ساخت نمونة جدید از روی یک رخداد وجود ندارد.

عضو شدن در یک رخداد (تبث شدن در یک رخداد)

برای افزودن delegate جدید به یک رخداد کافیست تا از عملگر += استفاده نماییم. مثال زیر کلاس Clock را نشان می‌دهد که در آن فیلدی از نوع Ticker با نام pulsed وجود دارد. کلاس Ticker دارای رخداد tick از نوع delegate ی بنام Tick است. متد Clock.Start، delegate ی از نوع Tick را با استفاده از عملگر += به pulsed.tick می‌افزاید. 

delegate void Tick(int hours, int minutes, int seconds);

class Ticker

{

    public event Tick tick;

   

}

class Clock

{

   

    public void Start()

    {

        pulsed.tick += new Tick(this.RefreshTime);

    }

   

    private void RefreshTime(int hours, int minutes, int seconds)

    {

       

    }

    private Ticker pulsed = new Ticker();

}

هنگامیکه رخداد pulsed.tick اجرا می‌شود، تمامی delegate های مرتبط با آن نیز فراخوانی می‌شوند که در اینجا یکی از آنها RefreshTime است. (به مثال موجود در بخش delegate رجوع نمایید.)

خارج شدن از لیست یک رخداد

همانطور که با استفاده از عملگر += می‌توان delegate ی را به یک رخداد افزور، با استفاده از عملگر -= نیز می‌توان delegate خاصی را از لیست اعضای یک رخداد خارج نمود.

class Clock
{
    
    public void Stop()
    {
        pulsed.tick -= new Tick(this.RefreshTime);
    }
    private void RefreshTime(int hours, int minutes, int seconds)
    {
        
    }    
    private Ticker pulsed = new Ticker();
}

نکته : همانطور که می‌دانید، عملگرهای += و -= بر پایة دو عملگر اصلی + و ایجاد شده‌اند. از اینرو در مورد delegate ها نیز می‌توان از عملگر + استفاده نمود. استفاده از عملگر + برای delegate ها باعث ایجاد delegate جدیدی می‌شود که به هنگام فراخوانی هر دو delegate را به هم فرامی‌خواند.

فراخوانی یک رخداد

یک رخداد نیز همانند delegate، با استفاده از دو پرانتز فراخوانی می‌گردد. پس از اینکه رخدادی فراخوانی شد، کلیه delegate های مرتبط با آن بترتیب فراخوانی می‌شوند. برای مثال در اینجا کلاس Ticker را در نظر بگیرید که دارای متد private با نام Notify است که رخداد tick را فرا می‌خواند :

class Ticker

{

    public event Tick tick;

   

    private void Notify(int hours, int minutes, int seconds)

    {

        if (tick != null)

        {

            tick(hours, minutes, seconds);

        }

    }

   

}

نکته مهم : توجه کنید که در مثال فوق چک کردن null نبودن رخداد tick ضروری است، چراکه فیلد رخداد بطور ضمنی null در نظر گرفته می‌شود و تنها زمانی مقداری به غیر null میگیرد که delegate ی به آن مرتبط شده باشد. در صورت فراخوانی رخداد null، خطای NullReferenceException روی خواهد داد.

رخدادها دارای سطح امنیتی داخلی بسیار بالایی هستند. رخدادی که بصورت public اعلان می‌شود، تنها از طریق متدها یا عناصر داخل همان کلاس قابل دسترسی است. بعنوان مثال، tick رخدادی درون کلاس Ticker است، از اینرو تنها متدهای درون Ticker می‌توانند tick را فرا بخوانند.

class Example
{
    static void Main()
    {
        Ticker pulsed = new Ticker();
        pulsed.tick(12, 29, 59); // خطای زمان کامپایل رخ می‌دهد
    }
}

مثالی پیشرفته از استفادة رخدادها در فرمهای ویندوز

حال که تا حدودی با رخدادها و ساختار آنها آشنا شدید، در این قسمت قصد دارم تا مقداری دربارة استفاده رخدادها در فرمهای ویندوز و GUI ها صحبت نمایم. هر چند تا کنون کلیه برنامه‌ها و مطالبی که مشاهده کرده‌اید مبتنی بر ‍Console بوده‌اند، اما به علت استفاده بیشمار رخدادها در فرمهای ویندوز و برنامه‌های مبتنی بر GUI، لازم دیدم تا مطالبی نیز در این باره بیان کنم. هر چند فرمهای ویندوز و GUI مطالبی هستند که خود نیاز به بحث و بررسی دقیق دارند و انشا ا... در رئوس آتی سایت مورد بررسی قرار خواهند گرفت. درصورتیکه مطالب این قسمت برای شما دشوار و یا گنگ بود نگران و یا ناراحت نشوید چرا که فعلاً برای یادگیری این مطالب آنهم بدون مقدمه اندکی زود است، بیشتر هدف من از این بخش آشنا شدن شما با کاربردهای پیشرفته‌تر رخدادها در برنامه‌نویسی بوده است.

کلاسهای GUI مربوط به .Net Framework بطور گسترده‌ای از رخدادها استفاده می‌نمایند. در مثالی که در اینجا مورد بررسی قرار می‌دهیم، برنامه‌ای وجود دارد که دارای یک فرم به همراه دو دکمه (Button) بر روی آن است. این دو دکمه بوسیلة دو فیلد از نوع Button ایجاد می‌شوند. (Button عضو System.Windows.Forms است). کلاس Button از کلاس Control ارث‌بری می‌کند و دارای رخدادی با نام Click از نوع EventHandler است. به مثال توجه نمایید.

namespace System

{

    public delegate void EventHandler(object sender, EventArgs args);

    public class EventArgs

    {

       

    }

}

namespace System.Windows.Forms

{

    public class Control :

    {

        public event EventHandler Click;

       

    }

    public class Button : Control

    {

       

    }

}

توجه نمایید که کد فوق، کد مربوط به namespace مربوط به System است که نحوة پیاده‌سازی آنرا نشان می‌دهد. همانطور که ملاحظه می‌نمایید، درون System، delegate ی با نام EventHandler تعریف شده است. در زیر این namespace، اعلان System.Windows.Forms نیز آورده شده تا نحوة اعلان رخداد Click و ارث‌بری کلاس Button از کلاس Control نیز مشخص شود.

پس از اینکه بر روی دکمه‌ای واقع در فرم ویندوز کلیک کنید، Button بطور خودکار رخداد Click را فرا می‌خواند. هدایت این پروسه باعث می‌شود تا بتوان به سادگی delegate ی برای کنترل این رخداد ایجاد نمود. در مثالی که در زیر مشاهده می‌کنید، دکمه‌ای با نام Okay، متدی بنام Okay_Click و رخدادی جهت اتصال Okay به متد Okay_Click وجود دارد.

class Example : System.Windows.Forms.Form
{
    private System.Windows.Forms.Button okay;
    
    public Example()
    {
        this.okay = new System.Windows.Forms.Button();
        this.okay.Click += new System.EventHandler(this.okay_Click);
        
    }
 
    private void okay_Click(object sender, System.EventsArgs args)
    {
        
    }
}

همانطور که مشاهده می‌کنید، کلاس Example از System.Windows.Forms.Form مشتق می‌شود، از اینرو تمامی خواص آن را به ارث می‌برد. Okay نیز از نوع Button اعلان شده است. درون سازندة (Constructor) کلاس Example، متد Okay.Click به رخداد افزوده شده و مرجع this.Okay.Click نیز، متد مورد نظر را تعیین نموده است. همانطور که گفته شد، EventHandler نیز delegate مورد نظر در این مثال است. درون متد Okay_Click نیز میتوان کد خاصی را قرار داد تا عمل مورد نظر را انجام دهد. پس از کلیک کردن بر روی دکمة Okay، عمل مورد نظری که درون متد Okay_Click قرار داده شده، اجرا می‌شود.

این کد، شبیه به کدهایی است که توسط محیط‌های برنامه‌سازی نظیر Visual Studio.Net و یا C#Builder بطور خودکار  تولید می‌شوند و تنها کافیست تا شما کد مربوط به Okay_Click را درون آن وارد نمایید.

رخدادهایی که توسط کلاسهای GUI تولید می‌شوند همواره از یک الگوی خاص پیروی می‌کنند. این رخدادها همواره از نوع delegate ی هستند که مقدار بازگشتی ندارد (void) و دارای دو آرگومان است. آرگومان اول همیشه فرستندة رخداد و آرگومان دوم همیشه آرگومان EventArgs یا کلاس مشتق شده از EventArgs است.

آرگومان sender به شما این امکان را می‌دهد تا از یک delegate برای چندین رخداد استفاده نمایید. (بعنوان مثال برای چندین دکمه.)

نکاتی چند درباره delegate ها و event ها

  • Delegate ها بطور ضمنی از System.Delegate ارث‌بری می‌کنند. Delegate حاوی متدها، property ها و عملگرهایی است که می‌توان آنها را بعنوان پارامتر به متدهای دیگر ارسال نمود. همچنین به دلیل اینکه System.Delegate بخشی از .Net Framework است، از اینرو delegate های ایجاد شده در C# را می‌توان در زبانهای دیگری نظیر Visual Basic.Net نیز استفاده نمود.
  • هنگام اعلان پارامترها برای delegate، حتماً باید برای آنها نام در نظر بگیرید و فقط به مشخص کردن نوع پارامترها بسنده نکنید.
  • رخدادها عناصر بسیار مفید و پر استفاده‌ای هستند که با بکارگیری delegate ها بسیار قدرتمند ظاهر می‌شوند. بدست آوردن مهارت در ایجاد و استفاده از آنها نیاز به تمرین و تفکر بسیار دارد.

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

  
نویسنده : ali gooliof ; ساعت ٤:۱٧ ‎ب.ظ روز ۱۳۸٧/٢/٦
تگ ها :