Menu

Throw exception ב-C#: זריקת חריגות, guard clauses, ביטויי throw וחריגות מותאמות

איך ומתי זורקים חריגות ב-C#. למדו את פקודת throw, איזה טיפוס חריגה מובנה מתאים לאיזו טעות, ביטויי throw עם ?? ו-?:, מתודות העזר ThrowIfNull, ואיך כותבים מחלקת חריגה מותאמת עם נתונים משלה וחריגה פנימית.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

תפיסת חריגות היא חצי מהטיפול בשגיאות; החצי השני הוא לזרוק את החריגה הנכונה. חריגה שנבחרה היטב אומרת לקורא בדיוק מה השתבש, והאם זו הייתה הטעות שלו או המצב של התוכנית. העמוד הזה מכסה את צד הזריקה; 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 מבלבל שלוש מתודות אחר כך.

איזה טיפוס חריגה לזרוק

השתמשו שוב בטיפוס מובנה כשהוא מתאר את המצב; הקוראים כבר יודעים איך לטפל בו.

מצבמה לזרוק
ארגומנט נדרש הוא nullArgumentNullException
ארגומנט מחוץ לטווח המותר (כמות שלילית, אינדקס אחרי הסוף)ArgumentOutOfRangeException
ארגומנט לא תקין בדרך אחרת (שם ריק, מזהה פגום)ArgumentException
הקריאה לא תקינה במצב הנוכחי של האובייקטInvalidOperationException
הפעולה אף פעם לא נתמכת בטיפוס הזה (Add של אוסף לקריאה בלבד)NotSupportedException
המתודה עוד לא נכתבהNotImplementedException
נעשה שימוש באובייקט אחרי DisposeObjectDisposedException
לפעולה עם מגבלת זמן נגמר הזמן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).

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל