مقدمه

در درس قبل با مبانی مدیریت استثناء و نحوه‌ی استفاده از بلوک try-catch-finally آشنا شدیم. تمام استثناها در .NET از کلاس پایه‌ی System.Exception ارث‌بری می‌کنند، اما آن‌ها را می‌توان به دو دسته‌ی کلی تقسیم کرد: استثناهای سطح سیستم (System-Level Exceptions) که توسط Common Language Runtime (CLR) پرتاب می‌شوند و خطاهای پایه‌ای و سیستمی را نشان می‌دهند، و استثناهای سطح اپلیکیشن (Application-Level Exceptions) که توسط خود برنامه‌نویس برای مدیریت خطاهای مربوط به منطق کسب‌وکار (business logic) ایجاد و پرتاب می‌شوند. درک تفاوت این دو دسته برای طراحی یک استراتژی مدیریت خطای مؤثر و خوانا ضروری است.

استثناهای سطح سیستم (System-Level Exceptions)

این دسته از استثناها از کلاس پایه‌ی System.SystemException ارث‌بری می‌کنند و معمولاً نشان‌دهنده‌ی مشکلاتی هستند که در سطح پایین و در زمان اجرا توسط CLR شناسایی می‌شوند. این خطاها اغلب به دلیل اشتباهات برنامه‌نویسی یا مشکلات محیطی رخ می‌دهند و در بسیاری از موارد، قابل بازیابی نیستند.

برخی از رایج‌ترین استثناهای سطح سیستم عبارتند از:

  • NullReferenceException: تلاش برای دسترسی به یک عضو از یک متغیر که مقدار آن `null` است.
  • IndexOutOfRangeException: تلاش برای دسترسی به یک عنصر آرایه با اندیسی که خارج از محدوده‌ی مجاز است.
  • StackOverflowException: یک خطای بحرانی که به دلیل پر شدن حافظه‌ی Stack (معمولاً ناشی از یک بازگشت بی‌نهایت) رخ می‌دهد و تقریباً همیشه منجر به پایان برنامه می‌شود.
  • OutOfMemoryException: یک خطای بحرانی دیگر که زمانی رخ می‌دهد که برنامه دیگر نمی‌تواند حافظه‌ی مورد نیاز خود را از سیستم‌عامل دریافت کند.
  • InvalidCastException: تلاش برای تبدیل (cast) یک نوع به نوع دیگری که با آن سازگار نیست.

بهترین استراتژی برای مقابله با این خطاها، پیشگیری است، نه مدیریت. یعنی باید کدی بنویسیم که از بروز آن‌ها جلوگیری کند (مثلاً با بررسی null بودن متغیرها قبل از استفاده). گرفتن (catch) این نوع خطاها، به خصوص خطاهای بحرانی، معمولاً کار درستی نیست، زیرا برنامه احتمالاً در وضعیت ناپایداری قرار گرفته است.

استثناهای سطح اپلیکیشن (Application-Level Exceptions)

این استثناها، که معمولاً از کلاس پایه‌ی System.ApplicationException ارث‌بری می‌کنند، برای مدل‌سازی خطاهایی به کار می‌روند که مختص منطق برنامه‌ی شما هستند. این‌ها خطاهایی نیستند که CLR تشخیص دهد، بلکه خطاهایی هستند که شما به عنوان برنامه‌نویس، بر اساس قوانین کسب‌وکار خود، آن‌ها را تعریف و پرتاب می‌کنید.

چرا یک استثناء سفارشی بسازیم؟

ایجاد استثناهای سفارشی، خوانایی و ساختار کد شما را به شدت بهبود می‌بخشد. به جای پرتاب یک Exception عمومی با یک پیام متنی، شما یک نوع استثناء با نامی گویا تعریف می‌کنید. این کار به کدی که از کلاس شما استفاده می‌کند اجازه می‌دهد تا خطاهای مختلف را به صورت تفکیک‌شده و تمیز مدیریت کند. برای مثال، catch (InsufficientFundsException) بسیار واضح‌تر از catch (Exception) است.

چگونه یک استثناء سفارشی بسازیم؟

ساختن یک استثناء سفارشی بسیار ساده است. کافی است یک کلاس جدید تعریف کنید که از ApplicationException (یا مستقیماً از Exception) ارث‌بری کند. طبق قرارداد، نام کلاس‌های استثناء باید به کلمه‌ی Exception ختم شود.

Copy Icon Program.cs
// A custom exception for a specific business rule violation.
public class InvalidLoginException : ApplicationException
{
    // A default constructor.
    public InvalidLoginException() { }

    // A constructor that accepts a message.
    public InvalidLoginException(string message) : base(message) { }
    
    // A constructor for wrapping another exception.
    public InvalidLoginException(string message, Exception innerException) 
        : base(message, innerException) { }
}

در این مثال، ما یک استثناء سفارشی به نام InvalidLoginException تعریف کرده‌ایم. ما سه سازنده‌ی استاندارد برای آن فراهم کرده‌ایم که سازنده‌های کلاس پایه‌ی ApplicationException را فراخوانی می‌کنند. سازنده‌ی سوم که یک innerException می‌پذیرد، برای زمانی مفید است که می‌خواهید یک استثناء سطح پایین‌تر را در قالب استثناء خودتان بسته‌بندی کنید تا اطلاعات خطای اصلی از بین نرود.

پرتاب و گرفتن استثناهای سفارشی

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

Copy Icon Program.cs
public class AuthService
{
    public void Login(string username, string password)
    {
        // Simulate a login failure.
        if (username != "admin" || password != "1234")
        {
            // Throw our custom application-level exception.
            throw new InvalidLoginException("Invalid username or password.");
        }
        Console.WriteLine("Login successful!");
    }
}

// --- Usage ---
AuthService auth = new AuthService();
try
{
    auth.Login("admin", "wrong_password");
}
// Catching our specific custom exception.
catch (InvalidLoginException ex)
{
    Console.WriteLine($"Login failed: {ex.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"An unknown error occurred: {ex.Message}");
}

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