رخدادها و 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 ها بسيار قدرتمند ظاهر ميشوند. بدست آوردن مهارت در ايجاد و استفاده از آنها نياز به تمرين و تفكر بسيار دارد.
مطالب اين درس در اينجا به پايان رسيد. اميدوارم مورد قبول شما بوده باشد. خواهشمندام نظرات وپيشنهادات ارزندة خود را براي من ايميل كنيد تا از آنها در جهت بهبود مطالب سايت استفاده گردد.