درس چهاردهم – رخدادها و 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 خواهیم پرداخت.
درس سیزدهم – واسطها (Interfaces)
در این درس با واسطها در زبان C# آشنا خواهیم شد. اهداف این درس بشرح زیر میباشند :
1- آشنایی با مفهوم کلی واسطها
2- تعریف یک واسط
3- استفاده از یک interface
4- پیادهسازی ارثبری در interface ها
5- نکات مهم و پیشرفته
6- مثالی کاربردی از واسطها
7- منابع مورد استفاده
واسطها از لحاظ ظاهری بسیار شبیه به کلاس هستند با این تفاوت که دارای هیچ گونه پیادهسازی نمیباشند. تنها چیزی که در interface به چشم میخورد تعاریفی نظیر رخدادها، متدها، اندیکسرها و یا property ها است. یکی از دلایل اینکه واسطها تنها دارای تعاریف هستند و پیادهسازی ندارند آنست که یک interface میتوان توسط چندین کلاس یا property مورد ارثبری قرار گیرد، از اینرو هر کلاس یا property خواستار آنست که خود به پیادهسازی اعضا بپردازد.
حال باید دید چرا با توجه به اینکه interface ها دارای پیادهسازی نیستند مورد استفاده قرار میگیرند یا بهتر بگوئیم سودمندی استفاده از interface ها در چیست؟ تصور کنید که در یک برنامه با مولفههایی سروکار دارید که متغیرند ولی دارای فیلدها یا متدهایی با نامهای یکسانی هستند و باید نام این متدها نیز یکسان باشد. با استفاده از یک interface مناسب میتوان تنها متدها و یا فیلدهای مورد نظر را اعلان نمود و سپس کلاسها و یا property های مورد از آن interface ارثبری نمایند. در این حالت تمامی کلاسها و property ها دارای فیلدها و یا متدهایی همنام هستند ولی هر یک پیادهسازی خاصی از آنها را اعمال مینمایند.
نکته مهم دیگر درباره interface ها، استفاده و کاربرد آنها در برنامههای بزرگی است که برنامهها و یا اشیاؤ مختلفی در تماس و تراکنش (transact) هستند. تصور کنید کلاسی در یک برنامه با کلاسی دیگر در برنامهای دیگر در ارتباط باشد. فرض کنید این کلاس متدی دارد که مقداری از نوع int بازمیگرداند. پس از مدتی طراح برنامه به این نتیجه میرسد که استفاده از int پاسخگوی مشکلش نیست و باید از long استفاده نماید. حال شرایط را در نظر بگیرید که برای تغییر یک چنین مسئله سادهای چه مشکل بزرگی پیش خواهد آمد. تمامی فیلدهای مورتبط با این متد باید تغییر داده شوند. در ضمن از مسئله side effect نیز نمیتوان چشم پوشی کرد.( تاثیرات ناخواسته و غیر منتظره و یا به عبارتی پیش بینی نشده که متغیر یا فیلدی بر روی متغیر یا فیلدی دیگر اعمال میکند، در اصطلاح side effect گفته میشود.) حال فرض کنید که در ابتدا interface ای طراحی شده بود. درصورت اعمال جزئیترین تغییر در برنامه مشکل تبدیل int به long قابل حل بود، چراکه کاربر یا برنامه و در کل user برنامه در هنگام استفاده از یک interface با پیادهسازی پشت پرده آن کاری ندارد و یا بهتر بگوئیم امکان دسترسی به آن را ندارد. از اینرو اعمال تغییرات درون آن تاثیری بر رفتار کاربر نخواهد داشت و حتی کاربر از آن مطلع نیز نمیشود. در مفاهیم کلی شیء گرایی، interface ها یکی از مهمترین و کاربردی ترین اجزاء هستند که در صورت درک صحیح بسیار مفید واقع میشوند. یکی از مثالهای مشهود درباره interface ها (البته در سطحی پیشرفته تر و بالاتر) رابطهای کاربر گرافیکی (GUI) هستند. کاربر تنها با این رابط سروکار دارد و کاری به نحوه عملیات پشت پرده آن ندارد و اعمال تغییرات در پیادهسازی interface کاربر را تحت تاثیر قرار نمیدهد.
از دیدگاه تکنیکی، واسطها بسط مفهومی هستند که از آن به عنوان انتزاع (Abstract) یاد میکنیم. در کلاسهای انتزاعی (که با کلمه کلید abstract مشخص میشدند.) سازندة کلاس قدر بود تا فرم کلاس خود را مشخص نماید : نام متدها، نوع بازگشتی آنها و تعداد و نوع پارامتر آنها، اما بدون پیادهسازی بدنه متد. یک interface همچنین میتواند دارای فیلدهایی باشد که تمامی آنها static و final هستند. یک interface تنها یک فرم کلی را بدون پیادهسازی به نمایش میگذارد.
از این دیدگاه، یک واسط بیان میدارد که : " این فرم کلی است که تمامی کلاسهایی که این واسط را پیادهسازی میکنند، باید آنرا داشته باشند." از سوی دیگر کلاسها و اشیاء دیگری که از کلاسی که از یک واسط مشتق شده استفاده میکنند، میدانند که این کلاس حتماً تمامی متدها و اعضای واسط را پیادهسازی میکند و میتوانند به راحتی از آن متدها و اعضا استفاده نمایند. پس به طور کلی میتوانیم بگوئیم که واسطها بمنظور ایجاد یک پروتکل (protocol) بین کلاسها مورد استفاده قرار میگیرند. (همچنان که برخی از زبانهای برنامهسازی بجای استفاده از کلمه کلیدی interface از protocol استفاده مینمایند.)
به دلیل اینکه کلاسها و ساختارهایی که از interface ها ارثبری میکنند موظف به پیادهسازی و تعریف آنها هستند، قانون و قاعدهای در این باره ایجاد میگردد. برای مثال اگر کلاس A از واسط IDisposable ارثبری کند، این ضمانت بوجود میآید که کلاس A دارای متد Dispose() است، که تنها عضو interface نیز میباشد. هر کدی که میخواهد از کلاس A استفاده کند، ابتدا چک مینماید که آیا کلاس A واسط IDisposable را پیادهسازی نموده یا خیر. اگر پاسخ مثبت باشد آنگاه کد متوجه میشود که میتواند از متد A.Dispose() نیز استفاده نماید. در زیر نحوه اعلان یک واسط نمایش داده شده است.
interface IMyInterface
{
void MethodToImplement();
}
در این مثال نحوه اعلان واسطی با نام IMyInterface نشان داده شده است. یک قاعده (نه قانون!) برای نامگذاری واسطها آنست که نام واسطها را با "I" آغاز کنیم که اختصار کلمه interface است. در interface این مثال تنها یک متد وجود دارد. این متد میتوان هر متدی با انواع مختلف پارامترها و نوع بازگشتی باشد. توجه نمایید همانطور که گفته شد این متد دارای پیادهسازی نیست و تنها اعلان شده است. نکته دیگر که باید به ان توجه کنید آنست که این متد به جای داشتن {} به عنوان بلوک خود، دارای ; در انتهای اعلان خود میباشد. علت این امر آنست که interface تنها نوع بازگشتی و پارامترهای متد را مشخص مینماید و کلاس یا شیای که از آن ارث میبرد باید آنرا پیادهسازی نماید. مثال زیر نحوه استفاده از این واسط را نشان میدهد.
مثال 1-13 : استفاده از واسطها و ارثبری از آنها
class InterfaceImplementer : IMyInterface
{
static void Main()
{
InterfaceImplementer iImp = new InterfaceImplementer();
iImp.MethodToImplement();
}
public void MethodToImplement()
{
Console.WriteLine("MethodToImplement() called.");
}
}
در این مثال، کلاس InterfaceImplementer همانند ارثبری از یک کلاس، از واسط IMyInterface ارثبری کرده است. حال که این کلاس از واسط مورد نظر ارثبری کرده است، باید، توجه نمایید باید، تمامی اعضای آنرا پیادهسازی کند. در این مثال این عمل با پیادهسازی تنها عضو واسط یعنی متد MethodToImplement() انجام گرفته است. توجه نمایید که پیادهسازی متد باید دقیقا از لحاظ نوع بازگشتی و تعداد و نوع پارامترها شبیه به اعلان موجود در واسط باشد، کوچکترین تغییری باعث ایجاد خطای کامپایلر میشود. مثال زیر نحوه ارثبری واسطها از یکدیگر نیز نمایش داده شده است.
مثال 2-13 : ارثبری واسطها از یکدیگر
using System;
interface IParentInterface
{
void ParentInterfaceMethod();
}
interface IMyInterface : IParentInterface
{
void MethodToImplement();
}
class InterfaceImplementer : IMyInterface
{
static void Main()
{
InterfaceImplementer iImp = new InterfaceImplementer();
iImp.MethodToImplement();
iImp.ParentInterfaceMethod();
}
public void MethodToImplement()
{
Console.WriteLine("MethodToImplement() called.");
}
public void ParentInterfaceMethod()
{
Console.WriteLine("ParentInterfaceMethod() called.");
}
}
مثال 2-13 دارای 2 واسط است : یکی IMyInterface و واسطی که از آن ارث میبرد یعنی IParentInterface. هنگامیکه واسطی از واسط دیگری ارثبری میکند، کلاس یا ساختاری که این واسطها را پیادهسازی میکند، باید تمامی اعضای واسطهای موجود در سلسله مراتب ارثبری را پیادهسازی نماید. در مثال 2-13، چون کلاس InterfaceImplementer از واسط IMyInterface ارثبری نموده، پس از واسط IParentInterface نیز ارثبری دارد، از اینرو باید کلیه اعضای این دو واسط را پیادهسازی نماید.
چند نکته مهم :
1- با استفاده از کلمه کلید interface در حقیقت یک نوع مرجعی (Reference Type) جدید ایجاد نمودهاید.
2- از لحاظ نوع ارتباطی که واسطها و کلاسها در ارثبری ایجاد مینمایند باید به این نکته اشاره کرد که، ارثبری از کلاس رابطه "است" یا "بودن" (is-a relation) را ایجاد میکند (ماشین یک وسیله نقلیه است) ولی ارثبری از یک واسط یا interface نوع خاصی از رابطه، تحت عنوان "پیادهسازی" (implement relation) را ایجاد میکند. ("میتوان ماشین را با وام بلند مدت خرید" که در این جمله ماشین میتواند خریداری شدن بوسیله وام را پیادهسازی کند.)
3- فرم کلی اعلان interface ها بشکل زیر است :
[attributes] [access-modifier] interface interface-name [:base-list]{interface-body}
که در اعضای آن بشرح زیر می باشند :
attributes : صفتهای واسط
access-modifiers : private یا public سطح دسترسی به واسط از قبیل
interface-name : نام واسط
:base-list : لیست واسطهایی که این واسط آنها را بسط میدهد.
Interface-body : بدنه واسط که در آن اعضای آن مشخص میشوند
توجه نمایید که نمیتوان یک واسط را بصورت virtual اعلان نمود.
4- هدف از ایجاد یک interface تعیین توانائیهاییست که میخواهیم در یک کلاس وجود داشته باشند.
5- به مثالی در زمینه استفاده از واسطها توجه کنید :
فرض کنید میخواهید واسطی ایجاد نمایید که متدها و property های لازم برای کلاسی را که میخواهد قابلیت خواندن و نوشتن از/به یک پایگاه داده یا هر فایلی را داشته باشد، توصیف نماید. برای این منظور میتوانید از واسط IStorable استفاده نمایید.
در این واسط دو متد Read() و Write() وجود دارند که در بدنه واسط تعریف میشوند ک
interface IStorable
{
void Read( );
void Write(object);
}
حال میخواهید کلاسی با عنوان Document ایجاد نمایید که این کلاس باید قابلیت خواندن و نوشتن از/به پایگاه داده را داشته باشد، پس میتوانید کلاس را از روی واسط IStorable پیادهسازی کنید.
public class Document : IStorable
{
public void Read( ) {...}
public void Write(object obj) {...}
// ...
}
حال بعنوان طراح برنامه، شما وظیفه داری تا به پیادهسازی این واسط بپردازید، بطوریکه کلیه نیازهای شما را برآورده نماید. نمونهای از این پیادهسازی در مثال 3-13 آورده شده است.
مثال 3-13 : پیادهسازی واسط و ارثبری – مثال کاربردی
using System;
// interface اعلان
interface IStorable
{
void Read( );
void Write(object obj);
int Status { get; set; }
}
public class Document : IStorable
{
public Document(string s)
{
Console.WriteLine("Creating document with: {0}", s);
}
public void Read( )
{
Console.WriteLine("Implementing the Read Method for IStorable");
}
public void Write(object o)
{
Console.WriteLine("Implementing the Write Method for IStorable");
}
public int Status
{
get
{
return status;
}
set
{
status = value;
}
}
private int status = 0;
}
public class Tester
{
static void Main( )
{
Document doc = new Document("Test Document");
doc.Status = -1;
doc.Read( );
Console.WriteLine("Document Status: {0}", doc.Status);
IStorable isDoc = (IStorable) doc;
isDoc.Status = 0;
isDoc.Read( );
Console.WriteLine("IStorable Status: {0}", isDoc.Status);
}
}
خروجی برنامه نیز بشکل زیر است :
Output:
Creating document with: Test Document
Implementing the Read Method for IStorable
Document Status: -1
Implementing the Read Method for IStorable
IStorable Status: 0
6- در مثال فوق توجه نمایید که برای متدها واسط IStorable هیچ سطح دسترسی (public,private و ...) در نظر گرفته نشده است. در حقیقت تعیین سطح دسترسی باعث ایجاد خطا میشود چراکه هدف اصلی از ایجاد یک واسط ایجاد شیء است که تمامی اعضای آن برای تمامی کلاسها قابل دسترسی باشند.
7- توجه نمایید که از روی یک واسط نمیتوان نمونهای جدید ایجاد کرد بلکه باید کلاسی از آن ارثبری نماید.
8- کلاسی که از واسط ارثبری میکند باید تمامی متدهای آنرا دقیقا همان گونه که در واسط مشخص شده پیادهسازی نماید. به بیان کلی، کلاسی که از یک واسط ارث میبرد، فرم و ساختار کلی خود را از واسط میگیرد و نحوه رفتار و پیادهسازی آنرا خود انجام میدهد.
خلاصه :
در این درس با مفاهیم کلی و اصلی درباره واسطها آشنا شدید. هم اکنون میدانید که واسطها چه هستند و سودمندی استفاده از آنها چیست. همچنین نحوه پیادهسازی واسط و ارثبری از آنرا آموختید.
مبحث واسطها بسیار گسترده و مهم است و امید است در بخشهای آینده در سایت، بتوانم تمامی مطالب را بطور حرفهای و کامل در اختیار شما قرار دهم.
درس دوازدهم – ساختارها در C# (Struct)
در این درس با ساختارها (Struct) در زبان C# آشنا میشویم. اهداف این درس بشرح زیر میباشند
ساختار (struct) چیست؟
همانطور که با استفاده از کلاسها میتوان انواع (types) جدید و مورد نظر را ایجاد نمود، با استفاده از struct ها میتوان انواع مقداری (value types) جدید و مورد نظر را ایجاد نمود. از آنجائیکه struct ها بعنوان انواع مقداری در نظر گرفته میشوند، از اینرو تمامی اعمال مورد استفاده بر روی انواع مقداری را میتوان برای struct ها در نظر گرفت. struct ها بسیار شبیه به کلاسها هستند و میتوانند دارای فیلد، متد و property باشند. عموماً ساختارها مجموعه کوچکی از عناصری هستند که منطقی با یکدیگر دارای رابطه میباشند. برای نمونه میتوان به ساختار Point موجود در Framework SDK اشاره کرد که حاوی دو property با نامهای X و Y است.
با استفاده از ساختارها (struct) میتوان اشیایی با انواع جدید ایجاد کرد که این اشیاء میتوانند شبیه به انواع موجود (int, float, …) باشند. حال سوال اینست که چه زمانی از ساختارها(struct) بجای کلاس استفاده میکنیم؟ در ابتدا به نحوه استفاده از انواع موجود در زبان C# توجه نمایید. این انواع دارای مقادیر و عملگرهای معینی جهت کار با این مقادیر هستند. حال اگر نیاز به شیای دارید که همانند این انواع رفتار نمایند لازم است تا از ساختارها (struct) استفاده نمایید. در ادامه این مبحث نکات و قوانینی را ذکر میکنیم که با استفاده از آنها بهتر بتوانید از ساختارها (struct) استفاده نمایید.
اعلان و پیادهسازی struct
برای اعلان یک struct کافیست تا با استفاده از کلمه کلیدی struct که بدنبال آن نام مورد نظر برای ساختار آمده استفاده کرد. بدنة ساختار نیز بین دو کروشة باز و بسته {} قرار خواهد گرفت. به مثال زیر توجه نمایید :
مثال 1-12 : نمونهای از یک ساختار (Struct)
using System;
struct Point
{
public int x;
public int y;
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
public Point Add(Point pt)
{
Point newPt;
newPt.x = x + pt.x;
newPt.y = y + pt.y;
return newPt;
}
}
///
/// struct مثالی از اعلان و ساخت یک
///
class StructExample
{
static void Main(string[] args)
{
Point pt1 = new Point(1, 1);
Point pt2 = new Point(2, 2);
Point pt3;
pt3 = pt1.Add(pt2);
Console.WriteLine("pt3: {0}:{1}", pt3.x, pt3.y);
}
}
مثال 1-12 نحوة ایجاد و استفاده از struct را نشان میدهد. به راحتی میتوان گفت که یک نوع(type) ، یک struct است، زیرا از کلمه کلیدی struct در اعلان خود بهره میگیرد. ساختار پایهای یک ساختار پایهای یک struct بسیار شبیه به یک کلاس است، ولی تفاوتهایی با آن دارد که این تفاوتها در پاراگراف بعدی مورد بررسی قرار میگیرند. ساختار Point دارای سازنده ایست که مقادیر داده شده با آنرا به فیلدهای x و y تخصیص میدهد. این ساختار همچنین دارای متد Add() میباشد که ساختار Point دیگری را دریافت میکند و آنرا به struct کنونی میافزاید و سپس struct جدیدی را باز میگرداند.
توجه نمایید که ساختار Point جدیدی درون متد Add() تعریف شده است. توجه کنید که در اینجا همانند کلاس، نیازی به استفاده از کلمه کلیدی new جهن ایجاد یک شیء جدید نمیباشد. پس از آنکه نمونة جدیدی از یک ساختار ایجاد شد، سازندة پیش فرض (یا همان سازندة بدون پارامترش) برای آن در نظر گرفته میشود. سازندة بدون پارامتر کلیه مقادیر فیلدهای ساختار را به مقادیر پیش فرض تغییر میدهد. بعنوان مثال فیلدهای صحیح به صفر و فیلدهای Boolean به false تغییر میکنند. تعریف سازندة بدون پارامتر برای یک ساختار صحیح نمیباشد. (یعنی شما نمیتوانید سازندة بدون پارامتر برای یک struct تعریف کنید.)
ساختارها (structs) با استفاده از عملگر new نیز قابل نمونهگیری هستند (هر چند نیازی به استفاده از این عملگر نیست.) در مثال 1-12 pt1 و pt2 که ساختارهایی از نوع Point هستند، با استفاده از سازندة موجود درون ساختار Point مقداردهی میشوند. سومین ساختار از نوع Point، pt3 است و از سازندة بدون پارامتر استفاده میکند زیرا در اینجا مقدار آن اهمیتی ندارد. سپس متد Add() از ساختار pt1 فراخوانده میشود و ساختار pt2 را بعنوان پارامتر دریافت میکند. نتیجه به pt3 تخصیص داده میشود، این امر نشان میدهد که یک ساختار میتواند همانند سایر انواع مقداری مورد استفاده قرار گیرد. خروجی مثال 1-12 در زیر نشان داده شده است :
pt3 : 3 : 3
یکی دیگر از تفاوتهای ساختار و کلاس در اینست که ساختارها نمیتوانند دارای تخریب کننده (deconstructor) باشند. همچنین ارثبری در مورد ساختارها معنی ندارد. البته امکان ارثبری بین ساختارها و interface ها وجود دارد. یک interface نوع مرجعی زبان C# است که دارای اعضایی بدون پیادهسازی است. هر کلاس و یا ساختاری که از یک interface ارثبری نماید باید تمامی متدهای آنرا پیادهسازی کند. دربارة interface ها در آینده صحبت خواهیم کرد.
خلاصه :
هم اکنون شما با چگونگی ایجاد یک ساختار آشنا شدید. هنگامیکه قصد دارید نوعی را بصورت ساختار یا کلاس پیادهسازی کنید، باید به این نکته توجه کنید که این نوع چگونه مورد استفاده قرار میگیرد. اگر میخواهید سازندهای بدون پارامتر داشته باشید، در اینصورت کلاس تنها گزینه شماست. همچنین توجه نمایید از آنجائیکه یک ساختار بعنوان یک نوع مقداری در نظر گرفته میشود، در پشته (Stack) ذخیره میشود و حال آنکه کلاس در heap ذخیره میگردد.
نکات مهم و مطالب کمکی
همانطور که بطور مختصر در بالا نیز اشاره شد، از نظر نوشتاری (syntax) struct و کلاس بسیار شبیه به یکدیگر هستند اما دارای تفاوتهای بسیار مهمی با یکدیگر میباشند.
همانطور که قبلاً نیز اشاره شد شما نمیتوانید برای یک struct سازندهای تعریف کنید که بدون پارامتر است، یعنی برای ایجاد سازنده برای یک struct حتماً باید این سازنده دارای پارامتر باشد. به قطعه کد زیر توجه کنید :
struct Time
{
public Time() { ... } // خطای زمان کامپایل رخ میدهد
⋮
}
پس از اجرای کد فوق کامپایلر خطایی را ایجاد خواهد کرد بدین عنوان که سازندة struct حتماٌ باید دارای پارامتر باشد. حال اگر بجای struct از کلمه کلیدی calss استفاده کرده بودیم این کد خطایی را ایجاد نمیکرد. در حقیقت تفاوت در اینست که در مورد struct، کامپایلر اجازة ایجاد سازندة پیش فرض جدیدی را به شما نمیدهد ولی در مورد کلاس چنین نیست. هنگام اعلان کلاس در صورتیکه شما سازندة پیش فرضی اعلان نکرده باشید، کامپایلر سازندهای پیش فرض برای آن در نظر میگیرد ولی در مورد struct تنها سازندة پیش فرضی معتبر است که کامپایلر آنرا ایجاد نماید نه شما !
یکی دیگر از تفاوتهای بین کلاس و struct در آن است که، اگر در کلاس برخی از فیلدهای موجود در سازندة کلاس را مقداردهی نکنید، کامپایلر مقدار پیش فرض صفر، false و یا null را برای آن فیلد در نظر خواهد گرفت ولی در struct تمامی فیلدهای سازنده باید بطور صریح مقداردهی شوند و درصورتیکه شما فیلدی را مقداردهی نکید کامپایلر هیچ مقداری را برای آن در نظر نخواهدگرفت و خطای زمان کامپایل رخ خواهد داد. بعنوان مثال در کد زیر اگر Time بصورت کلاس تعریف شده بود خطایی رخ نمیداد ولی چون بصورت struct تعریف شده خطای زمان کامپایل رخ خواهد داد :
struct Time
{
public Time(int hh, int mm)
{
hours = hh;
minutes = mm;
} // خطای زمان کامپایلی بدین صورت رخ میدهد : seconds not initialized
⋮
private int hours, minutes, seconds;
}
تفاوت دیگر کلاس و struct در اینست که در کلاس میتوانید در هنگام اعلان فیلدها را مقداردهی کنید حال آنکه در struct چنین عملی باعث ایجاد خطای زمان کامپایل خواهد شد. همانند کدهای فوق، در کد زیر اگر از کلاس بجای struct استفاده شده بود خطا رخ نمیداد :
struct Time
{
⋮
private int hours = 0; // خطای زمان کامپایل رخ میدهد
private int minutes;
private int seconds;
}
آخرین تفاوت بین کلاس و struct که ما به آن خواهیم پرداخت در مورد ارثبری است. کلاسها میتوانند از کلاس پایة خود ارثبری داشته باشند در حالیکه ارثبری در struct ها معنایی ندارد و یک struct تنها میتواند از واسطها (interface) ارثبری داشته باشد.
همانطور که گفتیم، ساختارها روشی برای ایجاد انواع جدید مقدار (Value Types) هستند. از اینرو پس از ایجاد یک ساختار میتوان از آن همانند سایر انواع مقداری استفاده نمود. برای استفاده از یک ساختار ایجاد شده کافیست تا نام آنرا قبل از متغیر مورد نظر قرار دهیم تا متغیر مورد نظر از نوع آن ساختار خاص تعریف شود.
struct Time
{
⋮
private int hours, minutes, seconds;
}
class Example
{
public void Method(Time parameter)
{
Time localVariable;
⋮
}
private Time field;
}
آخرین نکتهای که در مورد ساختارها برای چندمین بار اشاره میکنم انست که، ساختارها انواع مقداری هستند و مستقیماً مقدار را در خود نگه میدارند و از اینرو در stack نگهداری میشوند. استفاده از ساختارها همانند سایر انواع مقداری است.