مقدمه

هیچ برنامه‌ای کامل نیست و خطاها بخش جدایی‌ناپذیر فرآیند توسعه نرم‌افزار هستند. این خطاها می‌توانند ناشی از اشتباهات برنامه‌نویس، ورودی نامعتبر کاربر، یا شرایط پیش‌بینی‌نشده‌ی محیطی مانند قطع شدن اتصال شبکه یا عدم وجود یک فایل باشند. اگر این خطاها به درستی مدیریت نشوند، می‌توانند منجر به از کار افتادن ناگهانی برنامه (crash) و تجربه‌ی کاربری بسیار بدی شوند. مدیریت استثناء (Exception Handling) مکانیزم ساختاریافته و مدرنی است که C# و فریم‌ورک .NET برای شناسایی و پاسخ به این شرایط استثنایی در زمان اجرا فراهم می‌کنند. در این درس، با بلوک try-catch-finally به عنوان ابزار اصلی مدیریت خطا آشنا خواهیم شد.

مشکل استثناهای مدیریت‌نشده

وقتی یک خطای جدی در زمان اجرا رخ می‌دهد، .NET یک استثناء (Exception) را "پرتاب" (throw) می‌کند. یک استثناء، شیئی است که اطلاعاتی در مورد خطا در خود دارد. اگر این استثناء در هیچ کجای برنامه "گرفته" (catch) و مدیریت نشود، به آن استثناء مدیریت‌نشده (unhandled exception) گفته می‌شود و نتیجه‌ی آن پایان یافتن فوری اجرای برنامه است.

Copy Icon Program.cs
int numerator = 10;
int denominator = 0;

// This line will throw a DivideByZeroException.
int result = numerator / denominator;

Console.WriteLine("This line will never be reached.");

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

بلوک try-catch: به دام انداختن خطاها

راه‌حل اصلی برای مدیریت خطاها، استفاده از بلوک try-catch است. این ساختار به ما اجازه می‌دهد کدهای پرخطر را در یک بلوک try قرار داده و در صورت بروز خطا، آن را در بلوک catch مدیریت کنیم.

  • بلوک try: کدی که پتانسیل پرتاب کردن استثناء را دارد، در این بلوک قرار می‌گیرد.
  • بلوک catch: اگر و فقط اگر استثنائی در بلوک try رخ دهد، اجرای عادی متوقف شده و کنترل برنامه بلافاصله به این بلوک منتقل می‌شود. منطق مدیریت خطا در اینجا قرار می‌گیرد.
Copy Icon Program.cs
int numerator = 10;
int denominator = 0;

try
{
    Console.WriteLine("Attempting to divide...");
    int result = numerator / denominator;
    Console.WriteLine($"Result: {result}"); // This line is skipped.
}
catch
{
    Console.WriteLine("An error occurred! Division by zero is not allowed.");
}

Console.WriteLine("Program execution continues...");

اکنون، به جای از کار افتادن برنامه، یک پیام دوستانه چاپ شده و برنامه به اجرای خود ادامه می‌دهد. ما با موفقیت خطا را مدیریت کردیم.

گرفتن استثناهای خاص

یک بلوک catch خالی، هر نوع استثنائی را می‌گیرد. اما این کار معمولاً رویه‌ی خوبی نیست، زیرا ما اطلاعات مربوط به نوع خطا را از دست می‌دهیم. روش بهتر، گرفتن انواع خاصی از استثناهاست تا بتوانیم برای هر نوع خطا، پاسخ متفاوتی داشته باشیم. برای این کار، نوع استثناء را در پرانتز جلوی catch مشخص می‌کنیم.

Copy Icon Program.cs
Console.Write("Enter a number: ");
string userInput = Console.ReadLine();

try
{
    int number = int.Parse(userInput);
    Console.WriteLine($"You entered: {number}");
}
// Catching a specific exception type
catch (FormatException ex)
{
    Console.WriteLine("Invalid format. Please enter a valid integer.");
    Console.WriteLine($"Error details: {ex.Message}");
}
// A general catch block for any other unexpected errors
catch (Exception ex)
{
    Console.WriteLine("An unexpected error occurred.");
    Console.WriteLine($"Error details: {ex.Message}");
}

در این مثال، اگر کاربر یک متن غیرعددی (مثلاً "abc") وارد کند، متد int.Parse یک FormatException پرتاب می‌کند که توسط اولین بلوک catch گرفته می‌شود. اگر خطای دیگری رخ دهد، توسط بلوک catch عمومی دوم مدیریت می‌شود. متغیر ex که شیء استثناء را در خود نگه می‌دارد، حاوی اطلاعات مفیدی مانند ex.Message (پیام خطا) است که برای لاگ‌گیری بسیار مفید است. ترتیب بلوک‌های catch مهم است و باید همیشه از خاص‌ترین به عمومی‌ترین نوع مرتب شوند.

بلوک finally: کدی که همیشه اجرا می‌شود

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

Copy Icon Program.cs
System.IO.StreamReader reader = null;
try
{
    reader = new System.IO.StreamReader("nonexistent_file.txt");
    Console.WriteLine(reader.ReadToEnd());
}
catch (System.IO.FileNotFoundException ex)
{
    Console.WriteLine(ex.Message);
}
finally
{
    // This block executes whether the file was found or not.
    // It's crucial for resource cleanup.
    if (reader != null)
    {
        reader.Close();
    }
    Console.WriteLine("The 'finally' block has been executed.");
}

در این مثال، حتی با وجود اینکه فایل پیدا نمی‌شود و یک استثناء رخ می‌دهد، بلوک finally اجرا شده و تلاش می‌کند تا منابع را آزاد کند. این الگو برای جلوگیری از نشت منابع (resource leaks) در برنامه بسیار حیاتی است.

پرتاب کردن استثناء با throw

شما نه تنها می‌توانید استثناهای تولید شده توسط فریم‌ورک را بگیرید، بلکه می‌توانید استثناهای خودتان را نیز "پرتاب" کنید. این کار زمانی مفید است که در منطق برنامه‌ی شما یک وضعیت غیرمجاز رخ می‌دهد. برای این کار از کلمه‌ی کلیدی throw استفاده می‌کنیم.

public void SetAge(int age)
{
    if (age < 0)
    {
        // Throw a new exception to signal an invalid argument.
        throw new ArgumentOutOfRangeException("Age cannot be negative.");
    }
    // ...
}

این کار به کدی که متد شما را فراخوانی می‌کند، اجازه می‌دهد تا با استفاده از بلوک try-catch این خطای منطقی را مدیریت کند.