Menu

Try catch ב-C#: טיפול בחריגות עם catch, when ו-finally

try, catch ו-finally הם הדרך שבה C# מטפלת בחריגות. למדו איך חריגה עולה במחסנית הקריאות, איך תופסים טיפוסי חריגות ספציפיים בסדר הנכון, מסננים עם when, מנקים ב-finally, זורקים מחדש עם throw; בלי לאבד את ה-stack trace, ואילו חריגות לא לתפוס.

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

כשמשהו משתבש בזמן ריצה (קובץ חסר, טקסט שאינו מספר, מפתח שלא נמצא במילון), .NET זורקת חריגה: אובייקט שמתאר את השגיאה. החריגה עולה במחסנית הקריאות עד שבלוק catch מטפל בה. אם שום דבר לא מטפל, התוכנית נעצרת ומדפיסה את השגיאה.

try catch בסיסי

עטפו את הקוד שעלול להיכשל ב-try, וטפלו בכישלון ב-catch:

פלט:

42 doubled is 84
'forty-two' is not a number
7 doubled is 14
Still running

כש-int.Parse("forty-two") זורקת, ה-Console.WriteLine שאחריה בבלוק ה-try מדולג, בלוק ה-catch (FormatException) רץ, והלולאה ממשיכה. אפשר להשמיט את המשתנה (catch (FormatException)) כשלא צריכים את אובייקט החריגה.

למקרה הספציפי הזה יש כלי טוב יותר: int.TryParse(input, out int n) מחזירה false במקום לזרוק. חריגות מיועדות למצבים שהקוד לא מצפה להם; קלט שלעיתים קרובות לא תקין הוא צפוי, אז בדקו אותו במקום לתפוס.

איך חריגה עוברת

חריגה שנזרקת עמוק בתוך שרשרת קריאות פורמת כל מתודה עד שהיא מוצאת מטפל מתאים. הקוד שאחרי הזריקה בכל אחת מהמתודות האלה לא רץ.

פלט:

OrderTotal finished
5.00
Caught KeyNotFoundException in Main

ל-PriceOf ול-OrderTotal אין catch, כך שהחריגה עוברת דרכן ישר ל-Main. שימו מטפל ברמה שיודעת מה לעשות עם הכישלון, ולרוב זו לא הרמה שבה הוא קרה.

תפיסת חריגות ספציפיות, בסדר הנכון

פסוקית catch מטפלת בטיפוס שלה ובכל טיפוס שיורש ממנו. כשיש כמה פסוקיות, הראשונה שמתאימה מנצחת, אז סדרו אותן מהספציפית ביותר לכללית ביותר:

פלט:

10 / 2 = 5
Cannot divide by zero
Both values must be whole numbers
Unexpected: OverflowException

הקריאה האחרונה זורקת OverflowException, שאף אחת מהפסוקיות הספציפיות לא מטפלת בה, ולכן ה-catch (Exception e) הכללית מטפלת. הצבת catch (Exception) ראשונה הייתה הופכת את הפסוקיות שאחריה לבלתי ניתנות להשגה, והקומפיילר דוחה את זה עם שגיאה CS0160.

מסנני חריגות עם when

C# 6 הוסיפה את when: תנאי שמחליט אם פסוקית catch חלה. אם הוא false, סביבת הריצה ממשיכה לחפש מטפל אחר כאילו הפסוקית לא קיימת.

פלט:

Not found: show an empty page
Server error 503: retry later
Unhandled by Call: HTTP 401

מסננים מאפשרים להסתעף לפי נתונים שבתוך החריגה בלי לתפוס ולזרוק מחדש. הם גם הדרך הנקייה לטפל בשני טיפוסים לא קשורים באופן זהה, כמו שהפסוקית השלישית עושה. מסנן רץ לפני שהמחסנית נפרמת, כך ש-debugger או crash dump עדיין מציגים את המצב המקורי כששום מסנן לא מתאים.

finally: קוד שתמיד רץ

בלוק finally רץ כשהשליטה עוזבת את ה-try, בין אם הוא הסתיים, חזר מוקדם או זרק. זה המקום לקוד הניקוי. (הסתייגות אחת: אם שום דבר בשום מקום לא תופס את החריגה, התהליך יכול להסתיים בלי להריץ אותו.)

פלט:

Open connection
Close connection
finished
Open connection
Close connection
returned early
Open connection
Close connection
handled error

בכל המקרים "Close connection" מודפס לפני שהתוצאה של המתודה מגיעה ל-Main: ה-finally רץ אחרי שערך ה-return חושב אבל לפני שהמתודה באמת חוזרת. ל-try יכול להיות finally בלי catch בכלל, וכך הוא מנקה ועדיין נותן לחריגה להמשיך לקורא.

לאובייקטים שמממשים IDisposable (קבצים, זרמים, חיבורים), פקודת using כותבת את ה-try/finally הזה בשבילכם.

זריקה מחדש: throw; מול throw e;

לפעמים בלוק catch רושם משהו ללוג ואז נותן לחריגה להמשיך. האופן שבו זורקים מחדש קובע אם ה-stack trace שורד:

פלט:

throw;   trace mentions LoadConfig: True
throw e; trace mentions LoadConfig: False

throw e; מתייחסת לחריגה כאילו נזרקה מחדש מבלוק ה-catch, כך שהמסגרות שמתחתיו, כולל המתודה שבה השגיאה קרתה, נעלמות מה-trace. זרקו מחדש תמיד עם throw; חשוף. (המאפיין NoInlining נמצא שם רק כי ה-JIT עשוי למזג מתודה קטנה כל כך לתוך הקורא שלה, וזה היה מסתיר אותה משני ה-traces.)

כדי להוסיף הקשר במקום זאת, עטפו את החריגה בחריגה חדשה והעבירו את המקורית כחריגה הפנימית: throw new ConfigException("Could not start the app", e);. לחריגה הפנימית נשמר ה-stack trace שלה, ו-loggers מדפיסים את השרשרת. כתיבת טיפוסי חריגה משלכם מכוסה ב-זריקת חריגות.

אובייקט ה-Exception

כל חריגה יורשת מ-System.Exception. האיברים שמשתמשים בהם הכי הרבה:

איברמה הוא מחזיק
Messageתיאור קריא לבני אדם
GetType().Nameטיפוס החריגה, כמו FormatException
StackTraceשרשרת הקריאות למתודות בנקודת הזריקה
InnerExceptionהחריגה שגרמה לחריגה הזו, או null
ToString()טיפוס, הודעה, חריגות פנימיות ו-stack trace יחד

רשמו ללוג את e.ToString() ולא את e.Message כשרוצים לאבחן כישלון מאוחר יותר: ההודעה לבדה כמעט אף פעם לא אומרת איפה הבעיה הייתה. אל תציגו את e.ToString() למשתמשי קצה.

טיפוסי חריגות נפוצים

חריגהסיבה אופיינית
NullReferenceExceptionקריאה לאיבר על הפניה שהיא null
ArgumentNullExceptionלמתודה הועבר null במקום שבו היא צריכה ערך
ArgumentOutOfRangeExceptionארגומנט או אינדקס ברשימה מחוץ לטווח המותר
IndexOutOfRangeExceptionאינדקס במערך מחוץ לגבולות שלו
FormatExceptionint.Parse, DateTime.Parse וחברותיהן על טקסט בפורמט שגוי
InvalidCastExceptionהמרה מפורשת לטיפוס שהאובייקט אינו
InvalidOperationExceptionהאובייקט במצב הלא נכון לקריאה (רצף ריק, אוסף ששונה)
KeyNotFoundExceptionקריאה של מפתח חסר במילון עם האינדקסר
DivideByZeroExceptionחלוקה באפס של מספר שלם או decimal
OverflowExceptionמספר שפוענח, המרה מבוקרת או תוצאת חשבון מבוקרת לא נכנסים בטיפוס
FileNotFoundException, IOExceptionבעיות במערכת הקבצים

NullReferenceException, IndexOutOfRangeException ו-InvalidCastException מעידות כמעט תמיד על באג. תקנו את הקוד במקום לתפוס אותן.

לא לבלוע חריגות

