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