تخطَّ إلى المحتوى

المجموعات في C#‎: Dictionary و HashSet و Queue و Stack

الدرس 11 من 11 · C#

في درس المصفوفات تعرّفت على List وعلى القاموس Dictionary وبعض دوال LINQ. لكن مكتبة المجموعات (Collections) في C#‎ أغنى من ذلك، ولكل نوع فيها مهمة يتقنها. في هذا الدرس ستتعمق في القاموس، وتتعرف على HashSet و Queue و Stack، ثم ترتب البيانات وتجمّعها بسطر واحد.

القاموس يخزن أزواجاً: مفتاح وقيمته، مثل دليل الهاتف: تبحث بالاسم فتصل للرقم مباشرة.

var stock = new Dictionary<string, int>
{
["قلم"] = 40,
["دفتر"] = 15,
};
stock["مسطرة"] = 8;
stock["قلم"] = 38;
stock.Remove("دفتر");
Console.WriteLine(stock["قلم"]);
Console.WriteLine(stock.Count);
Console.WriteLine(stock.ContainsKey("دفتر"));
if (stock.TryGetValue("مسطرة", out int rulers))
{
Console.WriteLine($"المساطر: {rulers}");
}
Console.WriteLine(stock.GetValueOrDefault("ممحاة", 0));
الناتج
38
2
False
المساطر: 8
0
  • stock[key] = value: تضيف زوجاً جديداً، أو تستبدل القيمة إذا كان المفتاح موجوداً. المفاتيح لا تتكرر.
  • قراءة مفتاح غير موجود بـ stock["ممحاة"] ترمي KeyNotFoundException.
  • TryGetValue: الطريقة الآمنة. تُرجع true وتضع القيمة في المتغير بعد out، أو تُرجع false بهدوء.
  • GetValueOrDefault: تُرجع قيمة بديلة إذا لم يوجد المفتاح.

والمرور على القاموس يعطيك كل زوج بمفتاحه وقيمته:

var ages = new Dictionary<string, int> { ["علي"] = 13, ["سارة"] = 14 };
foreach (var (name, age) in ages)
{
Console.WriteLine($"{name} عمره {age}");
}
Console.WriteLine(string.Join(", ", ages.Keys));
Console.WriteLine(ages.Values.Sum());
الناتج
علي عمره 13
سارة عمره 14
علي, سارة
27

مثال عملي: عدّ التكرارات

Section titled “مثال عملي: عدّ التكرارات”
string[] votes = { "أحمر", "أزرق", "أحمر", "أخضر", "أحمر", "أزرق" };
var counts = new Dictionary<string, int>();
foreach (string vote in votes)
{
counts[vote] = counts.GetValueOrDefault(vote) + 1;
}
foreach (var (color, count) in counts)
{
Console.WriteLine($"{color}: {count}");
}
الناتج
أحمر: 3
أزرق: 2
أخضر: 1

في كل مرة نقرأ العدد الحالي للون (أو صفراً إن لم يظهر من قبل)، ونزيده واحداً.

HashSet تشبه القائمة لكنها ترفض التكرار، والتحقق من وجود عنصر فيها سريع جداً:

var visitors = new HashSet<string> { "سارة", "عمر" };
Console.WriteLine(visitors.Add("ليلى"));
Console.WriteLine(visitors.Add("سارة"));
Console.WriteLine(visitors.Count);
Console.WriteLine(visitors.Contains("عمر"));
int[] numbers = { 3, 1, 3, 2, 1 };
var unique = new HashSet<int>(numbers);
Console.WriteLine(string.Join(", ", unique.Order()));
var football = new HashSet<string> { "علي", "منى", "زياد" };
var swimming = new HashSet<string> { "منى", "هدى", "علي" };
football.IntersectWith(swimming);
Console.WriteLine(string.Join(", ", football.Order()));
الناتج
True
False
3
True
1, 2, 3
علي, منى
  • Add تُرجع false إذا كان العنصر موجوداً ولا تضيفه مرة ثانية.
  • IntersectWith تُبقي المشترك فقط، و UnionWith تضيف كل عناصر المجموعة الأخرى، و ExceptWith تحذفها.
  • المجموعة لا تضمن ترتيباً لعناصرها، ولهذا رتبناها بـ Order() قبل الطباعة.

المجموعة مثل سجل الحضور: الاسم يُكتب مرة واحدة مهما دخل صاحبه وخرج.

الطابور (Queue) يعمل بقاعدة: من يدخل أولاً يخرج أولاً، تماماً مثل طابور المخبز:

var orders = new Queue<string>();
orders.Enqueue("طلب علي");
orders.Enqueue("طلب منى");
orders.Enqueue("طلب زياد");
Console.WriteLine(orders.Peek());
Console.WriteLine(orders.Dequeue());
Console.WriteLine(orders.Dequeue());
Console.WriteLine(orders.Count);
الناتج
طلب علي
طلب علي
طلب منى
1

