تتيح لك الأنواع العامة (generics) كتابة نوع أو دالة مرة واحدة واستخدامها مع أنواع كثيرة، دون فقدان سلامة الأنواع. بدل List من object تحتاج إلى تحويلات وتقبل أي شيء، تحصل على List<int> وList<Order>، كل منها يفحصه المترجم. النائب بين علامتي الزاوية، T عرفًا، معامل نوع؛ والنوع الذي يقدّمه المستدعي وسيط النوع.
لماذا الأنواع العامة: المشكلة التي تحلّها
قبل الأنواع العامة (C# 1) كانت المجموعات تخزّن object. كان ذلك يُترجم، لكن الأخطاء لم تكن تظهر إلا وقت التشغيل، وكان كل int يُغلَّف عند دخوله:
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 الثاني خطأ ترجمة، ولا تحتاج الحلقة إلى تحويل، وتُخزَّن القيم العشرية كقيم عشرية.
الأصناف العامة
صرّح بمعاملات النوع بعد اسم الصنف، ثم استخدمها كأي نوع آخر داخل الصنف:
المخرجات:
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 | نوع قيمة غير قابل لـ null | استخدام 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) | بنية بلا مراجع في داخلها | المؤشرات، stackalloc |
where T : Enum (C# 7.3) | نوع تعداد | التمرير إلى دوال Enum |
where T : notnull (C# 8) | نوع غير قابل لـ null | وسوم المراجع القابلة لـ null |
تُفصل عدة قيود على معامل واحد بفواصل؛ ولكل معامل من عدة معاملات بند 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 للأنواع المرجعية والأنواع القابلة لـ null، وبنية مصفّرة في غير ذلك:
المخرجات:
31
0
Porto
True
0001-01-01
يُظهر السطر الثاني ضعف هذا النمط: في أنواع القيم يبدو "لم يُعثر عليه" و"عُثر على 0" متطابقين. ولهذا تستخدم دوال الإطار شكل TryGet بدلًا منه (bool TryGetValue(TKey key, out TValue value)). ومنذ C# 7.1 يمكنك كتابة return default; الأقصر حين يكون النوع واضحًا من السياق.
الأعضاء الساكنة لكل نوع مُنشأ
يحصل كل نوع مُنشأ (Counter<int> وCounter<string>) على نسخته الخاصة من الحقول الساكنة للصنف:
المخرجات:
2
1
False
هذا مفيد أحيانًا (ذاكرة مؤقتة لكل نوع مثل 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> إن كانت تمرّ فقط)، وأعد نوعًا فعليًا أو للقراءة فقط.
التغاير والتغاير المعاكس
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، ينطبق على الواجهات والمفوّضات التي تستهلك القيم فقط: يمكن استخدام Action<Animal> حيث يُتوقع Action<Dog>، لأن ما يستطيع التعامل مع أي حيوان يستطيع التعامل مع كلب. IComparer<in T> متغايرة معاكسة، وFunc<in T, out TResult> تستخدم العلامتين. ولا ينطبق التغاير إلا على الواجهات والمفوّضات، ولا إلا على وسائط الأنواع المرجعية: لا تتحوّل IEnumerable<int> إلى IEnumerable<object>.
أخطاء شائعة
- استخدام
objectحيث مكان معامل نوع. يتخلّى عن فحوص وقت الترجمة ويغلّف أنواع القيم. - مقارنة قيمتي
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>بدلًا من ذلك.
الأسئلة الشائعة
ما هي الأنواع العامة في C#؟
تتيح لك الأنواع العامة (generics) كتابة صنف أو واجهة أو دالة مرة واحدة بنوع نائب، T، يملؤه المستدعون: List<int> وList<Customer>. يفحص المترجم كل استخدام مقابل النوع الحقيقي، فتحصل على سلامة الأنواع دون تحويلات، وتُخزَّن أنواع القيم دون تغليف. المجموعات في 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 (نوع قيمة غير قابل لـ null)، وwhere T : new() (له مُنشئ عام بلا معاملات، فتستطيع الشيفرة استدعاء new T())، وwhere T : SomeBaseClass، وwhere T : ISomeInterface، التي تتيح للشيفرة استدعاء أعضاء تلك الواجهة على T.
ما هي default(T) في C#؟
القيمة الافتراضية لأي نوع يتبيّن أن T هو: 0 للأرقام، وfalse لـ bool، وnull للأنواع المرجعية والأنواع القابلة لـ null، وبنية مصفّرة لأنواع القيم الأخرى. تستخدمها الشيفرة العامة حين تحتاج إلى "لا قيمة" دون معرفة النوع، مثلًا كقيمة معادة لبحث فاشل. وأضافت C# 7.1 القيمة الحرفية الأقصر default.
لماذا لا أستطيع إسناد List<Dog> إلى List<Animal> في C#؟
لأن List<Animal> تقبل أي حيوان: لو سُمح بالإسناد لاستطاعت الشيفرة إضافة Cat إلى ما هو في الحقيقة List<Dog>. يرفض المترجم ذلك (CS0029). أما الواجهات للقراءة فقط فمتغايرة، فتُترجم IEnumerable<Animal> animals = dogs; وIReadOnlyList<Animal> animals = dogs; كلتاهما، لأنه لا يمكن إضافة شيء عبرهما.