Generics מאפשרים לכתוב טיפוס או מתודה פעם אחת ולהשתמש בהם עם טיפוסים רבים, בלי לוותר על בטיחות טיפוסים. במקום List של object שצריך המרות ומקבל כל דבר, מקבלים List<int> ו-List<Order>, שכל אחד מהם נבדק על ידי הקומפיילר. ממלא המקום שבסוגריים המשולשים, לפי המוסכמה T, הוא פרמטר טיפוס; הטיפוס שמי שקורא מספק הוא ארגומנט הטיפוס.
למה generics: הבעיה שהם פותרים
לפני generics (C# 1), אוספים שמרו object. זה התקמפל, אבל טעויות התגלו רק בזמן ריצה, וכל int עבר boxing בדרך פנימה:
var prices = new System.Collections.ArrayList();
prices.Add(9.99m);
prices.Add("12.50"); // compiles: anything is an object
decimal total = 0;
foreach (object p in prices)
total += (decimal)p; // InvalidCastException on the string
עם List<decimal>, ה-Add השני הוא שגיאת קומפילציה, הלולאה לא צריכה המרה, וה-decimals נשמרים כ-decimals.
מחלקות גנריות
הצהירו על פרמטרי טיפוס אחרי שם המחלקה, ואז השתמשו בהם כמו בכל טיפוס אחר בתוך המחלקה:
פלט:
Page 2/3: Chen, Dara, more: True
Page 1/1: 90, 72, 85, more: False
Page<string> ו-Page<int> הם שני טיפוסים שונים שנוצרו מהגדרה אחת. מחלקה יכולה לקבל כמה פרמטרי טיפוס (Dictionary<TKey, TValue>); קראו להם T כשיש אחד ו-TSomething כשיש כמה.
מתודות גנריות והסקת טיפוסים
למתודה יכולים להיות פרמטרי טיפוס משלה, בין אם המחלקה שלה גנרית ובין אם לא. מי שקורא בדרך כלל משמיט את ארגומנט הטיפוס, כי הקומפיילר מסיק אותו מהארגומנטים:
פלט:
2 1
back front
ho ho ho
4.50 EUR, 12.00 EUR, 0.99 EUR
ההסקה עובדת רק מהארגומנטים, אף פעם לא מטיפוס ההחזרה. Repeat(0, 3) לא צריך שום ציון, אבל למתודה כמו T Create<T>() אין ממה להסיק, ולכן מי שקורא חייב לכתוב Create<Customer>(). רוב LINQ הוא מתודות גנריות שנשענות על ההסקה הזו: prices.Where(p => p > 1) הוא Where<decimal> בלי שכתבתם את זה.
אילוצים: מה T רשאי להיות
בתוך מתודה גנרית, הקומפיילר מרשה לעשות רק מה שעובד עבור כל T אפשרי. בלי אילוץ, זה בקושי יותר ממה ש-object תומך בו. כדי לקרוא ל-CompareTo, ליצור מופע או לקרוא מאפיין, אמרו לקומפיילר מה T חייב להיות, עם where:
פלט:
17
plum
19.99
42 Zoe
האילוצים שתשתמשו בהם:
| אילוץ | משמעות | מאפשר |
|---|---|---|
where T : class | טיפוס הפניה | להשים null, להשתמש ב-as |
where T : struct | טיפוס ערך שאינו nullable | להשתמש ב-T? כ-Nullable<T> |
where T : new() | בנאי ציבורי בלי פרמטרים | לקרוא ל-new T() (חייב להופיע אחרון) |
where T : Entity | יורש מ-Entity | להשתמש באיברים של Entity |
where T : IComparable<T> | מממש את הממשק | לקרוא ל-CompareTo |
where T : U | יורש מפרמטר טיפוס אחר | לקשר בין שני פרמטרים |
where T : unmanaged (C# 7.3) | struct בלי הפניות בתוכו | מצביעים, stackalloc |
where T : Enum (C# 7.3) | טיפוס enum | להעביר למתודות של Enum |
where T : notnull (C# 8) | טיפוס שאינו nullable | הערות של nullable reference |
כמה אילוצים על פרמטר אחד מופרדים בפסיקים; לכל אחד מכמה פרמטרים יש פסוקית where משלו: class Cache<TKey, TValue> where TKey : notnull where TValue : class.
אילוץ חסר מופיע כשגיאת קומפילציה במקום שבו משתמשים באיבר, למשל CS1061 ('T' does not contain a definition for 'CompareTo') או CS0304 כשקוראים ל-new T() בלי new().
default(T)
קוד גנרי צריך לפעמים את "הערך הריק" של טיפוס שהוא לא מכיר. default(T) נותן 0 למספרים, false ל-bool, null לטיפוסי הפניה ולטיפוסים nullable, ו-struct מאופס בכל מקרה אחר:
פלט:
31
0
Porto
True
0001-01-01
השורה השנייה מראה את החולשה של התבנית הזו: בטיפוסי ערך, "לא נמצא" ו"נמצא 0" נראים אותו דבר. זו הסיבה שמתודות של ה-framework משתמשות במקום זאת בצורת TryGet (bool TryGetValue(TKey key, out TValue value)). מאז C# 7.1 אפשר לכתוב את הצורה הקצרה return default; כשהטיפוס ברור מההקשר.
איברים סטטיים הם לכל טיפוס בנוי
כל טיפוס בנוי (Counter<int>, Counter<string>) מקבל עותק משלו של השדות הסטטיים של המחלקה:
פלט:
2
1
False
זה שימושי לפעמים (cache לכל טיפוס, כמו TypeInfo<T>.Name שמחושב פעם אחת לכל T) ומפתיע לפעמים, כשציפיתם למונה משותף אחד. שימו מצב משותף במחלקה לא גנרית.
אוספים גנריים
האוספים הגנריים מכסים את רוב הצרכים: List<T>, Dictionary<TKey, TValue>, HashSet<T>, Queue<T>, Stack<T>, והממשקים המשותפים להם, IEnumerable<T>, ICollection<T>, IList<T>, IReadOnlyList<T>. קבלו את הממשק הצר ביותר שהמתודה צריכה (IEnumerable<T> אם היא רק עוברת בלולאה), והחזירו טיפוס קונקרטי או טיפוס לקריאה בלבד.
Covariance ו-contravariance
Dog הוא Animal, אבל List<Dog> הוא לא List<Animal>:
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs;
// error CS0029: Cannot implicitly convert type 'List<Dog>' to 'List<Animal>'
אם זה היה מותר, animals.Add(new Cat()) היה מכניס חתול לרשימת הכלבים. ממשקים שרק מפיקים ערכים הם בטוחים, ומוצהרים כ-covariant עם out:
IEnumerable<Animal> readOnly = dogs; // fine: IEnumerable<out T>
IReadOnlyList<Animal> alsoFine = dogs; // fine: IReadOnlyList<out T>
תמונת המראה, contravariance, שמסומנת ב-in, חלה על ממשקים ו-delegates שרק צורכים ערכים: אפשר להשתמש ב-Action<Animal> במקום שבו מצופה Action<Dog>, כי כל מה שיכול לטפל בכל חיה יכול לטפל בכלב. IComparer<in T> הוא contravariant, ו-Func<in T, out TResult> משתמש בשני הסימונים. variance חלה רק על ממשקים ו-delegates, ורק על ארגומנטים שהם טיפוסי הפניה: IEnumerable<int> לא מומר ל-IEnumerable<object>.
טעויות נפוצות
- שימוש ב-
objectבמקום שבו צריך פרמטר טיפוס. זה מוותר על בדיקות בזמן קומפילציה ומבצע boxing לטיפוסי ערך. - השוואה של שני ערכי
Tעם==. בלי אילוץclassזה לא מתקמפל (CS0019), ועם אילוץ כזה זה משווה הפניות. השתמשו ב-EqualityComparer<T>.Default.Equals(a, b), שעובד לכלTומכבד את ה-Equalsשל הטיפוס עצמו. - החזרת
default(T)בתור "לא נמצא". זה דו-משמעי בטיפוסי ערך. השתמשו ב-bool TryX(..., out T value). - יותר מדי אילוצים. כל אחד מצמצם את מי שיכול להשתמש במתודה. הגבילו רק את מה שהגוף באמת צריך.
- ציפייה ש-
List<Derived>יומר ל-List<Base>. קבלו במקום זאתIEnumerable<Base>אוIReadOnlyList<Base>.
שאלות נפוצות
מה זה generics ב-C#?
Generics מאפשרים לכתוב מחלקה, ממשק או מתודה פעם אחת עם טיפוס ממלא מקום, T, שמי שמשתמש ממלא: List<int>, List<Customer>. הקומפיילר בודק כל שימוש מול הטיפוס האמיתי, כך שמקבלים בטיחות טיפוסים בלי המרות, וטיפוסי ערך נשמרים בלי boxing. האוספים ב-System.Collections.Generic הם הדוגמה המוכרת ביותר.
איך כותבים מתודה גנרית ב-C#?
כתבו את פרמטרי הטיפוס אחרי שם המתודה: static T Largest<T>(List<T> items) where T : IComparable<T> { ... }. מי שקורא בדרך כלל משמיט את ארגומנט הטיפוס כי הקומפיילר מסיק אותו מהארגומנטים: Largest(prices) עובד כש-prices הוא List<decimal>. כתבו אותו במפורש, Largest<decimal>(...), כשאין ממה להסיק.
מה המשמעות של where T : class ב-C#?
זה אילוץ: T חייב להיות טיפוס הפניה, ולכן הקוד הגנרי רשאי להשים null ל-T ולהשתמש ב-as T. אילוצים אחרים הם where T : struct (טיפוס ערך שאינו nullable), where T : new() (יש בנאי ציבורי בלי פרמטרים, כך שהקוד יכול לקרוא ל-new T()), where T : SomeBaseClass, ו-where T : ISomeInterface, שמאפשר לקוד לקרוא לאיברים של הממשק הזה על T.
מה זה default(T) ב-C#?
ערך ברירת המחדל של הטיפוס ש-T יתברר שהוא: 0 למספרים, false ל-bool, null לטיפוסי הפניה ולטיפוסים nullable, ו-struct מאופס לשאר טיפוסי הערך. קוד גנרי משתמש בו כשהוא צריך "אין ערך" בלי לדעת את הטיפוס, למשל כערך ההחזרה של חיפוש שנכשל. C# 7.1 הוסיפה את הליטרל הקצר יותר default.
למה אי אפשר להשים List<Dog> ל-List<Animal> ב-C#?
כי List<Animal> מקבלת כל חיה: אם ההשמה הייתה מותרת, קוד היה יכול להוסיף Cat למה שהוא בעצם List<Dog>. הקומפיילר דוחה את זה (CS0029). ממשקים לקריאה בלבד הם covariant, ולכן IEnumerable<Animal> animals = dogs; וגם IReadOnlyList<Animal> animals = dogs; מתקמפלים, כי אי אפשר להוסיף דרכם שום דבר.