مدیریت استثنا (خطا) در سی شارپ

در هنگام برنامه‌نویسی اشتباه کردن یک امر متداول است؛ حتی حرفه‌ای‌ترین و کارآزموده‌ترین برنامه‌نویس‌ها هم ممکن است در ساده‌ترین مسائل مرتکب خطا شوند. این خطا می‌تواند حاصل اشتباه محاسباتی، منطق غلط و یا حتی یک اشتباه تایپی باشد.

به طور معمول IDEهای کدنویسی آن‌قدر هوشمند هستند که می‌توانند خطا و مکان آن را تشخیص داده و در مواردی راه اصلاحش را به برنامه‌نویس پیشنهاد دهند. اما خطاهایی هم وجود دارند که برنامه‌نویس و کامپایلر قادر به تشخیصشان نیستند و بعد از کامپایل شدن و هنگام اجرای برنامه خودشان را نشان می‌دهند.

به خطاهایی که در زمان اجرای برنامه رخ می‌دهند استثنا (به انگلیسی: exception) گفته می‌شود. از آن‌جایی که برنامه‌نویس بعد از اجرا شدن برنامه امکان مدیریت کد و اصلاح آن را ندارد کد به هیچ‌وجه نباید باعث بروز خطا و مشکل شود؛ چرا که حتی یک استثنا می‌تواند باعث کرش کردن برنامه شود.

با این حال در برخی از موارد رخ دادن یک خطا خارج از کنترل برنامه‌نویس است. برای مثال فرض کنید یک برنامه یا بازی باید از طریق اینترنت به یک دیتابیس متصل شده و از روی آن اطلاعاتی را بخواند. دلایل زیادی وجود دارد که ممکن است این فرآیند با مشکل روبرو شود؛ دلایلی مثل قطع بودن اینترنت، شلوغی سرور و عدم پاسخگویی، حذف شدن فایل از روی سرور و…

در چنین مواردی خطاهایی رخ می‌دهند که برنامه‌نویس نقشی در ایجادشان ندارد. در این حالت برای جلوگیری از کرش کردن برنامه باید تمام حالاتی که ممکن است موجب بروز خطا شوند را شناسایی کرده و تعیین کرد که در صورت بروز خطا کد جایگزین اجرا شود (به عنوان مثال پیام «دریافت داده با خطا مواجه شد» به کاربر نمایش داده شود). به این کار مدیریت استثنا گفته می‌شود.

مدیریت استثنا به عملیاتی گفته می‌شود که در طی آن قابلیت‌هایی به کد اضافه می‌شود تا در برابر هر نوع استثنا عکس‌العمل مناسب داده شده و از کرش برنامه جلوگیری شود.

پیش از بررسی نحوه‌ی مدیریت استثنا در سی شارپ ابتدا با انواع استثناهای این زبان آشنا می‌شویم. در زبان سی شارپ دو نوع استثنا وجود دارد.

استثناهای ایجاد شده توسط CLR

این استثناها به هنگام برخورد با مشکلاتی که وقوع آن‌ها قبلاً توسط دات نت پیش‌بینی شده «پرتاب» می‌شوند (دلیل استفاده از کلمه‌ی پرتاب در ادامه توضیح داده می‌شود).

برای مثال کد زیر را در نظر بگیرید:

using System;
class Program
{
static void Main(string[] args)
{
int zero = 0;
int a = 10 / zero;
//Console.ReadKey();
}
}

در این کد یک عدد بر صفر تقسیم می‌شود که این عمل در ریاضی امکان‌پذیر نیست. بنابراین در صورتی که تلاشی برای اجرای این کد انجام شود در هنگام اجرای کد به همراه Debug (با کلید میانبر F5)، اجرا به محض رسیدن به تقسیم عدد به صفر توسط debugger متوقف (break) شده و به محیط IDE (در اینجا ویژوال استودیو) باز خواهیم گشت:

image

همانطور که در تصویر مشاهده می‌شود کامپایلر یک استثنا از نوع DivideByZeroException گرفته و اخطاری مبنی بر handle نبودن (مدیریت نشدن) آن داده می‌شود. اگر برنامه در حالت Debug اجرا نمی‌شد (برای مثال با انتخاب گزینه Start Without Debugging از منوی Debug) یک exception مدیریت (handle) نشده می‌توانست باعث کرش کردن برنامه شود.

