درس پانزدهم - برخورد با استثناها (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 مطمئن باشید که که منابع تخصیص داده شده به برنامه، به سیستم باز خواهند گشت چراکه این بلوک حتما اجرا میشود و میتوان کدهای مهمی را که میخواهیم تحت هر شرایطی اجرا شوند را درون آن قرار داد.