C#: المجموعات والأنواع العامة

1. المفاهيم العامة والدوافع

تتيح لنا العناصر العامة كتابة كود معلم بالأنواع، مما يحقق أمان الأنواع وإعادة استخدام الكود. قبل ظهور العناصر العامة، كان ArrayList يخزن أنواع object؛ حيث كانت أنواع القيم تُعبَّأ عند إضافتها وتُفكَّ عند استرجاعها، مما أضر بالأداء وأدى إلى عدم وجود فحص للأنواع في وقت التحويل البرمجي.

▶ مثال

CSHARP
using System;
using System.Collections;

ArrayList list = new ArrayList();
list.Add(42);
list.Add("hello");

int value = (int)list[0];

foreach (object item in list)
{
    Console.WriteLine(item);
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
42
hello

تحدد النسخة العامة أنواع العناصر في وقت التحويل البرمجي، ولا تتطلب أي عملية تغليف أو فك تغليف، ويتم اكتشاف الأخطاء أثناء التحويل البرمجي:

CSHARP
using System;
using System.Collections.Generic;

List````<int>```` numbers = new List````<int>````();
numbers.Add(42);

foreach (int n in numbers)
{
    Console.WriteLine(n);
}
TEXT 📖 للعرض فقط
42

2. قائمة <T> قائمة ديناميكية

List<T> هي المجموعة العامة الأكثر استخدامًا، حيث تدعم قائمة مرتبة قابلة لتغيير حجمها ديناميكيًّا، مع مجموعة غنية من الأساليب لإضافة العناصر وحذفها والبحث عنها وتعديلها.

▶ مثال

CSHARP
using System;
using System.Collections.Generic;

List````<string>```` fruits = new List````<string>```` { "apple", "banana", "cherry" };

fruits.Add("date");
fruits.AddRange(new string[] { "elderberry", "fig" });
fruits.Insert(1, "blueberry");

bool hasApple = fruits.Contains("apple");
int idx = fruits.IndexOf("cherry");

string found = fruits.Find(f => f.StartsWith("b"));
List````<string>```` allB = fruits.FindAll(f => f.StartsWith("b"));

fruits.Sort();
fruits.ForEach(f => Console.WriteLine(f));

Console.WriteLine("---");
Console.WriteLine($"Count: {fruits.Count}");
Console.WriteLine($"Has apple: {hasApple}");
Console.WriteLine($"Cherry index: {idx}");
Console.WriteLine($"First b-word: {found}");
Console.WriteLine($"All b-words: {string.Join(", ", allB)}");
▶ جرّب الكود
TEXT 📖 للعرض فقط
apple
blueberry
banana
cherry
date
elderberry
fig
---
Count: 7
Has apple: True
Cherry index: 3
First b-word: blueberry
All b-words: blueberry, banana

(1) إزالة العناصر

CSHARP
using System;
using System.Collections.Generic;

List````<int>```` nums = new List````<int>```` { 10, 20, 30, 40, 50 };

nums.Remove(30);
nums.RemoveAt(0);

Console.WriteLine(string.Join(", ", nums));
Console.WriteLine($"Count: {nums.Count}");
TEXT 📖 للعرض فقط
20, 40, 50
Count: 3

3. القاموس`<TKey, TValue>` القاموس

يخزن Dictionary<TKey, TValue> أزواج القيم والمفاتيح، مما يتيح البحث السريع عن القيم باستخدام المفتاح بزمن بحث يقارب O(1).

▶ مثال

CSHARP
using System;
using System.Collections.Generic;

Dictionary`<string, int>` scores = new Dictionary`<string, int>`
{
    { "Alice", 90 },
    { "Bob", 85 }
};

scores.Add("Charlie", 78);
scores["Bob"] = 92;

bool exists = scores.ContainsKey("Alice");

if (scores.TryGetValue("Dave", out int daveScore))
{
    Console.WriteLine($"Dave: {daveScore}");
}
else
{
    Console.WriteLine("Dave not found");
}

scores.Remove("Charlie");

Console.WriteLine($"Count: {scores.Count}");
Console.WriteLine("---Keys---");
foreach (string key in scores.Keys)
{
    Console.WriteLine(key);
}
Console.WriteLine("---Values---");
foreach (int val in scores.Values)
{
    Console.WriteLine(val);
}
Console.WriteLine("---Pairs---");
foreach (KeyValuePair`<string, int>` kv in scores)
{
    Console.WriteLine($"{kv.Key}: {kv.Value}");
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Dave not found
Count: 2
---Keys---
Alice
Bob
---Values---
90
92
---Pairs---
Alice: 90
Bob: 92

4. الطرق العامة والفئات العامة

لا تقتصر العناصر العامة على المجموعات فحسب؛ بل يمكن للطرق والفئات أيضًا استخدام معلمات الأنواع، مما يجعل الخوارزميات قابلة للتطبيق على أنواع متعددة.

▶ مثال

CSHARP
using System;

T Max```<T>```(T a, T b) where T : IComparable```<T>```
{
    return a.CompareTo(b) >= 0 ? a : b;
}

Console.WriteLine(Max(3, 7));
Console.WriteLine(Max("apple", "banana"));
▶ جرّب الكود
TEXT 📖 للعرض فقط
7
banana

مثال على الفئة العامة:

CSHARP
using System;

class Box```<T>```
{
    private T _value;

    public Box(T value)
    {
        _value = value;
    }

    public T Value => _value;

    public override string ToString() => $"Box<{typeof(T).Name}>: {_value}";
}

Box````<int>```` intBox = new Box````<int>````(42);
Box````<string>```` strBox = new Box````<string>````("hello");

Console.WriteLine(intBox);
Console.WriteLine(strBox);
TEXT 📖 للعرض فقط
Box<Int32>: 42
Box<String>: hello

5. القيود العامة

استخدم الكلمة الرئيسية where لفرض قيود على معلمات الأنواع، مما يحد من نطاق الأنواع التي يمكن استخدامها.

▶ مثال

CSHARP
using System;

class Repository```<T>``` where T : class, new()
{
    public T Create()
    {
        T instance = new T();
        Console.WriteLine($"Created: {typeof(T).Name}");
        return instance;
    }
}

class Player
{
    public string Name { get; set; } = "Unknown";
}

Repository<Player> repo = new Repository<Player>();
Player p = repo.Create();
Console.WriteLine($"Name: {p.Name}");
▶ جرّب الكود
TEXT 📖 للعرض فقط
Created: Player
Name: Unknown

مرجع للقيود الشائعة:

القيد الوصف
where T : class يجب أن يكون T من النوع المرجعي
where T : struct يجب أن يكون T نوعًا قيمياً
where T : new() يجب أن تحتوي T على مُنشئ عام بدون معلمات
where T : IFoo يجب أن تنفذ T واجهة IFoo
where T : Base يجب أن ترث T من الفئة الأساسية

6. قائمة الانتظار <T> قائمة الانتظار

الطابور هو بنية بيانات تعمل وفقًا لمبدأ «الدخول أولاً، الخروج أولاً» (FIFO). استخدم Enqueue للإضافة وDequeue للحذف.

▶ مثال

CSHARP
using System;
using System.Collections.Generic;

Queue````<string>```` queue = new Queue````<string>````();
queue.Enqueue("first");
queue.Enqueue("second");
queue.Enqueue("third");

string front = queue.Peek();
Console.WriteLine($"Peek: {front}");

while (queue.Count > 0)
{
    Console.WriteLine($"Dequeue: {queue.Dequeue()}");
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Peek: first
Dequeue: first
Dequeue: second
Dequeue: third

7. المكدس <T> المكدس

المكدس هو بنية بيانات تعمل وفقًا لمبدأ «آخر ما يدخل أول ما يخرج» (LIFO). استخدم Push للإضافة وPop للحذف.

▶ مثال

CSHARP
using System;
using System.Collections.Generic;

Stack````<int>```` stack = new Stack````<int>````();
stack.Push(10);
stack.Push(20);
stack.Push(30);

int top = stack.Peek();
Console.WriteLine($"Peek: {top}");

while (stack.Count > 0)
{
    Console.WriteLine($"Pop: {stack.Pop()}");
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Peek: 30
Pop: 30
Pop: 20
Pop: 10

8. مُهيئات المجموعات

تتيح لك مُهيئات المجموعات ملء العناصر مباشرةً عند إنشاء المجموعة، وذلك باستخدام صيغة موجزة وسهلة الفهم.

▶ مثال

CSHARP
using System;
using System.Collections.Generic;

List````<int>```` nums = new List````<int>```` { 1, 2, 3, 4, 5 };
Dictionary`<string, double>` prices = new Dictionary`<string, double>`
{
    { "coffee", 3.5 },
    { "tea", 2.8 },
    { "juice", 4.0 }
};

Console.WriteLine(string.Join(", ", nums));
foreach (var kv in prices)
{
    Console.WriteLine($"{kv.Key}: ${kv.Value}");
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
1, 2, 3, 4, 5
coffee: $3.5
tea: $2.8
juice: $4

9. مقدمة إلى IEnumerable والمكررات

IEnumerable<T> هي الواجهة الأساسية لجميع المجموعات القابلة للتجول، وتعتمد حلقة foreach عليها. باستخدام yield return، يمكنك بسهولة كتابة متكررات مخصصة (تجد التفاصيل في الدرس 35).

▶ مثال

CSHARP
using System;
using System.Collections.Generic;

IEnumerable````<int>```` GetRange(int start, int end)
{
    for (int i = start; i <= end; i++)
    {
        yield return i;
    }
}

foreach (int n in GetRange(1, 5))
{
    Console.WriteLine(n);
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
1
2
3
4
5
💡 yield return يحول الدالة إلى مُكرر: حيث يُرجع كل استدعاء قيمة واحدة ثم يتوقف مؤقتًا، وتستأنف الدورة التالية من نقطة التوقف. للاطلاع على شرح كامل، انظر الدرس 35.


❓ أسئلة شائعة

س ما الفرق بين List<T> والمصفوفات؟
ج تدعم List<T> إضافة العناصر وحذفها ديناميكيًا، بينما تتميز المصفوفات بطول ثابت. يتم دعم List داخليًا بواسطة مصفوفة؛ وعند الحاجة إلى تغيير الحجم، تقوم تلقائيًا بإنشاء مصفوفة أكبر ونسخ العناصر إليها.
س ماذا يحدث إذا أضفت مفتاحًا مكررًا إلى قاموس (Dictionary)؟
ج يؤدي استخدام طريقة Add مع مفتاح مكرر إلى إثارة استثناء ArgumentException؛ أما استخدام التعيين عبر المؤشر dict[key] = value فيؤدي إلى استبدال القيمة الموجودة.
س هل يمكن استخدام where T : class وwhere T : struct معًا؟
ج لا، فقيد الفئة يتطلب نوعًا مرجعيًّا، بينما يتطلب قيد البنية نوعًا قيميًّا — وهما متنافيان.
س هل يمكن تصفح قائمة الانتظار (Queue) والمكدس (Stack) باستخدام foreach؟
ج نعم، فالتصفح باستخدام foreach لا يزيل العناصر؛ بل يقتصر على قراءتها فقط. أما Dequeue وPop فهما اللتان تزيلان العناصر فعليًّا.
س لماذا يُعد القيد العام where T : new() ضروريًا؟
ج لأنه يضمن إمكانية استخدام new T() لإنشاء مثيل داخل دالة. فبدون هذا القيد، لا يسمح المُترجم باستخدام عامل new على T.

📖 ملخص

📝 تمارين

  1. قم بإنشاء List<double>، وأضف 5 درجات، واستخدم FindAll لتصفية الدرجات التي تزيد عن 80 وعرضها
  2. إنشاء Dictionary<string, string> لتخزين 3 توزيعات بين العواصم والمقاطعات، وتنفيذ عملية البحث عن مقاطعة باستخدام اسم المدينة (باستخدام TryGetValue)
  3. اكتب دالة عامة `T Clamp<T>(T value, T min, T max) where T : IComparable<T> تحدد قيمة ضمن النطاق [min, max]
  4. استخدم Queue<string> لمحاكاة قائمة انتظار الطباعة: أضف 3 أسماء مستندات إلى قائمة الانتظار، ثم أخرجها من القائمة واطبعها بالترتيب
  5. اكتب دالة تكرارية IEnumerable<int> باستخدام yield return لتوليد أول 10 حدود لمتتابعة فيبوناتشي
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%