ירושה, העמסה ושיכתוב מתודות במחלקות פסקל

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

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

constructor Create;
destructor Destroy;

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

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

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

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

destructor Destroy; override;

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

destructor TSomeClass.Destroy; // no need to redeclare the override word, it only appears on the definition of the structure
begin
Free Things you have initialized
inherited //Destroy // <- you can if you want to also specify what to inherit, but it is not a must in most Object Pascal Compilers.
end;

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

בשביל להגיע למצב בו אפשר לעשות override על מתודה של מחלקה, אנחנו צריכים להכריז עליה שהיא דינאמית כלומר dynamic או שהיא ווירטואלית כלומר virtual.

בקרוב ל99% מהפעמים נרצה להשתמש במתודה ווירוטאלית ולא דינאמית. כל מתודה במחלקה שלא מקבלת dynamic או virtual (או מבוססת עליהם, כלומר עם override או עוד מילה שמורה שנכיר בהמשך) היא מתודה סטטית. המתודה הסטטית אבל היא לא מתודה סטטית המוכרת מ Java למשל, אלא ההגדרה הסטטית היא בכך שהזכרון שלה נשאר קבוע ואי אפשר לשכתב אותו – כלומר אי אפשר לעשות פולימורפיזם עם אותה מתודה.

מתודה ווירטואלית ומתודה דינאמית זהות כמעט לגמרי, כאשר ישנם 2 דברים שמשתנים כאן:

  1. אופן השמירה של הכתובת זכרון של המתודה
  2. התלות ב TObject

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

לפי התעוד של דלפי, השמירה של מתודות ווירטואליות ומתודות דינמיות מתבצעת בצורה הבאה (העתק מהתעוד):

Dynamic functions are allowed for classes derived from TObject. Dynamic functions are similar to virtual functions except for the way they are stored in the virtual tables. Virtual functions occupy a slot in the virtual table in the object they are defined in, and in the virtual table of every descendant of that object. Dynamic functions occupy a slot in every object that defines them, not in any descendants. That is, dynamic functions are virtual functions stored in sparse virtual tables. If you call a dynamic function, and that function is not defined in your object, the virtual tables of its ancestors are searched until the function is found.

Therefore, dynamic functions reduces the size of your virtual tables at the expense of a delay at runtime to look up the address of the functions.

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

נגיד ויצרנו את המתודה הווירטואלית הבאה:

procedure OverrideMe; virtual;

ועכשיו נרצה בבן שיורש את המתודה בלי לשנות את המבנה שלה, נשתמש ב override:

procedure OverrrideMe; override;

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

בשביל לפתור את הבעיה, אנחנו נשתמש במילה השמורה reintroduce במקום ה override:

procedure OverrideMe(SomeParam : Float); reintroduce

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

2 מחשבות על “ירושה, העמסה ושיכתוב מתודות במחלקות פסקל

  1. פינגבק: class var « לראות שונה

  2. פינגבק: מערך כתכונה | לראות שונה

להשאיר תגובה

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

הלוגו של WordPress.com

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

תמונת Twitter

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

תמונת Facebook

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

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

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

מתחבר ל-%s