Generics
מה זה?
כבר השתמשנו ב-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 — 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> גנרית פשוטה לניהול אוסף בזיכרון.
דרישות:
class Repository<T>עםList<T>פנימית, ומתודותAdd(T item),GetAll()(מחזירהList<T>), ו-Count()(מחזירהint)- שני שימושים שונים:
Repository<string>לניהול שמות, ו-Repository<int>לניהול מספרים - הדגימו הוספה והדפסת התוכן בשני המקרים
בדיקה: אותה מחלקת Repository<T> בדיוק עובדת גם על string וגם על int, בלי שום שינוי בקוד שלה — Type Safety נשמרת בשני המקרים.
מה בפרק הבא
בפרק הבא — SOLID: SRP ו-OCP: שני העקרונות הראשונים שמנחים איך לפצל אחריות בין מחלקות, ואיך להוסיף התנהגות חדשה בלי לשנות קוד קיים.