ניהול חריגות גמיש

פעם חשבתי שניהול החריגות של ג'אווה ורובי גמיש מאוד, ומאוד רציתי שגם בפסקל יהיה ניהול שכזה (מבחינת תחביר הכוונה), אבל אז התחלתי לעבוד בצורה רצינית יותר עם 2 השפות והתחלתי למצוא הרבה בעיות שם.

למשל ברובי מצאתי מצב בו אני ממש צריך לתפוס את אובייקט Object (המחלקה הנמוכה ביותר ברובי עד גרסה 1.8 כולל) בשביל ממש לטפל בכל חריגה שקיימת. בג'אווה גם יש כל מיני סוגי חריגות שException פשוט יפספס (נגיד Thread.Exception שלא שייך לך, אבל יפיל אותך וכמובן גם RunTimeException ששוכחים אותו הרבה מאוד פעמים), כך שלא תמיד פשוט וברור לדעת אילו חריגות יתפסו אצלנו ואילו לא, בייחוד כאשר אנחנו רוצים לטפל בהם, ולא רק לתפוס את כל החריגות. כלומר ברובי אם אני תופס את הכל וזהו, אז מן הסתם יהיה לי ניהול חריגות, אבל אז לא תמיד אצליח לעבוד איתו כמו שצריך.

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

לשם השוואה, בשפת Java אנחנו עושים חריגות בצורה הבאה:

try {
   ...
} catch (ExceptionClassOfSomeKind e)  {
   ...
} catch (Exception e) { // Catch almost everything else ...
   ...
}

ובשפת רובי זה בכלל בצורה הבאה:

begin # yes that's like try
  ...
rescue ExceptionClassOfSomeKind => e
  ...
rescue Exception => e # or just => e is the same thing,
                      # catch almost everything else ...
  ...
end

הנה מקרה בפסקל שדורש טיפול בחריגות והוא ללא טיפול שכזה:

{$mode objfpc}
program test;
Uses SysUtils; // To Add support for EDivByZero instead of runtime error.

var
 a, b : Byte;

begin
  a := 1;
  b := 0;
  writeln ('About to Divide ', a,' with ', b);
  writeln(a div b);
  writeln('Done dividing.');
end.

הסיבה למשתנים הוא כי אם הייתי משתמש מייד במספרים המהדר היה מתעצבן עלינו ואומר שאנחנו מחלקים ב0, אז צריך לעשות את זה פחות ברור.

הפלט שיהיה הוא:

$ ./test0
About to Divide 1 with 0
An unhandled exception occurred at $000000000040025C :
EDivByZero : Division by zero
  $000000000040025C

בשביל לטפל במקרה הזה, אני צריך לעשות את הפעולה הבאה:

{$mode objfpc}
program test;
uses SysUtils; // To Add support for EDivByZero instead of runtime error. Also for Exception Class.

var
 a, b : Byte;

begin
  a := 1;
  b := 0;
  writeln ('About to Divide ', a,' with ', b); 

  try
    writeln(a div b);
  except
    on E:Exception do
     begin
       if E is EDivByZero then // "is" -> check if E is a class named EDivByZero
         writeln(STDERR,'Divided in Zero you moron')
       else
         writeln(STDERR,'Unknown exception: ', E.Message);
    end;
  end;
  writeln('Done dividing.');
end.

הפלט הוא:

$ ./test1
About to Divide 1 with 0
Divided in Zero you moron
Done dividing.

הניהול גמיש יותר, היות ואנחנו גורמים לכך שכל חריגה שתגיע היא לפחות ירושה של Exception שהוא המחלקה הבסיסית ביותר עבור החריגות בפסקל (מגיע ביחידה של SysUtils), וכל חריגה צריכה לרשת ממנו או מחריגה גבוהה יותר (כך גם אפשר לבדוק האם E שייך נגיד לחריגה של קבוצה מסויימת, נגיד של עבודה עם שקעים, ולבצע קוד על זה עם המכנה המשותף הנמוך ביותר של אותן חריגות במידה ויש).

ירושה של חריגות מתבצעת בד"כ בשיטה הזו (לפעמים נדרש עוד קוד, אבל בד"כ אין צורך לזה):

type EMyException = class(Exception);

דבר שני, כל מחלקה שהיא חריגה צריכה לקבל ב Object Pascal את המקדם של E ממש כמו הדוגמא שלי או כדוגמת EDivByZero למעלה. זה לא כלל שאי אפשר לשבור, אבל זו צורת הכתיבה בObject Pascal המקובלת והיא מאוד קריאה בזכות הנוטציה ההונגרית הזו כמו שאר הנוטציות האחרות שיש בשפה.

הבדיקה של E is EDivByZero אומרת שאנחנו בודקים האם E שייך או יורש מהמחלקה EDivByZero. כך שאם היינו בודקים אם הוא שייך ל Exception היינו תופסים את כל החריגות ללא יוצא מן הכלל (אלא אם התחכמו ולא ירשו מ Exception או חריגה גובהה יותר).

הגמישות כאן מאפשרת לנו לטפל במקום אחיד בחריגות בהתאם לצורך ממש כמו חלק קוד אחר בשפה, במקום לבזר את הטיפול למספר חלקים שאין להם קשר (תארו לכם מספר חריגות שהבסיס שלהן משותף למעט שורת קוד אחת למשל). כמו כן, במידה ואנחנו רוצים לתפוס הכל, אבל לא לטפל בחריגה כחריגה אלא לטפל באופן גורף בזה שהיתה איזושהי חריגה, לא צריך בכלל להגדיר את המשתנה E (המשפט on e:Exception מגדיר את e כמשתנה מסוג exception). התוכן של e יהיה שייך רק בתוך החלק של ה begin, אבל לא ניתן להגדיר משתנה כזה אם כבר יש לנו משתנה איפשהו בבלוק בשם הזה.

עוד יכולת נחמדה שיש בFPC (ודלפי) היא היכולת לקבל רשימה של כל המחסנית שלנו שהגיעה עד לנקודה הזו של החריגה ממש (backtrace) כמו בשפות דינאמיות כדוגמת רובי, פיתון, פרל ובג'אווה (שהיא לא דינאמית), ואנחנו נעשה את זה בצורה הזו:

raise Exception.Create('You arrived to my exception.') at get_caller_addr(get_frame);

המילה השמורה at אומרת ל raise בעצם להציג גם את המחסנית אשר get_caller_addr מחזיר מהframe ריצה הנוכחי (דרך הפונקציה get_frame‏). 2 הפונקציות האלו שייכות עד כמה שאני יודע רק ל FPC, אבל התחביר של at קיים גם בדלפי, רק המימוש מתבצע קצת בצורה שונה (אינני זוכר בעל פה) .

ללא השימוש בat היינו מקבלים משהו בסגנון הזה (שימו לב שהידרתי עם מספרי שורות שיכנסו לחריגות ון מידע על הקבצים בשימוש):

$ ./test2
An unhandled exception occurred at $00000000004001CA :
Exception : You arrived to my exception.
$00000000004001CA line 7 of test2.pp
$00000000004001EE line 11 of test2.pp

בעוד שעם השימוש בו אנחנו נקבל משהו בסגנון הזה:

$ ./test3
An unhandled exception occurred at $0000000000400206 :
Exception : You arrived to my exception.
$0000000000400206 line 11 of test3.pp

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

באפשרות הראשונה של test2, פשוט קיבלנו את שורה 7 שהיא השורה של הפרוצדורה של מי שמעלה את החריגה, אבל אנחנו נשתמש ב at כאשר אנחנו נרצה לכוון למי שקרא לפרוצדורה. אנחנו כמובן נעשה את זה, אם החריגה כמובן תלויה במידע שסופק לנו מבחוץ, ואנחנו רוצים להצביע ששם מקור הבעיה, ולכן השימוש הזה ממוקד יותר לשימושים ספציפיים.

ואם זה לא מספיק לכם, אז יש עוד תכונה מאוד מעניינת לחריגות בפסקל מונחה עצמים (גם דלפי וגם FPC, לא יודע לגבי מהדרים אחרים), וזה האפשרות "להאציל סמכויות" בטפול של חריגות. מה הכוונה ? אפשר ליצור וו (hook) אשר תוכל בעצמה לטפל בכל החריגות של האפליקציה שלנו, ולהגיד לה האם אנחנו בעצם טיפלנו שם או לא, ואם לא, אז מחזירים את הטיפול למישהו אחר, וכך אפשר לטפל בחריגות כלליות במקום אחד, ובחריגות נקודתיות במקום אחר באפליקציה שלנו, אבל אם לא ננהל את זה נכון, נאבד את הידיים והרגליים שלנו בנושא. ניתו למצוא על כך מידע תחת SysUtils.ShowException וכן מימוש של TCustomApplication.OnException .

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

ללא קשר, הייתי מאוד רוצה את else שיש ברובי גם בפסקל, שהוא מתקיים כאשר אין חריגה. כלומר הבלוק של try מתקיים, ואם לא היתה חריגה אז הוא נכנס לפעולה, וזה בנוסף ל ensure (המקביל ל finally) שיתבצע בכל מקרה במידה ומוגדר (ברובי).

5 מחשבות על “ניהול חריגות גמיש

  1. ארתיום

    מספר שאלות:

    1. לא הבנתי מדוע זה שונה מ־Java? הרי גם שם אתה יכול לתפוס כל exception כולל RunTimeError שיורשת מ־Exception.
    2. אהבתי שיש לך backtrace מובנה (אין כזה ב־C++‎) אם כי חבל שהוא נוצר רק בצורה מפורשת. למשל ב־CppCMS יש לי מחלקה שמייצרת trace בצורה אוטומטית כשנזרקת חריגה, כמובן שיש לזה מגבלה דומה שמי שזורק חריגה חייב לעשות משהו יזום – לרשת מסוג החריגה הזו.

    בנוסף אין לי יכולת לקבל דיוק ה־backtrace ברמת שורות רק פונקציות (אם הן לא inlined כמובן).

    חוץ מזה אני למדתי דבר אחד אני מעדיף לעבוד כמעט ללא try-catch אלא לתפוס הכל ברמות הגבוהה ביותר.

    הדבר היפה ב־C++‎ הוא שיש לך RAII ואז למעשה אתה לא צריך finally ובפועל אני צריך לכתוב try/catch במקרים בודדים בלבד וזה בד"כ הרמות הגבוהות ביותר.

    1. ik_5 מאת

      1. ג'אווה מרקת לך catch לפי סוג חריגה. RunTimeError אינו יורש מ Exception אלא צריך לתפוס אותו במיוחד (לפחות בפועל, לא יודע בתאוריה).
      אם יש לך בג'אווה טרדים, אז אתה לא יכול לתפוס בצורה יעילה את החריגות אלא אם אתה יודע במדוייק איך לתפוס את זה (כאב ראש, לא באמת מסובך).

      2. ה backtrace הוא טוב כאשר אתה רוצה להראות שיש בעיה מסויימת אבל שהיא לא אצלך. שים לב שאם אני לא תופס חריגה, אז בכל מקרה יש לי את זה בFPC, בעוד שבדלפי יש לי את הכתובת זיכרון של מי שהעלה את החריגה בלבד עד כמה שאני זוכר.

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

      1. ארתיום

        ב־Java ‏RunTimeError כן יורש מ־Exception, דוגמה:

        class Test {
                public static void main(String[] args)
                {
                        try {
                                int a = 1;
                                int b = 0;
                                int c = a / b;
                        }
                        catch(Exception e) {
                                System.out.println("Catched " + e.getMessage());
                        }
                }
        }
        

        עובד מצוין

  2. יורם

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

    הפתרון שהוצג כאן, של בדיקת הטיפוס של החריגה באמצעות if הוא בעיני משהו שאני משתדל להימנע ממנו ככל האפשר – טיפוסים אמורים להשפיע על הFLOW באמצעות קריאה לאימפלמנטציה הספציפית שלהם, ולא באמצעות בדיקת הטיפוס בזמן ריצה ואז כתיבת תנאים שונים ומשונים.

    לא יודע עד כמה זה ישים בשפות לא-דינמיות, אבל הפתרון של פרל (eval של בלוק תופס כל die שהתרחש בבלוק) הוא במידה רבה גמיש יותר, חזק יותר ולא דורש עודף תחביר.

    1. ik_5 מאת

      הצגתי דרך אחת מתוך מספר דרכים להתמודד עם חריגות. אישית כמו שעניתי לארתיום, מעדיף לא לגעת בחריגות, אלא אם אין ברירה. אבל זה כלי כמו כל כלי אחר בתכנות, שצריך לדעת ולהבין מתי הוא מתאים ומתי הוא לא.

כתיבת תגובה

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

הלוגו של WordPress.com

אתה מגיב באמצעות חשבון WordPress.com שלך. לצאת מהמערכת / לשנות )

תמונת Twitter

אתה מגיב באמצעות חשבון Twitter שלך. לצאת מהמערכת / לשנות )

תמונת Facebook

אתה מגיב באמצעות חשבון Facebook שלך. לצאת מהמערכת / לשנות )

תמונת גוגל פלוס

אתה מגיב באמצעות חשבון Google+ שלך. לצאת מהמערכת / לשנות )

מתחבר ל-%s