دسته بندی وبلاگ

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