תפיסת חריגות היא חצי מהטיפול בשגיאות; החצי השני הוא לזרוק את החריגה הנכונה. חריגה שנבחרה היטב אומרת לקורא בדיוק מה השתבש, והאם זו הייתה הטעות שלו או המצב של התוכנית. העמוד הזה מכסה את צד הזריקה; try catch מכסה את הטיפול.
פקודת throw ו-guard clauses
throw מקבלת אובייקט חריגה. הביצוע של המתודה נעצר שם, והחריגה מחפשת מטפל למעלה במחסנית הקריאות. השימוש הנפוץ ביותר הוא guard clause: בדיקות בראש המתודה שדוחות קלט לא תקין לפני שנעשית עבודה כלשהי.
פלט:
ArgumentOutOfRangeException for parameter 'amount'
InvalidOperationException: The account is frozen.
Balance: 100
nameof(amount) מייצר את המחרוזת "amount" ונשאר נכון גם אם משנים את שם הפרמטר. חריגות הארגומנטים שומרות אותו ב-ParamName, שכלים ולוגים משתמשים בו כדי להצביע על הארגומנט השגוי.
guard clauses שומרים על שאר המתודה פשוט: אחרי הבדיקות, הקוד יכול להניח שהקלט תקין. הם גם נכשלים בנקודת הטעות, במקום לתת לערך שגוי להמשיך הלאה ולגרום ל-NullReferenceException מבלבל שלוש מתודות אחר כך.
איזה טיפוס חריגה לזרוק
השתמשו שוב בטיפוס מובנה כשהוא מתאר את המצב; הקוראים כבר יודעים איך לטפל בו.
| מצב | מה לזרוק |
|---|---|
ארגומנט נדרש הוא null | ArgumentNullException |
| ארגומנט מחוץ לטווח המותר (כמות שלילית, אינדקס אחרי הסוף) | ArgumentOutOfRangeException |
| ארגומנט לא תקין בדרך אחרת (שם ריק, מזהה פגום) | ArgumentException |
| הקריאה לא תקינה במצב הנוכחי של האובייקט | InvalidOperationException |
הפעולה אף פעם לא נתמכת בטיפוס הזה (Add של אוסף לקריאה בלבד) | NotSupportedException |
| המתודה עוד לא נכתבה | NotImplementedException |
נעשה שימוש באובייקט אחרי Dispose | ObjectDisposedException |
| לפעולה עם מגבלת זמן נגמר הזמן | TimeoutException |
הקו בין שלוש השורות הראשונות ל-InvalidOperationException הוא מי צריך לשנות משהו. חריגת ארגומנט אומרת "קראו לזה אחרת". InvalidOperationException אומרת "הקריאה הייתה בסדר, אבל לא עכשיו".
אל תזרקו Exception, SystemException או ApplicationException ישירות: קוראים לא יכולים לתפוס אותן בלי לתפוס גם את כל השאר. אל תזרקו בעצמכם גם NullReferenceException, IndexOutOfRangeException או StackOverflowException; סביבת הריצה שומרת אותן לבאגים אמיתיים.
ביטויי throw
לפני C# 7, throw הייתה רק פקודה. מאז C# 7 היא יכולה להופיע גם כביטוי בשלושה מקומות, וזה הופך בדיקות נפוצות לשורה אחת:
פלט:
Ana <ana@example.com>
Null: name
ArgumentException: email
שימו לב ש-ArgumentNullException יורשת מ-ArgumentException, כך ש-catch (ArgumentException) שממוקם ראשון היה תופס גם את מקרה ה-null. מאותה סיבה הסדר של פסוקיות ה-catch חשוב כשמטפלים בשתיהן.
ThrowIfNull וחברותיה (.NET 6 ומעלה)
.NET מודרני מוסיף מתודות עזר סטטיות שכותבות בשבילכם את הבדיקה ואת הזריקה, עם שם הפרמטר שנלכד אוטומטית:
public void Ship(Order order, int quantity, string address)
{
ArgumentNullException.ThrowIfNull(order); // .NET 6
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity); // .NET 8
ArgumentException.ThrowIfNullOrWhiteSpace(address); // .NET 8
ObjectDisposedException.ThrowIf(disposed, this); // .NET 7
// ...
}
הן מתנהגות בדיוק כמו if ו-throw שנכתבו ידנית, ושומרות כל guard clause בשורה אחת. בגרסאות ישנות יותר, כתבו את צורת ה-if שהוצגה קודם.
כתיבת מחלקת חריגה מותאמת
צרו טיפוס חריגה משלכם כשהקוראים צריכים לתפוס את הכישלון הספציפי הזה בנפרד, או כשהמטפל צריך נתונים שמחרוזת הודעה לא יכולה לשאת היטב.
פלט:
Cannot withdraw 25 from a balance of 15.
Short by 10
המוסכמות:
- השם מסתיים ב-
Exception. - המחלקה יורשת מ-
Exception(או מטיפוס מובנה ספציפי יותר כשהיא מקרה פרטי שלו, כמוInvalidOperationException). - יש לה שלושת הבנאים הסטנדרטיים: בלי כלום, הודעה, והודעה ועוד חריגה פנימית. הוסיפו בנאים משלכם מעבר להם.
- נתונים נוספים נכנסים למאפיינים לקריאה בלבד, שמוגדרים בבנאי. כך מטפל יכול לפעול לפי
e.Requestedבמקום לפענח את ההודעה.
עטיפה עם חריגה פנימית
כשכישלון ברמה נמוכה צריך להופיע ככישלון ברמה גבוהה יותר, עטפו אותו. המקור נשמר כ-InnerException, כך ששום מידע לא הולך לאיבוד:
פלט:
Setting 'port' must be a number, got '80a'.
Caused by: FormatException
הקורא עוסק עכשיו במונחים של קונפיגורציה, שאותם הוא מבין, ורישום של e.ToString() ללוג מדפיס את כל השרשרת, כולל ה-FormatException וה-stack trace שלה. עטפו רק כשאתם מוסיפים משמעות; עטיפה של כל חריגה ב-MyAppException כללית רק מכריחה מטפלים לחפור ב-InnerException.
זריקה מול החזרת תוצאה
חריגות מיועדות לכישלונות שהקורא לא מצפה להם בפעולה רגילה. לתוצאות שגרתיות, כמו חיפוש שלעיתים קרובות לא מוצא כלום או קלט משתמש שלעיתים קרובות לא תקין, המוסכמה ב-.NET היא תבנית ה-Try: מחזירים bool ומעבירים את הערך בחזרה דרך פרמטר out.
public bool TryWithdraw(decimal amount, out string error)
{
if (amount > Balance) { error = "Insufficient funds."; return false; }
Balance -= amount;
error = null;
return true;
}
להרבה טיפוסים יש את שתי הדרכים: int.Parse זורקת, int.TryParse מחזירה false; dict[key] זורק, dict.TryGetValue מחזירה false. זריקת חריגה עולה הרבה יותר מהחזרת ערך, כך שהיא לא צריכה להיות בנתיב שרץ אלפי פעמים בשנייה. ראו ref ו-out על פרמטרי out.
כתיבת הודעות טובות
הודעת חריגה נקראת על ידי מפתח שמסתכל בלוג. כתבו בה מה היה שגוי, וכשזה בטוח, את הערך הבעייתי: "Quantity must be between 1 and 99, got 0." עדיף על "Invalid input." כתבו משפטים שלמים, והשאירו סודות כמו סיסמאות ו-tokens מחוץ להודעות, כי הן מגיעות לקובצי לוג.
טעויות נפוצות
- זריקת
Exceptionעצמה. קוראים לא יכולים לתפוס אותה באופן סלקטיבי; השתמשו בטיפוס ספציפי. - העברת ההודעה במקום שם הפרמטר.
new ArgumentNullException("name")מקבלת את שם הפרמטר; ההודעה באה שנייה. - שמות פרמטרים מקודדים ידנית. השתמשו ב-
nameof(param)כדי ששינויי שם ישמרו עליהם נכונים. - חריגות מותאמות בלי משמעות נוספת. אם טיפוס מובנה מתאים, השתמשו בו.
- איבוד השגיאה המקורית בזמן עטיפה. העבירו אותה תמיד כחריגה הפנימית.
שאלות נפוצות
איך זורקים חריגה ב-C#?
יוצרים אובייקט חריגה וזורקים אותו: throw new ArgumentException("Amount must be positive", nameof(amount));. הביצוע נעצר בשורה הזו והחריגה עולה במחסנית הקריאות עד ה-catch המתאים הקרוב ביותר. בחרו את הטיפוס המובנה הספציפי ביותר שמתאר את הבעיה, או טיפוס מותאם כשהקוראים צריכים לטפל במקרה הזה בנפרד.
איך יוצרים חריגה מותאמת (custom exception) ב-C#?
גזרו מחלקה מ-Exception שהשם שלה מסתיים ב-Exception, ותנו לה את הבנאים הסטנדרטיים: אחד בלי ארגומנטים, אחד שמקבל הודעה, ואחד שמקבל הודעה וחריגה פנימית, וכל אחד מהם קורא לבנאי base(...) המתאים. הוסיפו מאפיינים לקריאה בלבד לכל נתון שהמטפל צריך, כמו מזהה הזמנה או יתרה.
מתי לזרוק ArgumentException ומתי InvalidOperationException?
זרקו ArgumentException (או ArgumentNullException / ArgumentOutOfRangeException) כשקורא העביר ערך שגוי: התיקון הוא לקרוא למתודה אחרת. זרקו InvalidOperationException כשהארגומנטים תקינים אבל האובייקט במצב הלא נכון לקריאה, כמו קריאה מחיבור סגור או משיכה מחשבון מוקפא.
מה זה ביטוי throw ב-C#?
מאז C# 7, אפשר להשתמש ב-throw כביטוי בשלושה מקומות: אחרי ??, כאחד הענפים של ?:, וכגוף של איבר עם גוף ביטוי או של למדה. למשל _name = name ?? throw new ArgumentNullException(nameof(name)); משים או זורק בשורה אחת.
מה ArgumentNullException.ThrowIfNull עושה?
זו מתודת עזר סטטית שנוספה ב-.NET 6: ArgumentNullException.ThrowIfNull(customer); זורקת ArgumentNullException עם שם הפרמטר ממולא אוטומטית כש-customer הוא null, ולא עושה כלום אחרת. גרסאות מאוחרות יותר הוסיפו מתודות עזר דומות כמו ArgumentException.ThrowIfNullOrEmpty (.NET 7) ו-ArgumentOutOfRangeException.ThrowIfNegative (.NET 8).