در سی شارپ استثناها در واقع کلاس هستند و هنگام بروز خطا از روی استثنای مربوطه شی ساخته می‌شود (Exception Object). اگر بخواهیم بیشتر به این موضوع وارد شویم باید بدانیم کلاسی به نام Exception وجود دارد که از کلاس Object مشتق شده است. این کلاس خود فرزندی تحت عنوان SystemException دارد که تمامی استثناهای از قبل ایجاد شده‌ی دات نت فریمورک از این کلاس (و در چند مورد از فرزندان این کلاس) ارث‌بری می‌کنند.

استثناهای متداول دات نت فریمورک که از قبل ایجاد شده‌اند

استثناهایی که از کلاس SystemException مشتق شده‌اند

  • IndexOutOfRangeException: هنگام تلاش کاربر برای دسترسی به عضوی از آرایه که index آن در خارج از محدوده مجاز قرار دارد پرتاب می‌شود.

  • NullReferenceException: هنگام ارجاع به یک شی null پرتاب می‌شود.

  • AccessViolationException: هنگام تلاش کاربر برای دسترسی به حافظه نامعتبر (مانند کد مدیریت نشده، کد Unsafe نادرست و یا استفاده از اشاره‌گر نامعتبر) پرتاب می‌شود.

  • InvalidOperationException: توسط متد و هنگام رسیدن آن به حالتی نامعتبر پرتاب می‌شود.

  • ArgumentException: کلاس پایه Argument Exceptionها

  • ExternalException: کلاس پایه استثناهایی که در خارج از runtime اتفاق افتاده یا هدفگیری می‌شوند.

استثناهایی که از کلاس ArgumentException مشتق شده‌اند

  • ArgumentNullException: هنگام قرارگیری یک مقدار null به عنوان آرگومان در یک متد، توسط آن متد پرتاب می‌شود.

  • ArgumentOutOfRangeException: هنگام قرارگیری یک مقدار خارج از بازه و بی‌معنی به عنوان آرگومان در یک متد، توسط آن متد پرتاب می‌شود. البته اگر این مقدار index یک آرایه باشد با استثنای IndexOutOfRangeException روبرو می شویم.

مدیریت خطا با بلوک‌های try/catch

در برنامه‌نویسی هنگام بروز یک استثنا از فعل پرتاب شدن (throw) استفاده می‌شود. دلیل استفاده از فعل فوق این است که برنامه‌نویس باید در نقاطی از کد که به صورت بالقوه امکان ایجاد خطایی وجود دارد اقدام به «گرفتن» یا catch کردن آن خطا نماید تا موجب کرش کردن برنامه نشود. برای پیاده‌سازی چنین منطقی از بلوک‌های try/catch استفاده می‌کنیم. این ساختار از دو بلوک به نام‌های بلوک try و بلوک catch تشکیل شده است.

  • در بلوک try کدهایی را می‌نویسیم که ممکن است با بروز خطا مواجه شوند.

  • در بلوک catch کدهایی را می‌نویسیم که می‌خواهیم در صورت بروز خطا (هنگام اجرای کدهای بلوک try) اجرا شوند.

یک ساختار try/catch به صورت زیر می‌باشد:

try
{
//کدهایی که اجرای موفق آن‌ها خارج از کنترل برنامه‌نویس است
}
catch (Exception e)
{
//کدهایی که در صورت بروز خطا هنگام اجرای بلوک ترای اجرا می‌شوند
}

همان‌طور که در کد بالا قابل ملاحظه است بلوک catch می‌تواند همانند یک متد دارای پارامتر باشد. این پارامتر که در کلی‌ترین حالت خود از نوع داده‌ی Exception است به شی خطایی اشاره می‌کند که موجب فراخوانی بلوک catch شده است (در کد بالا Exception e). با استفاده از این پارامتر می‌توان در بلوک catch متدهایی را بر روی این شی صدا زد (مثل متدهایی که پیغام خطا را در قالب string به کاربر بازمی‌گردانند).

اصلی‌ترین نوع استثنا نوع داده‌ی Exception است که تمام استثناهای دیگر از روی آن ارث‌بری کرده‌اند و بنابراین استفاده از این نوع داده به عنوان پارامتر در بلوک catch باعث می‌شود تمام استثناهای رخ داده هنگام اجرای بلوک try توسط این بلوک گرفته شوند. اما در صورتی که بخواهید برنامه به ازای استثناهای تخصصی‌تر کدهای مختلفی را اجرا کند می‌توانید برای هرکدام بلوک catch جداگانه بنویسید. در این حالت هر بار که یک استثنا پرتاب می‌شود بلوک catch مربوطه آن را دریافت کرده و سپس به جای کرش کردن برنامه آن بلوک catch اجرا می‌شود.