catch ריק מסתיר כל שגיאה, כולל כאלה שלא ציפיתם להן:

try
{
    SaveOrder(order);
}
catch (Exception)
{
    // nothing: the order silently was not saved
}

התוכנית ממשיכה לרוץ כאילו השמירה הצליחה, והסיבה האמיתית אובדת. הנחיות ששומרות על טיפול כן בחריגות:

  • תפסו רק את מה שאתם יכולים לטפל בו, ברמה שיכולה לטפל בו.
  • אם אתם תופסים כדי לרשום ללוג, זרקו מחדש עם throw; אלא אם התוכנית באמת יכולה להמשיך.
  • תפסו Exception רק בקצה החיצוני: Main, מטפל בקשות, לולאת worker.
  • העדיפו TryParse, TryGetValue ובדיקות null על פני חריגות במקרים צפויים. זריקה איטית בהשוואה לבדיקה, בעוד שבלוק try שלא זורק כלום כמעט לא עולה דבר.

טעויות נפוצות

  • catch (Exception) ראשון. הפסוקיות שאחריו הופכות לבלתי ניתנות להשגה (CS0160).
  • throw e; לזריקה מחדש. זה מוחק את ה-stack trace המקורי; השתמשו ב-throw;.
  • בלוקי catch ריקים. שגיאות נעלמות; לפחות רשמו ללוג וזרקו מחדש.
  • שימוש בחריגות לבקרת זרימה. בדקו קלט עם TryParse במקום לתפוס FormatException.
  • הצגת e.Message של חריגת framework למשתמשים. הניסוח משתנה בין גרסאות .NET ונכתב עבור מפתחים.

שאלות נפוצות

איך try catch עובד ב-C#?

קוד שעלול להיכשל נכנס לבלוק ה-try. אם פקודה שם זורקת חריגה, שאר הבלוק מדולג וסביבת הריצה מחפשת פסוקית catch שהטיפוס שלה מתאים לחריגה, קודם במתודה הנוכחית ואחר כך בכל קורא. ה-catch הראשון שמתאים רץ, והביצוע ממשיך אחרי כל פקודת ה-try.

האם finally תמיד רץ ב-C#?

כמעט תמיד: אחרי שבלוק ה-try מסתיים כרגיל, אחרי ש-catch מטפל בחריגה, אחרי return או break בתוך הבלוק, וכשחריגה עוברת דרכו בדרכה ל-catch גבוה יותר במחסנית הקריאות. הוא לא רץ כשהתהליך מסתיים קודם: תהליך שנהרג, Environment.FailFast, StackOverflowException, וב-.NET Core ומעלה חריגה ששום דבר לא תופס, שמסיימת את התהליך לפני שבלוקי finally רצים.

איך תופסים כמה חריגות ב-C#?

כתבו כמה פסוקיות catch, הטיפוס הספציפי ביותר ראשון: catch (FileNotFoundException) לפני catch (IOException) לפני catch (Exception). הקומפיילר דוחה פסוקית שאי אפשר להגיע אליה כי פסוקית קודמת כבר תופסת את הטיפוס שלה. כדי לטפל בשני טיפוסים לא קשורים באותה דרך, השתמשו במסנן: catch (Exception e) when (e is FormatException || e is OverflowException).

מה ההבדל בין throw ל-throw ex ב-C#?

בתוך catch, throw; זורקת מחדש את החריגה הנוכחית עם ה-stack trace המקורי שלה. throw ex; זורקת את אותו אובייקט אבל מאפסת את ה-stack trace לשורה הנוכחית, כך שהמתודה שבה השגיאה באמת קרתה נעלמת מה-trace. השתמשו ב-throw;, או עטפו: throw new MyException("context", ex);.

מה זה מסנן חריגות ב-C#?

פסוקית when אחרי catch (C# 6 ומעלה): catch (HttpRequestException e) when (e.Message.Contains("404")). בלוק ה-catch רץ רק אם התנאי true; אחרת החריגה ממשיכה לחפש מטפל אחר כאילו הפסוקית לא קיימת, והמחסנית שלה לא נפרמת.

כדאי לתפוס Exception ב-C#?

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

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

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

להתחיל