מה זה?

כבר השתמשנו ב-List<T> ו-Dictionary<TKey, TValue> לאורך הקורס — הם עבדו על int, string, Student בלי שום שינוי בקוד שלהם. איך זה אפשרי? הם כתובים עם Generics — קוד שכותבים פעם אחת, עם T כ"placeholder לטיפוס", ו-C# ממלאת אותו בטיפוס האמיתי בכל שימוש. בלי Generics, היינו צריכים IntStack, StringStack, StudentStack — מחלקה נפרדת לכל טיפוס, עם אותו קוד בדיוק פרט לשם הטיפוס.

מילות מפתח שחשוב לזכור

• T (Type Parameter) — placeholder לטיפוס, מוחלף בטיפוס האמיתי בזמן קומפילציה בכל שימוש קונקרטי

• Generic Class — מחלקה שמוגדרת עם <T>, וכל שימוש בה בוחר טיפוס ספציפי (Stack<int>, Stack<string>)

• Dictionary<TKey, TValue> — אוסף Generic עם שני Type Parameters: מפתח וערך

• Type Safety — הקומפיילר בודק ש-Stack<int> מכיל רק int, בדיוק כמו כל טיפוס אחר — אין “ערבוב” אפשרי

// Generic class — written once, for every type
class Box<T>
{
    private T _item;
    public void Set(T item) => _item = item;
    public T Get() => _item;
}

Box<int> intBox = new Box<int>();
intBox.Set(42);
Console.WriteLine(intBox.Get());   // 42

Box<string> stringBox = new Box<string>();
stringBox.Set("Hello");
Console.WriteLine(stringBox.Get());   // Hello
// intBox.Set("text");   // compile error — Box<int> only accepts int
// Dictionary<TKey, TValue> — two Type Parameters
Dictionary<string, int> ages = new Dictionary<string, int>();
ages["Dana"] = 25;
ages["Avi"] = 30;

if (ages.TryGetValue("Dana", out int age))
{
    Console.WriteLine($"Age: {age}");   // 25
}
// Custom generic method
static T FindMax<T>(List<T> items) where T : IComparable<T>
{
    T max = items[0];
    foreach (T item in items)
    {
        if (item.CompareTo(max) > 0) max = item;
    }
    return max;
}

int maxNumber = FindMax(new List<int> { 3, 7, 2, 9, 4 });   // 9

הסבר עיקרי

למה לא סתם להשתמש ב-object במקום T — אפשר היה לכתוב Box עם object פנימי במקום T, שיכול “להכיל הכל”. אבל אז מאבדים את בדיקת הטיפוסים — object יכול להיות כל דבר, כולל טעות (לשים int ולשלוף כ-string וזה יקרוס בזמן ריצה). T שומר על Type Safety מלאה: Box<int> תמיד מכיל int, נבדק בקומפילציה, בדיוק כמו כל טיפוס רגיל.

where T : IComparable<T> — constraint (אילוץ) — לפעמים מתודה Generic צריכה לדעת משהו על T כדי לפעול (כמו FindMax שצריך להשוות ערכים). ה-where מגביל אילו טיפוסים מותר להשתמש כ-T — כאן: רק טיפוסים שמממשים IComparable<T> (בדיוק מה שתרגלנו בשיעור ה-Interfaces). בלי ה-constraint, item.CompareTo(max) בכלל לא היה מתקמפל — C# לא הייתה יודעת שכל T אפשרי תומך בהשוואה.

Dictionary<TKey, TValue> הוא בדיוק אותו רעיון עם שני Type Parameters — כמו List<T> עם T אחד, Dictionary מקבל שניים: TKey (סוג המפתח) ו-TValue (סוג הערך). כל שימוש קונקרטי (Dictionary<string, int>) בוחר טיפוסים ספציפיים, וה-Dictionary הגנרי (שנכתב פעם אחת ב-.NET) עובד זהה לכל שילוב.

יתרונות

Generics חוסכים שכפול קוד בין טיפוסים דומים; שומרים על Type Safety מלאה — שגיאות טיפוס נתפסות בקומפילציה, לא בזמן ריצה; where constraints מאפשרים למתודה Generic להשתמש בהתנהגות ספציפית (כמו CompareTo) כשהיא נחוצה.

חסרונות

תחביר Generic עם constraints מרובים יכול להיות מסורבל לקריאה; שגיאות קומפילציה על קוד Generic (במיוחד מורכב) לפעמים ארוכות ומבלבלות יותר מקוד רגיל.

נקודות חשובות

• T הוא placeholder לטיפוס, נקבע בזמן שימוש קונקרטי (Box<int>, Box<string>)

• Generics שומרים Type Safety מלאה — לא כמו object שמאבד בדיקת טיפוסים

• where T : IComparable<T> הוא constraint — מגביל אילו טיפוסים מותר להשתמש כ-T

• List<T>, Dictionary<TKey,TValue> הם Generics שכבר בשימוש לאורך כל הקורס

טעויות נפוצות

• להשתמש ב-object במקום Generics “כי זה יותר גמיש” — מאבד Type Safety ומוסיף המרות (casting) מסוכנות

• לנסות להשתמש במתודה שדורשת IComparable על T בלי constraint מתאים — שגיאת קומפילציה

• לבלבל בין T (Type Parameter, בזמן קומפילציה) ל-object (טיפוס בסיס לכל דבר, בדיקה בזמן ריצה)

סיכום

Generics מאפשרים לכתוב קוד (מחלקות ומתודות) פעם אחת שעובד על כל טיפוס, עם T כ-placeholder שנקבע בכל שימוש קונקרטי — תוך שמירה מלאה על בדיקת טיפוסים בקומפילציה. List<T> ו-Dictionary<TKey, TValue> הם דוגמאות מובנות ב-.NET; where T : ... מאפשר להגביל אילו טיפוסים תואמים, כשצריך להשתמש בהתנהגות ספציפית שלהם.

דוקומנטציה רשמית

Microsoft Learn — Generics

Microsoft Learn — Dictionary<TKey,TValue>


תרגילים

תרגיל 1 — מחלקה Generic ראשונה

המשימה: כתבו class Pair<T1, T2> עם שני שדות, First ו-Second, וקונסטרוקטור שמקבל אותם. צרו מופע עם Pair<string, int>.

בדיקה: Pair<string, int> p = new Pair<string, int>("דנה", 25); מתקמפל ומדפיס נכון את שני השדות.

תרגיל 2 — Dictionary בפועל

המשימה: בנו Dictionary<string, List<string>> שממפה שם מחלקה לרשימת תלמידים בה. הוסיפו 2 מחלקות עם תלמידים, והדפיסו את כולם עם foreach מקונן.

בדיקה: הפלט מציג כל מחלקה עם רשימת התלמידים הנכונה שלה.


פרויקט מסכם

המשימה: כתבו מחלקת Repository<T> גנרית פשוטה לניהול אוסף בזיכרון.

דרישות:

  1. class Repository<T> עם List<T> פנימית, ומתודות Add(T item), GetAll() (מחזירה List<T>), ו-Count() (מחזירה int)
  2. שני שימושים שונים: Repository<string> לניהול שמות, ו-Repository<int> לניהול מספרים
  3. הדגימו הוספה והדפסת התוכן בשני המקרים

בדיקה: אותה מחלקת Repository<T> בדיוק עובדת גם על string וגם על int, בלי שום שינוי בקוד שלה — Type Safety נשמרת בשני המקרים.

מה בפרק הבא

בפרק הבא — SOLID: SRP ו-OCP: שני העקרונות הראשונים שמנחים איך לפצל אחריות בין מחלקות, ואיך להוסיף התנהגות חדשה בלי לשנות קוד קיים.