Exceptions לעומק — hierarchy וחריגות מותאמות אישית
מה זה?
ביחידת היסודות תפסנו סוגי חריגה ספציפיים (FormatException, IndexOutOfRangeException) בנפרד. אבל מה אם רוצים לתפוס “כל שגיאה שקשורה לקלט”, בלי לכתוב catch נפרד לכל תת-סוג? ומה אם השגיאה היא לא טכנית אלא עסקית — כמו “יתרה לא מספיקה”? הפתרון: כל חריגות ה-.NET יורשות מ-Exception בהיררכיה אחת, ואפשר ליצור חריגות מותאמות-אישית משלנו שמייצגות בדיוק את השגיאות העסקיות של הדומיין שלנו.
מילות מפתח שחשוב לזכור
• Exception Hierarchy — כל סוגי החריגות ב-.NET יורשים (בסופו של דבר) מ-Exception; catch (Exception) תופס הכל
• catch לפי סוג בסיס — תפיסת catch (IOException) תופס גם תתי-סוגים ספציפיים יותר שלה (כמו FileNotFoundException)
• Custom Exception — מחלקה שיורשת מ-Exception (או תת-סוג שלו), מייצגת שגיאה עסקית ספציפית לדומיין
• throw — זריקת חריגה חדשה באופן יזום, כשמצב עסקי לא תקין מתגלה
// Hierarchy — a more general catch also catches more specific types
try
{
// code that might throw FileNotFoundException, a subtype of IOException
}
catch (FileNotFoundException)
{
Console.WriteLine("Specific file not found"); // most specific — checked first
}
catch (IOException)
{
Console.WriteLine("More general I/O error"); // catches all other IO errors
}
catch (Exception)
{
Console.WriteLine("Unexpected general error"); // last safety net
}// Custom Exception — a specific business error, not a technical one
class InsufficientFundsException : Exception
{
public double RequestedAmount { get; }
public double AvailableBalance { get; }
public InsufficientFundsException(double requested, double available)
: base($"Attempted to withdraw {requested}, but the available balance is only {available}")
{
RequestedAmount = requested;
AvailableBalance = available;
}
}
class BankAccount
{
public double Balance { get; private set; }
public void Withdraw(double amount)
{
if (amount > Balance)
throw new InsufficientFundsException(amount, Balance); // thrown deliberately
Balance -= amount;
}
}try
{
var account = new BankAccount();
account.Withdraw(500);
}
catch (InsufficientFundsException ex)
{
Console.WriteLine($"{ex.Message} (missing {ex.RequestedAmount - ex.AvailableBalance})");
}הסבר עיקרי
סדר ה-catch קובע מה נתפס — C# בודקת בלוקי catch מלמעלה למטה, ותופסת ברגע שמצאה סוג תואם. לכן חייבים לסדר מהספציפי ביותר לכללי ביותר — catch (Exception) ראשון היה “בולע” הכל, ובלוקי ה-catch הספציפיים יותר אחריו לעולם לא היו רצים (וזו אפילו שגיאת קומפילציה ב-C# אם הכללי מגיע קודם).
Custom Exceptions הופכים שגיאות עסקיות למפורשות — InsufficientFundsException אומר בדיוק מה קרה, כולל נתונים רלוונטיים (RequestedAmount, AvailableBalance) שקוד תופס יכול להשתמש בהם — הרבה יותר שימושי מ-throw new Exception("no money") גנרי, שקשה לתפוס באופן ממוקד ולחלץ ממנו מידע מובנה.
: base(message) — בדיוק כמו קונסטרוקטור של מחלקה יורשת רגילה (מהיחידה הקודמת), InsufficientFundsException קורא ל-base(...) כדי להעביר את ה-Message הסטנדרטי ל-Exception הבסיסי — כך ש-ex.Message יעבוד כרגיל, בנוסף לשדות המותאמים-אישית.
יתרונות
היררכיית חריגות מאפשרת תפיסה ברמת דיוק גמישה — ספציפי מאוד או כללי; Custom Exceptions הופכים שגיאות עסקיות למפורשות וניתנות-לזיהוי, עם נתונים רלוונטיים מובְנים; קוד קורא יכול לבחור להגיב אחרת לכל סוג חריגה עסקית ספציפי.
חסרונות
יותר מדי סוגי Custom Exception לכל תרחיש קטן מוסיף מורכבות מיותרת; catch (Exception) כללי מדי בלי catch ספציפי לפניו מסתיר מידע חשוב על מה בדיוק השתבש.
נקודות חשובות
• כל החריגות יורשות מ-Exception; catch לפי סוג בסיס תופס גם תתי-סוגים
• סדר בלוקי catch קריטי — מהספציפי לכללי, אחרת קוד לא מגיע (ואף שגיאת קומפילציה)
• Custom Exception יורש מ-Exception, קורא ל-base(message), ויכול להוסיף שדות עסקיים משלו
• throw יזום זורק חריגה כשמצב עסקי (לא רק טכני) מתגלה כלא תקין
טעויות נפוצות
• לסדר catch (Exception) לפני catch ספציפי — שגיאת קומפילציה, כי הספציפי אף פעם לא יכול להיתפס
• להשתמש ב-Exception הגנרי במקום Custom Exception לשגיאות עסקיות חוזרות — מקשה על תפיסה ממוקדת
• לשכוח : base(message) בקונסטרוקטור של Custom Exception, ולאבד את ה-Message הסטנדרטי
סיכום
כל חריגות ה-.NET יורשות מ-Exception בהיררכיה אחת — catch לפי סוג בסיס תופס גם תתי-סוגים ספציפיים יותר, וסדר בלוקי ה-catch (מהספציפי לכללי) קובע מה נתפס איפה. Custom Exceptions מאפשרים לייצג שגיאות עסקיות ספציפיות לדומיין (כמו InsufficientFundsException) עם נתונים מובְנים רלוונטיים, במקום Exception גנרי שקשה לתפוס ולפרש באופן ממוקד.
דוקומנטציה רשמית
Microsoft Learn — Creating and throwing exceptions
תרגילים
תרגיל 1 — היררכיית catch
המשימה: כתבו קוד עם שני בלוקי catch — FormatException ואחריו Exception כללי — שמנסה להמיר קלט לא תקין למספר.
בדיקה: קלט לא-מספרי נתפס על ידי catch (FormatException) הספציפי, לא על ידי הכללי שאחריו.
תרגיל 2 — Custom Exception ראשון
המשימה: צרו class InvalidAgeException : Exception עם קונסטרוקטור שמקבל גיל לא תקין ומעביר הודעה מתאימה ל-base. זרקו אותה ממתודה SetAge(int age) אם age < 0.
בדיקה: SetAge(-5) זורקת InvalidAgeException עם הודעה ברורה שנתפסת ומודפסת בהצלחה.
פרויקט מסכם
המשימה: בנו מחלקת Inventory (מלאי) עם Custom Exception עסקי.
דרישות:
class OutOfStockException : Exceptionעם שדותProductNameו-RequestedQuantity, וקונסטרוקטור עםbase(message)מתאיםclass InventoryעםDictionary<string, int>פנימי (מוצר → כמות), ומתודתRemoveStock(string product, int quantity)שזורקתOutOfStockExceptionאם הכמות המבוקשת חורגת מהמלאי- קוד ב-
Mainשתופס את החריגה ומדפיס הודעה מפורטת עם שני השדות המותאמים-אישית
בדיקה: בקשת כמות תקינה מצליחה ומעדכנת את המלאי; בקשה חורגת זורקת OutOfStockException עם ProductName/RequestedQuantity נכונים, ונתפסת בהצלחה.
מה בפרק הבא
בפרק הבא — קבצים (File I/O): איך קוראים וכותבים לקבצים על הדיסק, כולל שילוב עם try/catch/finally לטיפול נכון במשאבים חיצוניים.