مقدمة: الخطأ الذي يبطئ كودك بشكل كارثي
\n\nكل من كتب كودًا بلغة C# وتعامل مع Dictionary أو HashSet، عاجلاً أم آجلاً، صادف هذا السؤال على Stack Overflow. آلاف المشاهدات وعشرات الإجابات، لكن الجوهر واحد: إذا قمت بإعادة تعريف Equals في الكلاس الخاص بك ولكن نسيت GetHashCode، فقد يعمل كودك بشكل خاطئ أو ببطء شديد. لماذا؟ دعنا نفهم ذلك من خلال أمثلة واقعية.
تخيل أنك تبني نظامًا لحجز تذاكر الطيران. لديك كلاس Flight برقم رحلة وتاريخ فريدين. تريد استخدام هذا الكلاس كمفتاح في Dictionary. يبدو الأمر بسيطًا. لكن بدون GetHashCode صحيح، قد يتحول القاموس إلى قائمة عادية بسرعة بحث O(n) بدلاً من O(1).
سحر جداول التجزئة: كيف يعمل Dictionary في C#
\n\nلفهم المشكلة، يجب أن ننظر تحت غطاء Dictionary. داخليًا، هو مصفوفة من "الدلاء" (buckets). عند إضافة عنصر، تستدعي C# دالة GetHashCode() للمفتاح لتحديد رقم الدلو. إذا كان الدلو يحتوي بالفعل على عناصر، يتم استدعاء Equals() للمقارنة مع كل منها. هذا الثنائي هو ما يضمن البحث السريع.
العقد الذي لا يمكن كسره
\nفي C#، هناك عقد غير مكتوب لكنه صارم:
\n- \n
- إذا أعادت
Equals()القيمةtrueلكائنين، فإن رموز التجزئة الخاصة بهما يجب أن تكون متساوية. \n - إذا كانت رموز التجزئة مختلفة، فلن يتم استدعاء
Equals()أبدًا (هذا تحسين للأداء). \n - العكس ليس ضروريًا: يمكن لكائنين مختلفين أن يكون لهما نفس رمز التجزئة (تصادم). \n
عند إعادة تعريف Equals فقط دون GetHashCode، فإنك ترث التنفيذ من System.Object، والذي يعتمد على المرجع في الذاكرة. كائنان "متساويان" حسب منطقك سيكون لهما رموز تجزئة مختلفة. وهنا تبدأ المشكلة.
مثال على المشكلة: رحلات ضائعة في القاموس
\n\nألق نظرة على هذا الكود. يبدو أنه يجب أن يعمل، لكنه لا يعمل:
\n\npublic class Flight\n{\n public string FlightNumber { get; }\n public DateTime Date { get; }\n\n public Flight(string flightNumber, DateTime date)\n {\n FlightNumber = flightNumber;\n Date = date;\n }\n\n public override bool Equals(object obj)\n {\n if (obj is Flight other)\n return FlightNumber == other.FlightNumber && Date == other.Date;\n return false;\n }\n \n // GetHashCode لم يتم إعادة تعريفه!\n}\n\n// الاستخدام\nvar flights = new Dictionary<Flight, string>();\nvar f1 = new Flight("SU100", new DateTime(2024, 12, 25));\nvar f2 = new Flight("SU100", new DateTime(2024, 12, 25));\n\nflights[f1] = "موسكو - باريس";\n\n// خطأ! لم يتم العثور على f2، على الرغم من أن f1.Equals(f2) == true\nConsole.WriteLine(flights.ContainsKey(f2)); // False\n\nلماذا؟ f1 و f2 كائنان مختلفان في الذاكرة. لهما رموز تجزئة مختلفة من Object.GetHashCode(). يضع القاموس f1 في الدلو رقم 123، ويبحث عن f2 في الدلو رقم 456. حتى Equals لا يتم استدعاؤها! النتيجة: لم يتم العثور على العنصر.
مشكلة الأداء: عندما يعمل كل شيء لكن ببطء
\n\nالأسوأ من ذلك هو الحل الجزئي. لنفترض أنك أعدت تعريف GetHashCode، لكنك فعلتها بشكل سيء، مثلاً بإرجاع قيمة ثابتة:
public override int GetHashCode() => 42; // كل الكائنات في دلو واحد\n\nشكليًا، تم الالتزام بالعقد (الكائنات المتساوية لها نفس رمز التجزئة). لكن الأداء سينهار: كل الكائنات ستذهب إلى دلو واحد، وسيتحول Dictionary إلى قائمة عادية. الإدراج والبحث سيصبحان O(n). بالنسبة لقاموس يحتوي على 10,000 عنصر، هذه كارثة.