Abstract Classes, sealed ופולימורפיזם
מה זה?
בשיעור הקודם Shape הייתה מחלקת בסיס עם Area() שמחזירה 0 כברירת מחדל — אבל מה זה בכלל אומר “ליצור new Shape() בפני עצמה”? אין “צורה” מופשטת בעולם האמיתי — רק עיגולים, מלבנים וכו’. abstract פותר את זה: מונע ליצור מופע ישיר של מחלקת בסיס שהיא רק “תבנית משותפת”, ומחייב כל מחלקה יורשת לספק מימוש משלה לחלקים שאין להם ברירת מחדל הגיונית.
מילות מפתח שחשוב לזכור
• abstract class — מחלקה שלא ניתן ליצור ממנה מופע ישירות עם new; קיימת רק כדי שיירשו ממנה
• abstract method — מתודה בלי גוף בכלל ב-Base Class; כל מחלקה יורשת חייבת לספק override עבורה
• sealed — מונע ירושה נוספת ממחלקה, או override נוסף על מתודה — “כאן ההיררכיה נעצרת”
• Polymorphism (פולימורפיזם) — אותה קריאת מתודה (shape.Area()) מתנהגת אחרת בהתאם לטיפוס האמיתי של המופע בזמן ריצה
abstract class Shape
{
public abstract double Area(); // no body — every subclass must implement it
public void PrintArea() // regular method — shared by everyone as-is
{
Console.WriteLine($"Area: {Area()}");
}
}
class Circle : Shape
{
public double Radius;
public Circle(double radius) { Radius = radius; }
public override double Area() => Math.PI * Radius * Radius; // must implement
}
// Shape shape = new Shape(); // compile error — cannot instantiate an abstract class
Shape circle = new Circle(5);
circle.PrintArea(); // Area: 78.53...// Polymorphism — same call, different behavior depending on the actual type
List<Shape> shapes = new List<Shape> { new Circle(5), new Rectangle(4, 3) };
foreach (Shape shape in shapes)
{
shape.PrintArea(); // each shape runs its own Area(), even though the variable is declared as Shape
}// sealed — stops further inheritance
sealed class FinalReport : Shape
{
public override double Area() => 0;
}
// class ExtraReport : FinalReport { } // compile error — FinalReport is sealedהסבר עיקרי
abstract class מול abstract method — מחלקה abstract היא “בלתי-ניתנת-ליצירה” בפני עצמה, אבל יכולה עדיין להכיל מתודות רגילות עם מימוש (כמו PrintArea) שמשותפות לכל היורשים כמו שהן. מתודה abstract בתוכה היא הצהרה בלי גוף בכלל — “כל יורש חייב לספק מימוש, אין ברירת מחדל שהגיונית לכולם”.
למה abstract חזק יותר מ-virtual רגיל — virtual (מהשיעור הקודם) נותן ברירת מחדל שמחלקה יורשת יכולה לשכתב אם רוצה. abstract לא נותן שום ברירת מחדל — הקומפיילר מכריח כל מחלקה יורשת קונקרטית לממש אותה, אחרת שגיאת קומפילציה. זה מתאים בדיוק למקרה כמו Area() — אין “שטח ברירת מחדל” הגיוני לצורה כללית.
פולימורפיזם הוא התוצאה, לא מושג נפרד — כשיש List<Shape> עם עיגולים ומלבנים אמיתיים בפנים, קריאה ל-shape.Area() על כל איבר מריצה את המימוש הנכון של הטיפוס האמיתי שלו בזמן ריצה — לא של Shape (שאין לו מימוש בכלל, כי הוא abstract). זו בדיוק התוצאה המעשית של virtual/override/abstract יחד — הקוד הקורא (foreach) לא צריך לדעת בכלל איזה טיפוס קונקרטי כל איבר הוא.
יתרונות
abstract מונע יצירת מופעים חסרי-משמעות של מחלקת בסיס כללית; מכריח כל מחלקה קונקרטית לספק מימוש הגיוני, ותופס שכחה בזמן קומפילציה; פולימורפיזם מאפשר קוד גנרי (foreach על List<Shape>) שעובד נכון על כל תת-טיפוס בלי if/switch על הטיפוס.
חסרונות
sealed מונע הרחבה עתידית — צריך החלטה מודעת אם זה באמת רצוי; שימוש-יתר ב-abstract על מחלקות שבפועל אף פעם לא נגזרות מוסיף מורכבות מיותרת.
נקודות חשובות
• abstract class לא ניתנת ליצירה ישירה עם new; קיימת רק כתבנית לירושה
• abstract method חייבת override בכל מחלקה יורשת קונקרטית — אין ברירת מחדל
• sealed עוצר ירושה נוספת (על מחלקה) או override נוסף (על מתודה)
• פולימורפיזם = קריאה זהה (shape.Area()) מריצה מימוש שונה בזמן ריצה, לפי הטיפוס האמיתי
טעויות נפוצות
• לנסות new Shape() על מחלקה abstract ולהתפלא משגיאת קומפילציה
• לשכוח לממש מתודה abstract באחת ממחלקות היורשות ולקבל שגיאת קומפילציה שם
• לבלבל בין virtual (ברירת מחדל אופציונלית) ל-abstract (חובה למימוש, אין ברירת מחדל)
סיכום
abstract class מונעת יצירת מופע ישיר ממחלקת בסיס שהיא רק תבנית משותפת, ו-abstract method מכריחה כל מחלקה יורשת קונקרטית לספק מימוש משלה. sealed עוצר הרחבה נוספת (ירושה או override) במפורש. פולימורפיזם הוא התוצאה המעשית של המנגנון כולו — קוד יכול לטפל בכל תתי-הטיפוסים דרך טיפוס הבסיס המשותף, וכל אחד “יודע” להריץ את ההתנהגות הנכונה לו.
דוקומנטציה רשמית
Microsoft Learn — Abstract and sealed classes
Microsoft Learn — Polymorphism
תרגילים
תרגיל 1 — abstract class
המשימה: הפכו את Animal (מהשיעור הקודם) ל-abstract class עם abstract method בשם MakeSound() בלי מימוש כלל. ודאו ש-Dog : Animal מספקת override תקין.
בדיקה: new Animal() ישירות לא מתקמפל; new Dog() עדיין עובד ומדפיס את הקול הנכון.
תרגיל 2 — פולימורפיזם על List
המשימה: צרו List<Animal> עם כמה מופעי Dog וטיפוס יורש נוסף (Cat למשל), ועברו עליהם עם foreach תוך קריאה ל-MakeSound() על כל אחד.
בדיקה: כל איבר ברשימה מדפיס את הקול הספציפי שלו, למרות שהמשתנה בלולאה מוצהר כ-Animal הכללי.
פרויקט מסכם
המשימה: הרחיבו את פרויקט Shape הקודם עם sealed class Triangle : Shape (משולש, עם Base/Height), ושדרגו את PrintArea להיות מוגנת מפני ירושה נוספת מ-Triangle.
דרישות:
Triangleמסומנתsealed— נסו (ותעדו) שקומפיילר חוסם ניסיון ליצור ממנה יורשoverride Area()נכון (base * height / 2)List<Shape>עם עיגול, מלבן ומשולש, ולולאה שמדפיסה את שטח כולם עםPrintArea()
בדיקה: כל שלוש הצורות מדפיסות שטח נכון דרך אותה לולאה בדיוק; ניסיון ירושה מ-Triangle נכשל בקומפילציה.
מה בפרק הבא
בפרק הבא — Interfaces: מה עושים כשכמה מחלקות שאין ביניהן קשר “הוא-סוג-של” צריכות בכל זאת “חוזה” משותף — כמו IComparable שמאפשר למיין כל טיפוס.