Menu
flag Ar iconالعربيةdown icon

الأنواع العامة Generics في C#: الأصناف والدوال العامة والقيود

كيف تعمل الأنواع العامة في C#: كتابة أصناف ودوال عامة بمعاملات نوع، واستنتاج الأنواع، والقيود بـ where (class وstruct وnew() والأصناف الأساسية والواجهات)، وdefault(T)، والأعضاء الساكنة لكل نوع، والتغاير مع IEnumerable<out T>.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

تتيح لك الأنواع العامة (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; كلتاهما، لأنه لا يمكن إضافة شيء عبرهما.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن