HashSet<T> مجموعة يظهر فيها كل عنصر مرة واحدة على الأكثر. لا فهرس لها ولا ترتيب مضمون، وفي المقابل تفحص العضوية في زمن ثابت تقريبًا: العثور على وسم واحد بين مليون يستغرق نحو ما يستغرقه العثور عليه بين عشرة.
إضافة العناصر: Add تعيد bool
المخرجات:
True
False
3
True
True
False
2
إضافة عنصر مكرّر ليست خطأ؛ تعيد Add ببساطة false وتبقى المجموعة دون تغيير. هذه القيمة المعادة أنفع ما في الدالة. تجمع بين "هل رأيت هذا؟" و"تذكّره" في استدعاء واحد:
المخرجات:
Duplicate: ana@x.com
Duplicate: ben@x.com
3 unique
لماذا Contains سريعة
تقارن List<T>.Contains القيمة بكل عنصر بدوره، فتنمو كلفتها مع القائمة. أما HashSet<T> فتحسب رمز تجزئة العنصر، وتقفز إلى الدلو الخاص بذلك الرمز، ولا تقارن إلا العناصر القليلة المخزّنة هناك. لفحص عضوية داخل حلقة يحوّل ذلك خطوة O(n) إلى خطوة O(1)، وحلقة متداخلة على قائمتين إلى مرور واحد:
// Slow on large inputs: Contains scans bannedList for every order.
var flagged = orders.Where(o => bannedList.Contains(o.CustomerId));
// Fast: build the set once, then each lookup is constant time.
var banned = new HashSet<int>(bannedList);
var flagged2 = orders.Where(o => banned.Contains(o.CustomerId));
بناء المجموعة يكلّف مرورًا واحدًا على القائمة، فلا يجدي إلا حين تبحث أكثر من بضع مرات.
إزالة التكرارات من قائمة
هناك ثلاث طرق شائعة، وتختلف فيما يحدث للترتيب:
المخرجات:
Lima, Oslo, Pune, Kyiv
4
Lima, Oslo, Pune, Kyiv
Distinct هي الافتراضي الصحيح حين تريد قائمة في النتيجة. وتعمل النسخة التي تعمل في المكان لأن RemoveAll تستدعي الشرط مرة لكل عنصر بالترتيب: تعيد seen.Add القيمة false للنسخة الثانية وما بعدها، فتُحذف تلك بالضبط.
عمليات المجموعات: الاتحاد والتقاطع والفرق
في HashSet<T> عمليات نظرية المجموعات. دوال ...With تغيّر المجموعة التي تُستدعى عليها ولا تعيد شيئًا.
المخرجات:
Union: Ana, Ben, Chloe, Dev
Intersect: Ben, Chloe
Except: Ana
Symmetric: Ana, Dev
True
True
False
True
يمكن أن يكون الوسيط أي IEnumerable<T>: مصفوفة أو قائمة أو مجموعة أخرى. تتجاهل SetEquals الترتيب والتكرارات في الوسيط. وترتّب الدالة المساعدة Show قبل الطباعة لأن ترتيب تعداد المجموعة ليس شيئًا يُعتمد عليه.
في LINQ دوال مقابلة تعيد تسلسلًا جديدًا وتترك المدخلات كما هي: monday.Union(tuesday) وmonday.Intersect(tuesday) وmonday.Except(tuesday). استخدمها حين لا تريد تغيير مجموعة، أو حين تكون المدخلات قوائم.
المساواة المخصصة لأصنافك
تقرّر المجموعة "العنصر نفسه" بـ GetHashCode وEquals. في الصنف الذي لا يتجاوزهما تقوم الدالتان على هوية الكائن، فيكون كائنان بحقول متساوية عنصرين مختلفين:
المخرجات:
2
1
True
القاعدة: الكائنات المتساوية بـ Equals يجب أن تعيد GetHashCode نفسه. تجاوز Equals وحدها فتبحث المجموعة في الدلو الخطأ وتظل تبلّغ عن التكرارات. في .NET Core 2.1 وما بعده تبني HashCode.Combine(X, Y) رمز تجزئة جيدًا دون الحساب المكتوب يدويًا.
حين لا تستطيع تغيير الصنف، أو تحتاج إلى مفهوم مختلف لـ "الشيء نفسه" في مجموعة واحدة، مرّر IEqualityComparer<T> إلى المُنشئ. وللنصوص أدوات مقارنة جاهزة:
المخرجات:
True
False
2
في C# 9 وما بعده يولّد record الدالتين Equals وGetHashCode بالقيمة عنك، فيعمل record Point(int X, int Y); في مجموعة دون شيفرة إضافية.
لا تغيّر أبدًا حقلًا يدخل في GetHashCode بينما الكائن في مجموعة. يبقى الكائن في دلو تجزئته القديمة، فتتوقف Contains وRemove عن إيجاده.
الترتيب وSortedSet
يُعدَّد HashSet<T> بترتيب يجب أن تعامله على أنه اعتباطي. إن احتجت إلى العناصر مرتّبة فرتّب عند الطباعة (set.OrderBy(x => x)) أو استخدم SortedSet<T>، الذي يبقي العناصر مرتّبة دائمًا ويضيف Min وMax واستعلامات النطاق بكلفة O(log n) لكل عملية:
var ranks = new SortedSet<int> { 30, 10, 20 };
Console.WriteLine(string.Join(", ", ranks)); // 10, 20, 30
Console.WriteLine(ranks.Min); // 10
HashSet مقابل List مقابل Dictionary
| الحاجة | الاستخدام |
|---|---|
| عناصر فريدة، و"هل هو موجود؟" سريعة | HashSet<T> |
| عناصر فريدة، مرتّبة دائمًا | SortedSet<T> |
| ترتيب، تكرار، وصول بالفهرس | List<T> |
| قيمة مخزّنة تحت كل مفتاح | Dictionary<TKey, TValue> |
المجموعة قاموس بمفاتيح بلا قيم. إن وجدت نفسك تكتب Dictionary<string, bool> فقط لتتبّع العضوية، فإن HashSet<string> تقول الشيء نفسه بوضوح أكبر.
أخطاء شائعة
- توقّع ترتيب. لا ترتيب يُعتمد عليه في المجموعة؛ رتّب، أو استخدم
SortedSet<T>. - أصناف مخصصة بلا
EqualsوGetHashCode. تصبح الكائنات المتشابهة عناصر منفصلة. - تجاوز
Equalsوحدها. تجاوزGetHashCodeمعها دائمًا. - تغيير عنصر بعد إضافته. لا تعود المجموعة قادرة على إيجاده.
- فهرسة مجموعة. لا تُترجم
set[0]؛ لا فهرس. حوّل بـToList()إن احتجت إلى مواضع.
الأسئلة الشائعة
ما هو HashSet في C#؟
HashSet<T> مجموعة من العناصر الفريدة بلا ترتيب محدد. إضافة عنصر موجود أصلًا لا تفعل شيئًا، وتجيب Contains في زمن ثابت تقريبًا مهما كبرت المجموعة، لأن العناصر تُخزَّن برمز التجزئة مثل مفاتيح Dictionary.
ماذا تعيد HashSet.Add؟
تعيد Add القيمة true حين يُضاف العنصر وfalse حين يكون موجودًا أصلًا في المجموعة. هذا يجعل if (!seen.Add(x)) فحص تكرار في سطر واحد: تضيف العناصر الجديدة وتخبرك بالمكرّرة في الاستدعاء نفسه.
كيف أزيل التكرارات من List في C#؟
تعيد list.Distinct().ToList() قائمة جديدة بلا تكرارات وتبقي أول ظهور لكل عنصر بترتيبه الأصلي. وتزيل new HashSet<T>(list) التكرارات أيضًا، لكن المجموعة بلا ترتيب مضمون. ولإزالة التكرار في المكان استخدم var seen = new HashSet<T>(); list.RemoveAll(x => !seen.Add(x));.
متى أستخدم HashSet بدل List؟
استخدم HashSet<T> حين تسأل غالبًا "هل هذا العنصر في المجموعة؟" أو تحتاج إلى أن تكون العناصر فريدة. تمرّ List<T>.Contains على كل عنصر، فتبطؤ كلما كبرت القائمة، بينما لا تبطؤ HashSet<T>.Contains. واستخدم List<T> حين يهمّ الترتيب أو التكرار أو الوصول بالفهرس.
لماذا يحتوي HashSet عندي على كائنات مكرّرة؟
لا يتجاوز صنفك Equals وGetHashCode، فتقارن المجموعة المراجع ويُحسب كائنان بقيم الحقول نفسها مختلفين. تجاوز الدالتين (معًا دائمًا)، أو مرّر IEqualityComparer<T> إلى مُنشئ المجموعة.