Enqueue تضيف في آخر الطابور، و Dequeue تسحب من أوله، و Peek تنظر إلى الأول دون سحبه. تستخدمه لطلبات تُعالَج بالدور، أو رسائل تنتظر الإرسال.

المكدس (Stack) عكس الطابور: آخر من يدخل أول من يخرج، مثل كومة الأطباق: تضع الطبق فوق الكومة وتأخذ من فوقها.

var history = new Stack<string>();
history.Push("الصفحة الرئيسية");
history.Push("قائمة الكورسات");
history.Push("درس المجموعات");
Console.WriteLine(history.Pop());
Console.WriteLine(history.Peek());
Console.WriteLine(history.Count);
الناتج
درس المجموعات
قائمة الكورسات
2

Push تضع فوق الكومة و Pop تسحب من فوقها. زر «رجوع» في المتصفح وأمر «تراجع» في المحرر يعملان بهذه الفكرة بالضبط.

عرفت Where و Select من قبل. هذه دوال أخرى ستحتاجها مع أي مجموعة:

var students = new List<Student>
{
new("هدى", 88, "القاهرة"),
new("كريم", 95, "عمّان"),
new("ريم", 72, "القاهرة"),
new("زياد", 60, "تونس"),
};
var sorted = students.OrderByDescending(s => s.Grade).Select(s => s.Name);
Console.WriteLine(string.Join(", ", sorted));
Console.WriteLine(students.Any(s => s.Grade > 90));
Console.WriteLine(students.All(s => s.Grade >= 70));
Console.WriteLine(students.First(s => s.City == "القاهرة").Name);
Console.WriteLine(students.FirstOrDefault(s => s.Grade > 99) == null);
foreach (var group in students.GroupBy(s => s.City))
{
Console.WriteLine($"{group.Key}: {group.Count()} - المتوسط {group.Average(s => s.Grade)}");
}
record Student(string Name, int Grade, string City);
الناتج
كريم, هدى, ريم, زياد
True
False
هدى
True
القاهرة: 2 - المتوسط 80
عمّان: 1 - المتوسط 95
تونس: 1 - المتوسط 60
  • OrderBy و OrderByDescending: ترتيب حسب خاصية تختارها، والنتيجة تسلسل جديد لا يمس القائمة الأصلية.
  • Any: هل يوجد عنصر واحد على الأقل يحقق الشرط؟ و All: هل كل العناصر تحققه؟
  • First ترمي خطأ إذا لم تجد شيئاً، و FirstOrDefault تُرجع null بدلاً من ذلك.
  • GroupBy: توزّع العناصر على مجموعات حسب مفتاح، ولكل مجموعة Key وعناصرها.
تحتاج إلى استخدم
عناصر مرتبة تصل إليها برقم الموضع List<T>
البحث عن قيمة بمفتاح Dictionary<TKey, TValue>
عناصر بلا تكرار وفحص وجود سريع HashSet<T>
معالجة بالدور: الأول فالأول Queue<T>
التراجع: الأحدث أولاً Stack<T>
  1. أنشئ قاموساً يربط اسم الدولة بعاصمتها، واطبع عاصمة دولة غير موجودة بأمان باستخدام TryGetValue.
  2. خزّن جملة في متغير، وقسّمها بـ Split(' ')، واستخدم HashSet لمعرفة عدد الكلمات المختلفة فيها.
  3. حاكِ طابور انتظار في عيادة: أضف خمسة مرضى، ثم استدعِهم واحداً واحداً بحلقة while (queue.Count > 0).
  4. تحدٍّ: استخدم Stack<char> للتحقق من توازن الأقواس في نص مثل (a[b]c).
  • قراءة مفتاح غير موجود: dict[key] ترمي KeyNotFoundException. استخدم TryGetValue أو ContainsKey أولاً.
  • Add بمفتاح مكرر: dict.Add(key, value) ترمي ArgumentException إذا كان المفتاح موجوداً، بعكس dict[key] = value التي تستبدل.
  • السحب من مجموعة فارغة: Dequeue و Pop على مجموعة فارغة ترميان InvalidOperationException. افحص Count أو استخدم TryDequeue و TryPop.
  • التعديل أثناء foreach: إضافة عنصر أو حذفه من مجموعة أثناء المرور عليها يرمي InvalidOperationException.

سؤال

أضفنا إلى Queue العناصر A ثم B ثم C. ماذا تُرجع أول Dequeue؟

سؤال

ما الطريقة الآمنة لقراءة قيمة من قاموس قد لا يحتوي المفتاح؟

  • Dictionary: أزواج مفتاح وقيمة، والقراءة الآمنة بـ TryGetValue أو GetValueOrDefault.
  • HashSet: عناصر بلا تكرار، مع IntersectWith و UnionWith.
  • Queue: الأول يخرج أولاً. و Stack: الأخير يخرج أولاً.
  • OrderBy و GroupBy و Any و All و FirstOrDefault تعمل مع كل المجموعات.
  • مبروك! أنهيت كورس C#‎. جرّب الآن بناء برنامج صغير أو أول لعبة لك في Unity.