-
נתונות המחלקות הבאות:
class A { public virtual void vfoo() { Console.WriteLine("vfoo from A"); } public void foo() { Console.WriteLine("foo of A"); } } class B : A { public override void vfoo() { Console.WriteLine("vfoo from B"); } public void foo() { Console.WriteLine("foo of B"); } public void baseVfoo() { base.vfoo(); } public void baseFoo() { base.foo(); } }
להלן הקוד ב-Main ובצידו הפלט
A a1 = new A(); a1.vfoo(); //vfoo from A a1.foo();//foo from A B b1 = new B(); b1.vfoo();//vfoo from B b1.foo();//vfoo from B b1.baseVfoo();//vfoo from A b1.baseFoo();//foo from A; A ab = new B(); ab.foo(); //foo from A ab.vfoo(); //foo from B
כעת לתעלומה:
כשאני מייצר B עם זיכרון של B (שורה 4) ופונה לפונקציה מסוג virtual ב-base שלו (שורה 7), אני מקבל את הפונקציה של המחלקה המורישה, כלומר פונקציה של A.
מסקנה: כאשר כותבים פונקציית override ביורש, היא לא דורסת את הזיכרון של המוריש.
מאידך כאשר אני מייצר A עם זיכרון של B (שורה 9), ופונה לפונקציה הוירטואלית (שורה 11) שלהבנתי היא ב-base (כלומר בחלק הזיכרון של ה-A, כי אין בכלל גישה כעת ל-B) אני מקבל להפתעתי את הפונקציה של B.
מסקנה: כאשר מתבצעת אפילו רק הקצאת זיכרון של B, הפונקציה הוירטואלית ב-A נדרסת.
איך מבינים את הסתירה הזו? -
@yyy נהנתי מהשאלה, אנסה לענות כפי הנלע"ד. מקווה שאני לא טועה, וגם שאני לא מבלבל יותר מאשר מסביר.
בכללי, אתה יותר מדי מדבר בשפה של שפות low level שלא מתאימים לשפות high level.
בשפה כמו #C לא מדברים על הקצאות זכרון ודריסת זכרון אלא על מבנים יותר אבסטרקטיים, איך זה ממומש ברמה הנמוכה לא כל כך איכפת לנו.ובכן, בשפת #C צריך להיות מודע לשני מאפיינים שונים של כל משתנה. הטייפ המוצהר שלו, והטייפ האמיתי שלו (שידוע גם כן כ-runtime type).
בד"כ כאשר קורא לפונקציה מתוך אובייקט ככה:foo.bar()
, אז ההגדרה שלbar
לקוחה מתוך הטייפ המוצהר של האובייקטfoo
.
אומנם, ההצהרה על פונקציה כ-virtual
אומרת ל-#C: כאשר אתה מחפש את ההגדרה של פונקציה זו, תיקח אותה מהטייפ האמיתי של האובייקט ולא מהטייפ המוצהר שלו. (אם הטייפ האמיתי לא כוללת מימוש לפונקציה זו אז עולים למעלה בהיררכית הירושה עד שמגיעים למימוש).
המילהbase
בכל מקרה אומרת לקחת את ההגדרה של הפונקציה מהמוריש.ועכשיו לשאלה שלך:
כשאני מייצר B עם זיכרון של B
תשתמש בניסוח המקובל. אתה בונה אובייקט מסוג B ומצביע עליו במשתנה מסוג B.
כאשר כותבים פונקציית override ביורש, היא לא דורסת את הזיכרון של המוריש
נכון, אין אף פעם דריסת זכרון ב-#C.
מאידך כאשר אני מייצר A עם זיכרון של B
כלומר אתה בונה אובייקט מסוג B ומצביע עליו עם משתנה מסוג A.
ופונה לפונקציה הוירטואלית (שורה 11) שלהבנתי היא ב-base (כלומר בחלק הזיכרון של ה-A, כי אין בכלל גישה כעת ל-B) אני מקבל להפתעתי את הפונקציה של B
לא נכון להגיד ש"אין בכלל גישה כעת ל-B". כמו שהסברתי #C תמיד מודעת לטייפ האמיתי של האובייקט ולטייפ המוצהר שלו. ההתנהגות בפונקציה וירטואלית הוא לקחת את המימוש מהטייפ האמיתי (שנקרא runtime type מכיון שזה דבר שנקבע בשעת ההרצה ולא בשעת כתיבה). ואם כן אין מקום להפתעות.
אגב, הדריסה של פונקציית
foo
ב-B
לא תקינה וגורמת לאזהרה מהמהדר. אתה אמור להצהיר על כוונתך בפירוש עם המילהnew
. -
@yossiz
תודה על ההסבר המושקע.
אני מבין שכאשר אתה בונה אובייקט מסוג B ומצביע עליו עם משתנה מסוג A, האובייקט ה"אמיתי" הוא B בגלל שהוא מבנה הנתונים שהוקצה (וסליחה על הגלישה שוב לכיוון אסמבלי).
מה שלא ברור זה למה המוריש נחשב לטייפ האמיתי, יותר מהטייפ היורש? -
@yossiz אמר בoverride תעלומה:
@yyy לא הבנתי את דבריך. איפה אתה רואה שהמוריש נחשב לטייפ האמיתי? זה לא קשור ליורש או למוריש, הטייפ האמיתי קשור לאיזה בנאי השתמשת כשבנית את האובייקט.
הבנתי.עכשיו איך תסביר מבחינה תיאורטית את המקרה הבא:
יש B שיורש מ-A, ונבנה אובייקט מסוג A, המוצבע ע"י מצביע מסוג A, אלא שיש לו downCasting ל-B, באופן הבא:((B)a).fooA();
התוצאה תהיה Exception בזמן ריצה.
פה חייבים לכאורה להגיע לסוגיית הקצאת הזיכרון, כלומר המערכת מנסה לגשת לזיכרון מסוג B, בשעה שההקצאה היא רק בגודל A. יש לך הסבר יותר אלגנטי? -
@yyy "הקצאת זכרון" היא מילה גסה בשפת #C או JAVA. אנחנו לא חושבים על הקצאות זכרון בשום פעם. יש אובייקטים וטייפס. ובתוך טייפס יש שני סוגים, יש את ה-runtime type (מה שקראתי למעלה ה"טייפ האמיתי" שיכול להיות דינאמי וידוע רק בשעת ריצה והוא נקבע לפי איזה בנאי השתמשת לבנות אותו) והטייפ המוצהר של המשתנה (שידועה בשעת הידור).
הפעולה של downCasting בודקת את הטייפ האמיתי דהיינו ה-runtime type וזורקת שגיאה אם זה לא מתאים.(אגב, בשפת C אין בדיקות על downcasting והשגיאה נזרקת במצב הכי טוב כאשר אתה מנסה לגשת לשדה בתוך האובייקט, ובמצבים יותר גרועים באיזה מקום לא קשור בתוכנה או אף פעם, רק שתקבל ערכים לא נכונים לשדות האובייקט)
-