درس شانزدهم – استفاده از صفتها در C#
در این درس با نحوه استفاده از صفتها در زبان C# آشنا خواهید شد. اهداف ما در این درس به شرح زیر است :
1- صفتها چه هستند و چرا از آنها استفاده میکنیم
2- استفاده از صفتهای تک پارامتری و چند پارامتری
3- انواع پارامترهای صفت (پارامترهای Named و Positional)
4- Target های صفتها (عناصری که صفتها بر روی آنها اعمال میشوند)
5- تولید صفتهای شخصی
6- تعریف و یا کنترل موارد استفاده از یک صفت
7- استفاده از پارامترهای Positional و Named در صفتهای شخصی
8- انواع (type) معتبر برای پارامترهای صفت
9- استفاده از صفتها در زمان اجرا
10- خلاصه مطالب
11- منابع
صفتها در حقیقت اطلاعات توضیحی هستند که میتوانید آنها را به برنامههای خود بیفزایید. صفتها را میتوان برای کلیه عناصر برنامه از قبیل کلاسها، واسطها، اسمبلی ها و ... مورد استفاده قرار داد. از این اطلاعات میتوان برای موارد متنوعی در زمان اجرای برنامه استفاده نمود. برای مثال میتوان به صفتی مانند DllImportAttribute اشاره کرد که امکان برقراری ارتباط با توابع کتابخانهای Win32 را فراهم مینماید. همچنین صفتهایی نیز وجود دارند که برنامهنویس یا توسعه دهنده برنامه را در امر تولید برنامه یاری مینمایند. برای مثال میتوان به صفت ObsoleteAttribute اشاره کرد که با استفاده از آن، در زمان کامپایل برنامه پیغامی برای برنامه نویس نمایش داده میشود و مشخص میکند که متدی خاص مورد استفاده قرار نگرفته و یا دیگر مورد استفاده نیست. همچنین هنگامیکه با فرمهای ویندوز کار میکنیم، صفتهای بسیاری وجود دارند که امکان استفاده از این فرمها را فراهم کرده و باعث میشوند تا اطلاعات مربوط به این عناصر در property فرم ظاهر شوند. یکی دیگر از موارد استفاده از صفتها در مسایل امنیتی اسمبلیهای .Net است. برای مثال صفتهایی وجود دارند که باعث جلوگیری از فراخوانیهای غیر مجاز میشوند، بدین معنی که تنها اجازه فراخوانی را به متدها یا اشیایی میدهند که قبلا تعریف شده و مشخص شده باشند.
یکی از علتهای استفاده از صفتها آنست که، اغلب سرویسهایی را که آنها برای کاربر فراهم مینمایند، بسیار پیچیده است و با کدهای معمولی نمیتوان آنرا را بدست آورد. از اینرو استفاده از صفتها در بسیاری از موارد ضروری و اجتناب ناپذیر است. همانطور که خواهید دید، صفتها به برنامههای ما Metadata اضافه مینمایند. پس از کامپایل برنامههای C#، فایل اسمبلی برای آن ایجاد میگردد که این اسمبلی معمولا یا یک فایل اجرایی است و یا یک Dll است. توصیف اسمبلی، در Metadata ی مربوط به آن قرار میگیرد. طی پروسهای تحت عنوان Reflection، صفت یک برنامه از طریق فایل Metadata ی موجود در اسمبلی آن قابل دسترس میگردد. .(برای آشنایی بیشتر با اسمبلی و Metadata میتوانید به " کامپایل یک برنامه سی شارپ " در همین سایت مراجعه نمایید.) در حقیقت صفتها، کلاسهایی هستند که میتوانید آنها را با زبان C# تولید کرده و جهت افزودن اطلاعاتی توضیحی به کد خود، از آنها استفاده نمایید. این اطلاعات در زمان اجرای برنامه از طریق Reflection قابل دسترسی هستند.
در این درس با روش استفاده از صفتها و چگونگی ارتباط دادن آنها با عناصر مختلف برنامه آشنا خواهید شد.
مفاهیم اولیه درباره صفتها
صفتها را معمولا قبل از اعلان عنصر مورد نظر در برنامه قرار میدهند. اعلان صفتها بدین صورت است که نام صفت درون دو براکت قرار میگیرد.
[ObsoleteAttribute]
استفاده از کلمه Attribute در اعلان صفت الزامی نیست، از اینرو اعلان زیر با اعلان فوق یکسان است :
[Obsolete]
همچنین صفتها میتوانند دارای پارامتر نیز باشند که با استفاده از آنها خواص بیشتری را در اختیار برنامه قرار میدهند. در مثال 1-16 موارد متنوعی از استفاده صفت ObsoleteAttribute را مشاهده مینمایید.
مثال 1-16 : نحوه استفاده از صفتها
using System;
class BasicAttributeDemo
{
[Obsolete]
public void MyFirstDeprecatedMethod()
{
Console.WriteLine("Called MyFirstDeprecatedMethod().");
}
[ObsoleteAttribute]
public void MySecondDeprecatedMethod()
{
Console.WriteLine("Called MySecondDeprecatedMethod().");
}
[Obsolete("You shouldn't use this method anymore.")]
public void MyThirdDeprecatedMethod()
{
Console.WriteLine("Called MyThirdDeprecatedMethod().");
}
// make the program thread safe for COM
[STAThread]
static void Main(string[] args)
{
BasicAttributeDemo attrDemo = new BasicAttributeDemo();
attrDemo.MyFirstDeprecatedMethod();
attrDemo.MySecondDeprecatedMethod();
attrDemo.MyThirdDeprecatedMethod();
}
}
همانطور که در مثال 1-16 نیز مشاهده میشود، صفت Obsolete در فرمهای مختلف مورد استفاده قرار گرفته است. اولین محلی که از این صفت استفاده شده است، متد MyFirstDeprecatedMethod() و پس از آن در متد MySecondDeprecatedMethod() است. تنها تفاوت استفاده در این دو حالت آنست که در متد دوم صفت با نام کامل یعنی به همراه کلمه Attribute مورد استفاده قرار گرفته است. نتیجه هر دو اعلان یکسان است. همانطور که گفته بودیم، صفتها میتوانند دارای پارامتر نیز باشند :
[Obsolete("You shouldn't use this method anymore.")]
public void MyThirdDeprecatedMethod()
...
این پارامتر، ویژگی خاصی را به صفت میافزاید که آن را با دو اعلان قبلی متمایز مینماید. نتیجه هر سه اعلان این صفت در زیر آورده شده است. این پیغامها، پیامهای کامپایلر C# هستند که به هنگام کامپایل برنامه تولید شدهاند.
>csc BasicAttributeDemo.cs
Microsoft (R) Visual C# .NET Compiler version 7.10.2292.4
for Microsoft (R) .NET Framework version 1.1.4322
Copyright (C) Microsoft Corporation 2001-2002. All rights reserved.
BasicAttributeDemo.cs(29,3): warning CS0612:
'BasicAttributeDemo.MyFirstDeprecatedMethod()' is obsolete
BasicAttributeDemo.cs(30,3): warning CS0612:
'BasicAttributeDemo.MySecondDeprecatedMethod()' is obsolete
BasicAttributeDemo.cs(31,3): warning CS0618:
'BasicAttributeDemo.MyThirdDeprecatedMethod()' is obsolete: 'You shouldn't use this method anymore.'
همانطور که ملاحظه میکنید، سومین اعلان صفت در این برنامه که با پارامتر همراه بود، باعث شده است تا پارامتر صفت نیز به عنوان بخشی از پیام نمایش داده شده توسط کامپایلر، نشان داده شود. در مورد دو صفت دیگر نیز مشاهده میشود که تنها پیغامی ساده تولید گردیده است.
مثال 1-16 شامل صفت دیگری نیز میباشد. این صفت STAThreadAttribute است که معمولا در ابتدای کلیه برنامههای C# و قبل از آغاز متد Main() قرار میگیرد. این صفت بیان میدارد که برنامه C# مورد نظر میتواند با کد مدیریت نشده COM از طریق Simple Threading Apartment ارتباط برقرار نماید. استفاده از این صفت در هر برنامهای میتواند مفید باشد، چراکه شما بعنوان برنامه نویس هیچگاه اطلاع ندارید که آیا کنابخانه ثالثی که از آن استفاده میکنید، قصد برقراری ارتباط با COM را دارد یا نه؟ (در صورتیکه با برخی از اصطلاحات بکار رفته آشنایی ندارید اصلا نگران نشوید. در اینجا هدف تنها نشان دادن موارد استفاده از صفتهاست.)
صفتها میتوانند دارای چندین پارامتر باشند. در مثال 2-16، استفاده از دو پارامتر برای یک صفت نشان داده شده است.
مثال 2-16
using System;
public class AnyClass
{
[Obsolete("Don't use Old method, use New method", true)]
static void Old( ) { }
static void New( ) { }
public static void Main( )
{
Old( );
}
}
همانطور که در مثال 2-16 مشاهده میکنید، صفت مورد استفاده دارای دو پارامتر است. پارامتر اول که یک جمله متنی است و همانند مثال 1-16 عمل میکند. پارامتر دوم نیز بیان کننده نوع پیغامی است که این صفت در هنگام کامپایل تولید میکند. در صورتیکه این مقدار برابر با True باشد، بدین معناست که در هنگام کامپایل پیغام خطا تولید میشود و کامپایل برنامه متوقف میگردد. در حالت پیش فرض مقدار این پارامتر برابر با False است که بیان میدارد، به هنگام کامپایل تنها پیغام هشداری تولید خواهد شد. در پیغام این برنامه، عنصری از برنامه را که نباید از آن استفاده شود معین شده و جایگزین آن نیز معرفی میشود.
AnyClass.Old()' is obsolete: 'Don't use Old method, use New method'
نکته مهمی که باید در مورد صفتها در نظر بگیرید آنست که اطلاعاتی که توسط صفت در کد برنامه قرار میگیرد، توسط سایر برنامهها نیز قابل تفسیر و استفاده است.
انواع پارامترهای صفت (پارامترهای Positional و Named)
همانطور که در بالا نیز اشاره شد، صفتها میتوانند دارای پارامتر نیز باشند. این پارامترها به دو دسته تقسیم میشوند. پارامترهای محلی (positional) و پارامترهای اسمی (named). از پارامترهای positional در زمانی استفاده میشود که میخواهیم پارامتر مورد نظر بصورت اجباری مورد استفاده قرار گیرد و البته این مسئله یک قانون نیست ! چراکه در مورد صفت Obsolete، این صفت دارای یک پارامتر positional دیگر با نام error و از نوع int نیز میباشد که ما آنرا در مثال 1-16 لحاظ نکردیم. همانطور که در مثال 2-16 مشاهده کردید، از این پارامتر positional میتوان برای ایجاد یک خطا در زمان کامپایل برنامه استفاده نمود.
[Obsolete("Don't use Old method, use New method", true)]
static void Old( ) { }
تفاوت پارامترهای positional با پارامترهای named در آنست که، پارامترهای named با نامشان مورد استفاده قرار میگیرند و همیشه اختیاری هستند. در مثال 3-16 صفت DllImport را مشاهده مینمایید که دارای هر دو نوع پارامتر positional و named است.
مثال 3-16
using System;
using System.Runtime.InteropServices;
class AttributeParamsDemo
{
[DllImport("User32.dll", EntryPoint="MessageBox")]
static extern int MessageDialog(int hWnd, string msg, string caption, int msgType);
[STAThread]
static void Main(string[] args)
{
MessageDialog(0, "MessageDialog Called!", "DllImport Demo", 0);
}
}
صفت DllImport در مثال 3-16 دارای یک پارامتر positional ("User32.dll") و یک پارامتر named (EntryPoint="MessageBox") است . پارامترهای named در هر مکانی میتوانند قرار گیرند و مانند پارامترهای positional دارای محدودیت مکانی نیستند. بدین معنا که چون در پارامترهای named، نام پارامتر مستقیما مورد استفاده قرار میگیرد، محل قرار گیری آن در لیست پارامترهای صفت مهم نیست اما در مورد پارامترهای positional چون اسم پارامتر مورد استفاده قرار نمیگیرد، این پارامترها حتما باید در مکانهای تعیین شده و تعریف شده در لیست پارامترهای صفت قرار گیرند. توجه کنید که چون هدف ما تنها آشنایی با صفتها و نحوه استفاده از آنهاست، درباره پارامترهای مختلف صفت DllImport بحث نخواهیم کرد چراکه پارامترهای این صفت نیاز به آشنایی کامل با Win32 API دارد.
در یک بررسی کلی میتوان گفت که پارامترهای Positional، پارامترهای سازنده(Constructor) صفت هستند و در هر بار استفاده از صفت باید مورد استفاده قرار گیرند، ولی پارامترهای Named کاملا اختیاری هستند و همیشه نیازی به استفاده از آنها نمیباشد.
Target های صفتها (عناصری که صفتها بر روی آنها اعمال میشوند)
صفتهایی که تا کنون مشاهده کردید، همگی بر روی متدها اعمال شده بودند. اما عناصر مختلف دیگری در C# وجود دارند که میتوان صفتها را بر روی آنها اعمال نمود. جدول 1-16 عناصر مختلف زبان C# را که صفتها بر روی آنها اعمال میشوند را نشان میدهد.
قابل اعمال به .... |
عناصر اعمال شونده |
به تمامی عناصر قابل اعمال هستند. |
all |
به تمام یک اسمبلی |
assembly |
کلاسها |
class |
سازندهها |
constructor |
Delegate ها |
delegates |
عناصر شمارشی |
enum |
رخدادها |
event |
فیلدها |
field |
واسطها |
interface |
متدها |
method |
ماژولها (کدهای کامپایل شدهای که میتوانند به عنوان قسمتی از یک اسمبلی در نظر گرفته شوند.) |
module |
پارامترها |
parameter |
Property ها |
property |
مقادیر بازگشتی |
returnvalue |
ساختارها |
struc |
هر چند ممکن است استفاده از این Target ها باعث ایجاد ابهام شوند، اما میتوان با استفاده از این Target ها معین کرد که صفت دقیقا به عنصر مورد نظر اعمال شود. یکی از صفتهایی که بر روی اسمبلی اعمال میشود و باعث ارتباط با CLS میگردد، صفت CLSCompliantAttribute است. CLS یا همان Common Language Specification امکان برقراری ارتباط بین کلیه زبانهایی که تحت .Net کار میکنند را فراهم مینماید. Target های صفتها با استفاده از اسم Target که بعد از آن کولون قرار میگیرد، ایجاد میشوند. در مثال 4-16 نحوه استفاده از این صفت نشان داده شده است.
مثال 4-16
using System;
[assembly:CLSCompliant(true)]
public class AttributeTargetDemo
{
public void NonClsCompliantMethod(uint nclsParam)
{
Console.WriteLine("Called NonClsCompliantMethod().");
}
[STAThread]
static void Main(string[] args)
{
uint myUint = 0;
AttributeTargetDemo tgtDemo = new AttributeTargetDemo();
tgtDemo.NonClsCompliantMethod(myUint);
}
}
با استفاده از Target مورد نظر در اینجا یعنی assembly، این صفت بر روی کل اسمبلی اعمال میگردد. کد موجود در مثال 4-16 کامپایل نخواهد شد، زیرا uint در متد NonClsCompliantMethod() اعلان شده است. در اینجا درصورتیکه فرم پارامتر صفت CLSCompliant را به false تغییر دهید و یا متد NonClsCompliantMethod() را به متدی منطبق با CLS تبدیل کنید (مثلا نوع بازگشتی آنرا int تعریف کنید) آنگاه برنامه کامپایل خواهد شد. (توضیحی که درباره CLS میتوانم بیان کنم اینست که CLS مجموعهای از ویژگیها و خواص .Net Framework است که به نحوی بیان میدارد، برای اینکه زبانهای مختلف تحت .Net بتوانند بدون مشکل با یکدیگر ارتباط برقرار نمایند، لازم است از یک سری از قوانین پیروی کنند، در غیر اینصورت امکان برقراری ارتباط با سایر کدهای نوسته شده تحت زبانهای برنامهسازی دیگر را نخواهند داشت. برای مثال، استفاده از نوع uint به دلیل اینکه در زبانهای مختلف میتواند به صورتهای متفاوتی پیادهسازی شود و یا وجود نداشته باشد، سازگار با CLS نیست و برای اینکه بخواهیم برنامهای منطبق با CLS داشته باشیم نباید از آن استفاده نماییم.)
نکته قابل توجه در مورد مثال 4-16 آنست که در این مثال صفت CLSCompliant به استفاده از یک Target که همان assembly است، مورد استفاده قرار گرفته است و از اینرو تمامی مشخصات این صفت به کلیه اعضای این اسمبلی اعمال خواهند شد. توجه نمایید که در این مثال علت و موارد استفاده از صفتها مشهودتر است، چراکه همانطور که مشاهده مینمایید، با استفاده از یک صفت میتوانیم کنترلی بر روی کل اسمبلی و برنامه قرار دهیم تا در صورتیکه میخواهیم برنامه ما با سایر زبانهای برنامهسازی تحت .Net ارتباط برقرار کند، از متدهای استاندارد و سازگار با CLS استفاده نماییم که این قابلیت بزرگی را در اختیار ما قرار خواهد داد.
تولید صفتهای شخصی
پس از اینکه با طریقه استفاده از صفتهای موجود در زبان آشنا شدید، حال نوبت به ساخت صفتهای شخصی میرسد. برای تولید یک صفت (Attribute) باید یک کلاس ایجاد نماییم و این کلاس باید از System.Attribute مشتق شود. کلاسی که از System.Attribute مشتق میشود (چه بطور مستقیم و چه بطور غیر مستقیم) یک کلاس صفت(Attribute Class) است. اعلان کلاس صفت باعث ایجاد صفت جدیدی میشود که میتوان از آن در برنامه استفاده نمود. به مثال 5-16 توجه فرمایید.
مثال 5-16
using System;
public class HelpAttribute : Attribute
{
}
در این مثال به سادگی یک صفت جدید تولید کردهایم و میتوانیم از آن استفاده کنیم.
[Help()]
public class AnyClass
{
}
همانطور که قبلا نیز گفتیم استفاده از کلمه Attribute به دنبال نام صفت الزامی نیست. صفتی که در اینجا ایجاد کردهایم عملا کار خاصی برای ما انجام نمیدهد پس اندکی در کد آن تغییر ایجاد میکنیم تا مفیدتر باشد.
مثال 6-16
using System;
public class HelpAttribute : Attribute
{
public HelpAttribute(String Descrition_in)
{
this.description = Description_in;
}
protected String description;
public String Description
{
get
{
return this.description;
}
}
}
[Help("this is a do-nothing class")]
public class AnyClass
{
}
هماطور که مشاهده میکنید با اضافه کردن چند خط کد توانستیم این صفت را کاراتر کنیم. با قرار دادن یک property در این صفت، پارامتر این صفت بعنوان پیغام نمایش داده میشود.
درس پانزدهم - برخورد با استثناها (Exception Handling)
در این درس با چگونگی برخورد با استثناها (یا خطاهای غیر قابل پیشبینی) در زبان برنامهسازی C# آشنا میشویم. اهداف ما در این درس بشرح زیر میباشد :
1) درک و فهم صحیح یک استثناء یا Exception
2) پیادهسازی یک روتین برای برخورد با استثناها بوسیله بلوک try/catch
3) آزادسازی منابع تخصیص داده شده به یک برنامه در یک بلوک finally
استثناها، در حقیقت خطاهای غیر منتظره در برنامههای ما هستند. اکثراً، میتوان و باید روشهایی را جهت برخورد با خطاهای موجود در برنامه در نظر گرفت و آنها را پیادهسازی کرد. بعنوان مثال، بررسی و تایید دادههای ورودی کاربران، بررسی اشیاء تهی یا Null و یا بررسی نوع بازگشتی متد ها، میتوانند از جمله مواردی باشند که باید مورد بررسی قرار گیرند. این خطاها، خطاهایی معمول و رایجی هستند که اکثر برنامهنویسان از آنها مطلع بوده و راههایی را برای بررسی آنها در نظر میگیرند تا از وقوع آنها جلوگیری نمایند.
اما زمانهایی وجود دارند که از اتفاق افتادن یک خطا در برنامه بی اطلاع هستید و انتظار وقوع خطا در برنامه را ندارید. بعنوان مثال، هرگز نمیتوان وقوع یک خطای I/O را پیشبینی نمود و یا کمبود حافظه برای اجرای برنامه و از کار افتادن برنامه به این دلیل. این موارد بسیار غیر منتظره و ناخواسته هستند، اما در صورت وقوع بهتر است بتوان راهی برای مقابله و برخورد با آنها پیدا کرده و با آنها برخورد نمود. در این جاست که مسئله برخورد با استثناها (Exception Handling) مطرح میشود.
هنگامیکه استثنایی رخ میدهد، در اصطلاح میگوئیم که این استثناء، thrown شده است. در حقیقت thrown، شیءای است مشتق شده از کلاس System.Exception که اطلاعاتی در مورد خطا یا استثناء رخ داده را نشان میدهد. در قسمتهای مختلف این درس با روش مقابله با استثناها با استفاده از بلوک های try/catch آشنا خواهید شد.
کلاس System.Exception حاوی تعداد بسیار زیادی متد و property است که اطلاعات مهمی در مورد استثناء و خطای رخ داده را در اختیار ما قرار میدهد. برای مثال، Message یکی از property های موجود در این کلاس است که اطلاعاتی درباره نوع استثناء رخ داده در اختیار ما قرار میدهد. StackTrace نیز، اطلاعاتی در مورد Stack (پشته) و محل وقوع خطا در Stack در اختیار ما قرار خواهد داد.
تشخیص چنین استثناهایی، دقیقاً با روتینهای نوشته شده توسط برنامهنویس در ارتباط هستند و بستگی کامل به الگوریتمی دارد که وی برای چنین شرایطی در نظر گرفته است. برای مثال، در صورتیکه با استفاده از متد System.IO.File.OpenRead()، اقدام به باز کردن فایلی نماییم، احتمال وقوع (Thrown) یکی از استثناهای زیر وجود دارد :
SecurityException
ArgumentException
ArgumentNullException
PathTooLongException
DirectoryNotFoundException
UnauthorizedAccessException
FileNotFoundException
NotSupportedException
با نگاهی بر مستندات .Net Framework SDK، به سادگی میتوان از خطاها و استثناهایی که ممکن است یک متد ایجاد کند، مطلع شد. تنها کافیست به قسمت Reference/Class Library رفته و مستندات مربوط به Namespace/Class/Method را مطالعه نمایید. در این مستندات هر خطا دارای لینکی به کلاس تعریف کننده خود است که با استفاده از آن میتوان متوجه شد که این استثناء به چه موضوعی مربوط است. پس از اینکه از امکان وقوع خطایی در قسمتی از برنامه مطلع شدید، لازم است تا با استفاده از مکانیزمی صحیح به مقابله با آن بپردازید.
هنگامیکه یک استثناء در اصطلاح thrown میشود (یا اتفاق میافتد) باید بتوان به طریقی با آن مقابله نمود. با استفاده از بلوکهای try/catch میتوان چنین عملی را انجام داد. پیادهسازی این بلوکها بدین شکل هستند که، کدی را که احتمال تولید استثناء در آن وجود دارد را در بلوک try، و کد مربوط به مقابله با این استثناء رخ داده را در بلوک catch قرار میدهیم. در مثال 1-15 چگونگی پیادهسازی یک بلوک try/catch نشان داده شده است. بدلیل اینکه متد OpenRead() احتمال ایجاد یکی از استثناهای گفته شده در بالا را دارد، آنرا در بلوک try قرار داده ایم. در صورتیکه این خطا رخ دهد، با آن در بلوک catch مقابله خواهیم کرد. در مثال 1-15 در صورت بروز استثناء، پیغامی در مورد استثناء رخ داده و اطلاعاتی در مورد محل وقوع آن در Stack برای کاربر بر روی کنسول نمایش داده میشود.
نکته : توجه نمایید که کلیه مثالهای موجود در این درس به طور تعمدی دارای خطاهایی هستند تا شما با نحوه مقابله با استثناها آشنا شوید.
using System;
using System.IO;
class TryCatchDemo
{
static void Main(string[] args)
{
try
{
File.OpenRead("NonExistentFile");
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
}
هر چند کد موجود در مثال 1-15 تنها داری یک بلوک catch است، اما تمامی استثناهایی که ممکن است رخ دهند را نشان داده و مورد بررسی قرار میدهد زیرا از نوع کلاس پایه استثناء، یعنی Exception تعریف شده است. در کنترل و مقابله با استثناها، باید استثناهای خاص را زودتر از استثناهای کلی مورد بررسی قرار داد. کد زیر نحوه استفاده از چند بلوک catch را نشان میدهد :
catch(FileNotFoundException fnfex)
{
Console.WriteLine(fnfex.ToString());
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}
در این کد، در صورتیکه فایل مورد نظر وجود نداشته باشد، FileNotFoundException رخ داده و توسط اولین بلوک catch مورد بررسی قرار میگیرد. اما در صورتیکه PathTooLongException رخ دهد، توسط دومین بلوک catch بررسی خواهد شد. علت آنست که برای PathTooLongException بلوک catch ای در نظر گرفته نشده است و تنها گزینه موجود جهت بررسی این استثناء بلوک کلی Exception است. نکته ای که در اینجا باید بدان توجه نمود آنست که هرچه بلوکهای catch مورد استفاده خاص تر و جزئی تر باشند، پیغامها و اطلاعات مفیدتری در مورد خطا میتوان بدست آورد.
استثناهایی که مورد بررسی قرار نگیرند، در بالای Stack نگهداری می شوند تا زمانیکه بلوک try/catch مناسبی مربوط به آنها یافت شود. در صورتیکه برای استثناء رخ داده بلوک try/catch در نظر گرفته نشده باشد، برنامه متوقف شده و پیغام خطایی ظاهر میگردد. این چنین حالتی بسیار نا مناسب بوده و کاربران را دچار آشفتگی خواهد کرد. استفاده از روشهای مقابله با استثناها در برنامه، روشی مناسب و رایج است و باعث قدرتمند تر شدن برنامه میشود.
یکی از حالتهای بسیار خطرناک و نامناسب در زمان وقوع استثناها، هنگامی است که استثناء یا خطای رخ داده باعث از کار افتادن برنامه شود ولی منابع تخصیص داده شده به آن برنامه آزاد نشده باشند. هر چند بلوک catch برای برخورد با استثناها مناسب است ولی در مورد گفته شده نمی تواند کمکی به حل مشکل نماید. برای چنین شرایطی که نیاز به آزادسازی منابع تخصیص داده شده به یک برنامه داریم، از بلوک finally استفاده میکنیم.
کد نشان داده شده در مثال 2-15، به خوبی روش استفاده از بلوک finally را نشان میدهد. همانطور که حتماً میدانید، رشته های فایلی پس از اینکه کار با آنها به اتمام میرسد باید بسته شوند، در غیر اینصورت هیچ برنامه دیگری قادر به استفاده از آنها نخواهد بود. در این حالت، رشته فایلی، منبعی است که میخواهیم پس از باز شدن و اتمام کار، بسته شده و به سیستم باز گردد. در مثال 2-15، outStream با موفقیت باز میشود، بدین معنا که برنامه handle ای به یک فایل باز شده در اختیار دارد. اما زمانیکه میخواهیم inStraem را باز کنیم، استثناء FileNotFound رخ داده و باعث میشود که کنترل برنامه سریعاً به بلوک catch منتقل گردد.
در بلوک catch میتوانیم فایل outStream را ببندیم. اما برنامه تنها زمانی به بلوک catch وارد میشود که استثنایی رخ دهد. پس اگر هیچ استثنائی رخ نداده و برنامه به درستی عمل نماید، فایل باز شده outStream هرگز بسته نشده و یکی از منابع سیستم به آن بازگردانده نمیشود. بنابراین باید برای بستن این فایل نیز فکری کرد. این کاری است که در بلوک finally رخ می دهد. بدین معنا که در هر حالت، چه برنامه با استثنائی روبرو شود و چه نشود، قبل از خروج از برنامه فایل باز شده، بسته خواهد شد. در حقیقت میتوان گفت بلوک finally، بلوکی است که تضمین مینماید در هر شرایطی اجرا خواهد شد. پس برای حصول اطمینان از اینکه منابع مورد استفاده برنامه پس از خروج برنامه، به سیستم باز گردانده میشوند، میتوان از این بلوک استفاده کرد.
using System;
using System.IO;
class FinallyDemo
{
static void Main(string[] args)
{
FileStream outStream = null;
FileStream inStream = null;
try
{
outStream = File.OpenWrite("DestinationFile.txt");
inStream = File.OpenRead("BogusInputFile.txt");
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}
finally
{
if (outStream != null)
{
outStream.Close();
Console.WriteLine("outStream closed.");
}
if (inStream != null)
{
inStream.Close();
Console.WriteLine("inStream closed.");
}
}
}
}
استفاده از بلوک finally الزامی نیست، اما روشی مناسب برای بالا بردن کارآیی برنامه است. ممکن است سوالی در اینجا مطرح شود : در صورتیکه پس از بلوک catch و بدون استفاده از بلوک finally، فایل باز شده را ببندیم، باز هم منبع تخصیص داده شده به برنامه آزاد می شود. پس چه دلیلی برای استفاده از بلوک finally وجود دارد؟ در پاسخ به این سوال باید گفت، در شرایط نرمال که تمامی برنامه بطور طبیعی اجرا میشود و اتفاق خاصی رخ نمیدهد، می توان گفت که دستورات بعد از بلوک catch اجرا شده و منبع تخصیص داده شده به سیستم آزاد می شود. اما برای بررسی همیشه باید بدترین حالت را در نظر گرفت. فرض کنید درون خود بلوک catch استثنائی رخ دهد که شما آنرا پیشبینی نکردهاید و یا این استثناء باعت متوقف شدن برنامه شود، در چنین حالتی کدهای موجود بعد از بلوک catch هرگر اجرا نخواهند شد و فایل همچنان باز میماند. اما با استفاده از بلوک finally میتوان مطمئن بود که کد موجود در این بلوک حتماً اجرا شده و منبع تخصیص داده شده به برنامه آزاد میگردد.
در اینجا به پایان درس پانزدهم رسیدیم. هم اکنون می بایست درک صحیحی از استثناء بدست آورده باشید. همچنین میتوانید به سادگی الگوریتمهایی جهت بررسی استثناها بوسیله بلوکهای try/catch پیادهسازی نمایید. بعلاوه میتوانید با ساتفاده از بلوک finally مطمئن باشید که که منابع تخصیص داده شده به برنامه، به سیستم باز خواهند گشت چراکه این بلوک حتما اجرا میشود و میتوان کدهای مهمی را که میخواهیم تحت هر شرایطی اجرا شوند را درون آن قرار داد.
درس چهاردهم – رخدادها و 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 خواهیم پرداخت.