ירושה מאפשרת למחלקה אחת להיבנות על גבי אחרת. המחלקה הנגזרת מקבלת את האיברים של מחלקת הבסיס, מוסיפה איברים משלה, ויכולה להחליף את ההתנהגות שמחלקת הבסיס מרשה לה להחליף. בשילוב עם מתודות virtual, היא נותנת פולימורפיזם: קוד שנכתב מול מחלקת הבסיס מריץ את ההתנהגות הנגזרת הנכונה בלי לדעת איזו מחלקה נגזרת יש לו.
גזירה של מחלקה
כתבו את מחלקת הבסיס אחרי נקודתיים. למחלקה הנגזרת יש כל מה שיש למחלקת הבסיס, ובנוסף מה שהיא מצהירה עליו:
פלט:
TX-19: 20 km, 2 fares
True
מה מחלקה נגזרת מקבלת ומה לא:
- עובר בירושה: שדות, מאפיינים, מתודות, אירועים וטיפוסים מקוננים. הכול נמצא פיזית באובייקט.
- נגיש: רק מה שהבסיס מרשה: איברים
public,protectedו-internal. איברprivateשלVehicleקיים בתוך כלTaxi, אבל הקוד שלTaxiלא יכול לפנות אליו בשמו. זו הסיבה שלמאפייןKmישprivate set:Taxiיכולה לקרוא אותו, ומשנה אותו רק דרךDrive. - לא עובר בירושה: בנאים.
Taxiחייבת להצהיר על בנאי משלה ולשרשר לאחד מהבנאים שלVehicleעם: base(plate). העמוד על בנאים מראה באיזה סדר שניהם רצים.
כל מחלקה נגזרת בסופו של דבר מ-object, ולכן לכל אובייקט יש ToString(), Equals() ו-GetHashCode().
virtual ו-override
מחלקת בסיס מסמנת מתודה כ-virtual כדי לומר "מחלקות נגזרות רשאיות לספק גרסה משלהן". מחלקה נגזרת מחליפה אותה עם override. בתוך הדריסה, base.Method() קוראת לגרסה של מחלקת הבסיס.
פלט:
[email] to lea@example.com: Your order 1042 has shipped today
(unsubscribe link appended)
[sms] to +351 912 000 111: Your order 1042 has...
[generic] to ops-team: Your order 1042 has shipped today
משתנה הלולאה הוא מטיפוס Notification, ובכל זאת כל אובייקט מוצג בדרך שלו. זה פולימורפיזם: הקריאה n.Render(...) נפתרת בזמן ריצה לפי הטיפוס האמיתי של האובייקט. שימו לב גם ש-base.Render ב-Email משתמשת ב-Channel, שהוא עצמו וירטואלי, ולכן מתודת הבסיס מדפיסה email ולא generic. קריאה וירטואלית בתוך מחלקת הבסיס עדיין מגיעה לדריסה.
גם מאפיינים יכולים להיות וירטואליים, כמו ש-Channel מראה. שדות לא.
הקומפיילר מחייב את שתי מילות המפתח. כתיבה של override על מתודה שאינה virtual היא שגיאה CS0506 ("cannot override inherited member ... because it is not marked virtual, abstract, or override"). השמטה של override כשמתודת הבסיס וירטואלית היא רק אזהרה, והיא משנה את המשמעות לגמרי, כמו שהסעיף הבא מראה.
new מול override: הסתרה במקום דריסה
אם מחלקה נגזרת מצהירה על מתודה עם אותה חתימה כמו מתודת בסיס בלי לכתוב override, היא מסתירה את מתודת הבסיס. הקומפיילר מזהיר (CS0114 למתודת בסיס וירטואלית, CS0108 בכל מקרה אחר) ומציע את מילת המפתח new, שמשתיקה את האזהרה אבל שומרת על התנהגות ההסתרה:
פלט:
Sales report
Report
Draft report
b ו-c הם אותו סוג אובייקט, ובכל זאת הם מדפיסים כותרות שונות. עם new, המתודה שנבחרת תלויה בטיפוס של המשתנה, ונקבעת בזמן קומפילציה. קוד שמטפל בדוחות כ-Report (רשימה, פרמטר של מתודה, callback של framework) אף פעם לא רואה את DraftReport.Title. זה כמעט אף פעם לא מה שרוצים. השתמשו ב-override לפולימורפיזם; new קיים בעיקר למקרה שבו מחלקת בסיס שאינה בשליטתכם מוסיפה איבר שהשם שלו מתנגש עם איבר שכבר יש לכם.
sealed
sealed על מחלקה אוסר לגזור ממנה:
sealed class Invoice { }
class CorrectedInvoice : Invoice { }
// error CS0509: 'CorrectedInvoice': cannot derive from sealed type 'Invoice'
string היא sealed, וכך גם טיפוסים רבים של ה-framework. על דריסה, sealed עוצר את השרשרת ברמה הזו:
class Shape { public virtual string Name() => "shape"; }
class Square : Shape { public sealed override string Name() => "square"; }
class Tile : Square { public override string Name() => "tile"; }
// error CS0239: 'Tile.Name()': cannot override inherited member 'Square.Name()' because it is sealed
תכנון מחלקה לירושה דורש עבודה: להחליט מה וירטואלי, על מה מחלקות נגזרות רשאיות לסמוך, באיזה סדר דברים קורים. מחלקה שלא תוכננה כך בטוחה יותר כשהיא sealed, ואפשר לבטל איטום מאוחר יותר בלי לשבור אף אחד, בעוד שאת ביטול האיטום אי אפשר להחזיר אחרי שאחרים גוזרים מכם. קריאות לאיברים של מחלקות sealed גם יכולות להיות מהירות מעט יותר, כי סביבת הריצה יודעת שאין דריסה.
מחלקת בסיס אחת, הרבה ממשקים
למחלקה ב-C# יש בדיוק מחלקת בסיס אחת. class Admin : User, Employee היא שגיאה CS1721 ("cannot have multiple base classes"). עם זאת, מחלקה יכולה לממש כל מספר של ממשקים לצד מחלקת הבסיס שלה:
class Admin : User, IAuditable, IComparable<Admin>
{
// base class first, then interfaces, in any order
}
השתמשו במחלקת בסיס עבור "הוא סוג של, וחולק איתו מימוש", ובממשקים עבור "יודע לעשות". כשמחלקת הבסיס קיימת רק כדי לחייב מחלקות נגזרות להשלים כמה מתודות, מחלקה אבסטרקטית היא הכלי.
המרה למעלה ולמטה בהיררכיה
תמיד אפשר להשתמש באובייקט נגזר במקום שבו מצופה טיפוס הבסיס שלו. ה-upcast הזה מרומז ולא יכול להיכשל. בכיוון השני, downcast צריך המרה מפורשת ונכשל בזמן ריצה אם האובייקט אינו מהטיפוס הזה:
פלט:
Rex fetches the ball
InvalidCastException: Tom is not a Dog
True
Rex fetches the ball
כתיבה של Dog d = pet; בלי ההמרה היא שגיאת קומפילציה (CS0266: "An explicit conversion exists (are you missing a cast?)"), כי הקומפיילר יודע רק ש-pet הוא Animal כלשהו. העדיפו is עם משתנה כשהאובייקט עשוי להיות מטיפוס אחר, והמרה פשוטה רק כשכל דבר אחר יהיה באג. downcasting תכוף הוא סימן לבעיה בתכנון: בדרך כלל זה אומר שההתנהגות שייכת למתודה וירטואלית במחלקת הבסיס. העמוד על pattern matching מכסה את כל הצורות של is.
ירושה ואוספים
פולימורפיזם עובד איבר אחרי איבר, אבל אוספים גנריים של טיפוס נגזר הם לא אוספים של טיפוס הבסיס: List<Animal> animals = new List<Dog>(); לא מתקמפל, כי אז הרשימה הייתה מקבלת Cat. תצוגות לקריאה בלבד הן covariant, ולכן IEnumerable<Animal> animals = new List<Dog>(); בסדר. העמוד על generics מסביר למה.
טעויות נפוצות
- לשכוח
override. המתודה מתקמפלת עם אזהרה ומסתירה בשקט במקום לדרוס. התייחסו ל-CS0114 כשגיאה. - להפוך הכול ל-
virtual. כל איבר וירטואלי הוא הבטחה למחלקות נגזרות לגבי מתי קוראים לו ומה הוא רשאי להניח. סמנו רק את נקודות ההרחבה שאתם מתכוונים אליהן. - היררכיות עמוקות. שלוש או ארבע רמות של ירושה מקשות לדעת איזו גרסה של מתודה רצה. הרכבה (composition), מחלקה שמחזיקה אובייקט אחר וקוראת לו, היא לעיתים קרובות פשוטה יותר מרמה חדשה.
- קריאה למתודות וירטואליות מתוך בנאי. הדריסה רצה לפני הגוף של הבנאי הנגזר, ולכן כל שדה שהגוף הזה מאתחל עדיין מחזיק את ערך ברירת המחדל שלו.
- שימוש בירושה רק כדי לעשות שימוש חוזר בקוד. אם
Stackיורשת מ-List, מי שקורא יכול לבצעInsertלאמצע המחסנית שלכם. החזיקו במקום זאתListבשדה פרטי.
שאלות נפוצות
איך עובדת ירושה ב-C#?
מחלקה מציינת מחלקת בסיס אחת אחרי נקודתיים: class Dog : Animal. המחלקה הנגזרת מקבלת את כל האיברים של מחלקת הבסיס (שדות, מאפיינים, מתודות, אירועים), יכולה להוסיף איברים משלה, ויכולה לדרוס את אלה שהבסיס סימן כ-virtual. בנאים לא עוברים בירושה, ואיברים private, למרות שהם קיימים באובייקט, לא נגישים מהמחלקה הנגזרת.
מה ההבדל בין virtual ל-override ב-C#?
virtual נכתב על המתודה במחלקת הבסיס ואומר "מחלקות נגזרות רשאיות להחליף את זה". override נכתב על המתודה במחלקה הנגזרת ומבצע את ההחלפה. צריך את שניהם: דריסה של מתודה שאינה virtual, abstract או כבר override היא שגיאה CS0506. כשקוראים למתודה וירטואלית, סביבת הריצה מריצה את הגרסה של הטיפוס האמיתי של האובייקט, לא של טיפוס המשתנה.
מה ההבדל בין new ל-override ב-C#?
override מחליף את מתודת הבסיס עבור כל מי שקורא, גם קוד שמחזיק את האובייקט דרך משתנה מטיפוס הבסיס. new רק מסתיר אותה: קוד שרואה את האובייקט כטיפוס הנגזר קורא למתודה החדשה, בעוד שקוד שרואה אותו כטיפוס הבסיס עדיין קורא למתודת הבסיס. לכן Base b = new Derived(); b.M(); מריץ את Derived.M עם override ואת Base.M עם new.
האם C# תומכת בירושה מרובה?
לא עבור מחלקות: למחלקה יש בדיוק מחלקת בסיס אחת, וציון של שתיים הוא שגיאה CS1721. מחלקה יכולה לממש כל מספר של ממשקים, וכך C# מתארת "הטיפוס הזה יודע לעשות כמה דברים". מאז C# 8, ממשקים יכולים גם לשאת מימושי ברירת מחדל למתודות.
מה המשמעות של sealed ב-C#?
אי אפשר להשתמש במחלקה sealed כמחלקת בסיס; גזירה ממנה היא שגיאה CS0509. string, למשל, היא sealed. על מתודה, sealed override מונע ממחלקות שנמצאות נמוך יותר בהיררכיה לדרוס אותה שוב (CS0239). איטום של מחלקות שלא תוכננו לירושה הוא ברירת מחדל סבירה.
איך קוראים למתודה של מחלקת הבסיס ב-C#?
השתמשו ב-base.MethodName(...) בתוך המחלקה הנגזרת, בדרך כלל בתוך הדריסה: public override string Describe() => base.Describe() + " with GPS";. בבנאים, השתמשו ב-: base(...) אחרי רשימת הפרמטרים כדי לבחור איזה בנאי של הבסיס ירוץ.