درس چهاردهم – رخدادها و 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(" Before Sort: ");
sd.PrintNames();
sd.Sort(cmp);
Console.WriteLine(" After Sort: ");
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: ");
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 خواهیم پرداخت.
جالب و اموزنده بود
بازم میام