ירושה — virtual ו-override
מה זה?
תארו לעצמכם מערכת עם Employee ו-Manager — למנהל יש את כל מה שיש לעובד רגיל (שם, משכורת) ועוד קצת (צוות שהוא מנהל). בלי ירושה, Manager היה מעתיק-מדביק את כל השדות והמתודות של Employee מחדש — כפילות שמסתכנת בחוסר-סנכרון אם Employee משתנה. ירושה (: BaseClass) פותרת את זה: Manager “יורש” אוטומטית את כל מה שיש ל-Employee, ומוסיף רק את מה שייחודי לו.
מילות מפתח שחשוב לזכור
• Base Class (מחלקת בסיס) — המחלקה שממנה יורשים; מגדירה את ההתנהגות המשותפת
• Derived Class (מחלקה נגזרת) — המחלקה שיורשת; מקבלת אוטומטית את כל מה שב-Base Class
• virtual — מסמן מתודה ב-Base Class כ"ניתנת לשכתוב" על ידי מחלקות יורשות
• override — מחלקה יורשת מגדירה מחדש מתודה virtual, עם התנהגות ייחודית לה
• base — מתייחס למימוש של המחלקה שממנה יורשים; שימושי לקרוא למימוש המקורי מתוך override
class Employee
{
public string Name;
public double Salary;
public Employee(string name, double salary)
{
Name = name;
Salary = salary;
}
public virtual string Describe()
{
return $"{Name}, employee, salary {Salary}";
}
}
class Manager : Employee // Manager inherits from Employee
{
public List<string> TeamMembers = new List<string>();
public Manager(string name, double salary) : base(name, salary) { }
public override string Describe()
{
return $"{base.Describe()}, manages a team of {TeamMembers.Count} people";
}
}Employee employee = new Employee("Dana", 15000);
Manager manager = new Manager("Avi", 22000);
manager.TeamMembers.Add("Rotem");
Console.WriteLine(employee.Describe()); // Dana, employee, salary 15000
Console.WriteLine(manager.Describe()); // Avi, employee, salary 22000, manages a team of 1 people
classDiagram
class Employee {
+string Name
+double Salary
+Describe() virtual
}
class Manager {
+List~string~ TeamMembers
+Describe() override
}
Employee <|-- Manager
הסבר עיקרי
: base(name, salary) — קונסטרוקטור של מחלקה יורשת חייב לוודא שהקונסטרוקטור של ה-Base Class רץ קודם, כדי ש-Name/Salary יאותחלו כראוי. התחביר : base(...) מעביר את הפרמטרים לקונסטרוקטור של Employee באופן מפורש.
למה virtual ו-override הכרחיים ביחד — בלי virtual על Describe ב-Employee, C# לא הייתה מרשה ל-Manager “לשכתב” את ההתנהגות — כל קריאה הייתה מריצה תמיד את הגרסה המקורית. virtual הוא “רישיון לשכתוב”; override הוא המימוש המחודש בפועל במחלקה היורשת.
base.Describe() — בתוך ה-override, אפשר לקרוא למימוש המקורי של ה-Base Class עם base.Describe(), ולהוסיף עליו — במקום לשכתב הכל מאפס. בדוגמה, Manager.Describe משתמש בתיאור הבסיסי של Employee ומוסיף עליו את פרטי הצוות — לא כותב את שם המשכורת מחדש.
יתרונות
ירושה מונעת שכפול קוד בין מחלקות קשורות; virtual/override מאפשרים לכל מחלקה יורשת להתאים התנהגות ספציפית לה, תוך שמירה על ממשק אחיד; base מאפשר להרחיב התנהגות קיימת בלי לשכתב אותה מאפס.
חסרונות
ירושה עמוקה מדי (מחלקה יורשת ממחלקה שיורשת ממחלקה…) הופכת קוד לקשה למעקב; שינוי במחלקת בסיס עלול להשפיע (לפעמים בהפתעה) על כל היורשות ממנה — “שביר” יותר מהרכבה (Composition), נושא שיוזכר בהמשך הקורס.
נקודות חשובות
• Derived Class מקבל אוטומטית את כל השדות/המתודות public/protected של ה-Base Class
• virtual ב-Base Class מאפשר override במחלקה יורשת; בלי virtual אי אפשר לשכתב
• base(...) בקונסטרוקטור מעביר פרמטרים לקונסטרוקטור של מחלקת הבסיס
• base.Method() בתוך override קורא למימוש המקורי, לא רק “מדלג” עליו
טעויות נפוצות
• לשכוח virtual על מתודה ב-Base Class ואז לתהות למה override לא מתקמפל
• לא לקרוא לקונסטרוקטור הבסיס (: base(...)) ולהשאיר שדות יורשים לא מאותחלים
• להשתמש בירושה כשבפועל אין קשר “הוא-סוג-של” אמיתי בין המחלקות (למשל Car : Engine — רכב הוא לא סוג של מנוע, הוא מכיל מנוע)
סיכום
ירושה (class Manager : Employee) מאפשרת למחלקה נגזרת לקבל אוטומטית שדות והתנהגות ממחלקת בסיס, ולהוסיף או לשנות רק את מה שייחודי לה. virtual על ה-Base Class מאפשר override במחלקה היורשת, ו-base מאפשר גישה למימוש המקורי — גם לקונסטרוקטור וגם למתודות — כדי להרחיב ולא לשכפל.
דוקומנטציה רשמית
תרגילים
תרגיל 1 — ירושה בסיסית
המשימה: הגדירו class Animal עם Name ומתודת virtual בשם MakeSound() שמדפיסה “השמעת קול כללית”. הגדירו class Dog : Animal עם override ל-MakeSound() שמדפיסה “הב הב!”.
בדיקה: קריאה ל-MakeSound() על מופע Animal מדפיסה את הקול הכללי; על מופע Dog מדפיסה “הב הב!”.
תרגיל 2 — base בקונסטרוקטור ובמתודה
המשימה: בדוגמת Employee/Manager מהשיעור, הוסיפו למחלקת Manager שדה Department, קונסטרוקטור שמעביר אותו דרך : base(...), ומתודת Describe שמשתמשת ב-base.Describe() ומוסיפה את המחלקה.
בדיקה: הדפסת manager.Describe() כוללת גם את פרטי Employee המקוריים וגם את המחלקה, בלי כפילות קוד.
פרויקט מסכם
המשימה: בנו היררכיית Shape פשוטה: class Shape עם virtual double Area() שמחזירה 0, ושתי מחלקות יורשות: Circle (עם Radius) ו-Rectangle (עם Width/Height), כל אחת עם override נכון לחישוב השטח שלה.
דרישות:
Shapeעם קונסטרוקטור בסיסי ומתודתvirtual Area()Circle : Shapeו-Rectangle : Shape, כל אחת עםoverride Area()נכון (π * r²ו-width * height)List<Shape>שמכילה כמה עיגולים ומלבנים, ולולאה שמדפיסה את השטח של כל אחד עםforeach
בדיקה: הלולאה מדפיסה שטח נכון לכל צורה, למרות שכולן מטופלות דרך אותו טיפוס Shape ברשימה — זו בדיוק ההוכחה שכל מופע “יודע” להריץ את ה-override הנכון שלו.
מה בפרק הבא
בפרק הבא — Abstract Classes, sealed ופולימורפיזם: מה קורה כשמחלקת בסיס לא אמורה בכלל להיות ניתנת ליצירה ישירה, ואיך C# אוכפת את זה.