c# - شرح - لماذا من المهم تجاوز GetHashCode عند تجاوز طريقة Equals؟




شرح delegate c# (8)

نظرا للفئة التالية

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Which is preferred?

        return base.GetHashCode();

        //return this.FooId.GetHashCode();
    }
}

لقد Equals طريقة Equals لأن Foo يمثل صفًا لجدول Foo . ما هي الطريقة المفضلة GetHashCode ؟

لماذا من المهم تجاوز GetHashCode ؟


أدناه باستخدام التفكير تبدو لي خيار أفضل النظر في الخصائص العامة كما هو الحال مع هذا لا داعي للقلق حول إضافة / إزالة خصائص (على الرغم من عدم سيناريو شائع جدا). هذا وجد أن أداء أفضل أيضا. (مقارنة الوقت باستخدام Diagonistics التوقف ساعة).

    public int getHashCode()
    {
        PropertyInfo[] theProperties = this.GetType().GetProperties();
        int hash = 31;
        foreach (PropertyInfo info in theProperties)
        {
            if (info != null)
            {
                var value = info.GetValue(this,null);
                if(value != null)
                unchecked
                {
                    hash = 29 * hash ^ value.GetHashCode();
                }
            }
        }
        return hash;  
    }

أفهم أن GetHashCode () الأصلية ترجع عنوان الذاكرة الخاص بالكائن ، لذا من الضروري تجاوزه إذا كنت ترغب في المقارنة بين كائنين مختلفين.

EDITED: التي كانت غير صحيحة ، لا يمكن لأسلوب GetHashCode () الأصلي ضمان المساواة لقيمتين. على الرغم من أن الكائنات المتساوية ترجع نفس كود التجزئة.


انها ليست بالضرورة مهمة. يعتمد ذلك على حجم مجموعاتك ومتطلبات أدائك وما إذا كان سيتم استخدام صفك في مكتبة حيث قد لا تعرف متطلبات الأداء. أعرف بشكل متكرر أن أحجام مجموعاتي ليست كبيرة جدًا ووقتي أكثر قيمة من بضع ميكرو ثانية من الأداء المكتسب عن طريق إنشاء رمز تجزئة مثالي ؛ لذلك (للتخلص من التحذير المزعج من المترجم) أستخدم ببساطة:

   public override int GetHashCode()
   {
      return base.GetHashCode();
   }

(بالطبع يمكنني استخدام # pragma لإيقاف التحذير أيضًا لكنني أفضل هذه الطريقة.)

عندما تكون في موقف أنك بحاجة إلى الأداء من جميع المشاكل المذكورة من قبل الآخرين هنا ، طبعا. الأكثر أهمية - وإلا ستحصل على نتائج خاطئة عند استرداد عناصر من مجموعة تجزئة أو قاموس: يجب ألا يتغير رمز التجزئة باختلاف مدة حياة كائن ما (بشكل أكثر دقة ، خلال الوقت الذي تكون فيه شفرة هاش مطلوبة ، مثل أثناء الوجود مفتاح في القاموس): على سبيل المثال ، الخطأ غير صحيح لأن القيمة عامة وبالتالي يمكن تغييره خارجياً إلى الفصل الدراسي خلال فترة حياة المستخدم ، لذلك يجب عدم استخدامه كأساس لشفرة التجزئة:


   class A
   {
      public int Value;

      public override int GetHashCode()
      {
         return Value.GetHashCode(); //WRONG! Value is not constant during the instance's life time
      }
   }    

من ناحية أخرى ، إذا كانت القيمة لا يمكن تغييرها ، يمكنك استخدام:


   class A
   {
      public readonly int Value;

      public override int GetHashCode()
      {
         return Value.GetHashCode(); //OK  Value is read-only and can't be changed during the instance's life time
      }
   }


بتجاوزك لـ Equals فإنك تفيد بشكل أساسي أنك أنت الشخص الذي يعرف بشكل أفضل كيفية مقارنة حالتين من نوع معين ، لذلك من المحتمل أن تكون أفضل مرشح لتقديم أفضل رمز تجزئة.

هذا مثال لكيفية كتابة ReSharper دالة GetHashCode () لك:

public override int GetHashCode()
{
    unchecked
    {
        var result = 0;
        result = (result * 397) ^ m_someVar1;
        result = (result * 397) ^ m_someVar2;
        result = (result * 397) ^ m_someVar3;
        result = (result * 397) ^ m_someVar4;
        return result;
    }
}

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


فقط لإضافة الإجابات المذكورة أعلاه:

إذا لم تقم بتجاوز Equals فإن السلوك الافتراضي هو أنه تتم مقارنة مراجع الكائنات. وينطبق الشيء نفسه على رمز hashcode - ويعتمد التقصير الافتراضي عادةً على عنوان ذاكرة للمرجع. لأنك تجاوزت Equals ، فهذا يعني أن السلوك الصحيح هو مقارنة كل ما قمت بتطبيقه على Equals وليس بالمراجع ، لذلك يجب عليك أن تفعل الشيء نفسه بالنسبة لـ hashcode.

يتوقع عملاء الفصل الدراسي أن يكون لدى hashcode منطق مشابه لطريقة equals ، على سبيل المثال أساليب linq التي تستخدم IEqualityComparer أولاً مقارنة علامات التجزئة ، وفقط إذا كانت متساوية ستقارن طريقة Equals () التي قد تكون أكثر تكلفة للتشغيل ، إذا لم نقم بتطبيق hashcode ، فسيكون للشيء المتساوي على الأرجح hashcodes مختلفة (لأن لديهم عنوان ذاكرة مختلف) وسيتم تحديده خطأ على أنه غير متساوٍ (لن يصل Equals () حتى).

بالإضافة إلى ذلك ، باستثناء المشكلة التي قد لا تتمكن من العثور على الكائن الخاص بك إذا كنت قد استخدمته في أحد القواميس (لأنه تم إدراجه بواسطة رمز واحد ، وعندما تبحث عنه ، فمن المحتمل أن يكون كود التجزئة الافتراضي مختلفًا ومرة ​​أخرى يساوي ()) لن يتم استدعاؤك حتى ، كما يوضح مارك غرافيل في إجابته ، أنك تقدم أيضًا انتهاكًا للقاموس أو مفهوم hashset الذي لا ينبغي أن يسمح بمفاتيح مماثلة - لقد سبق لك أن أعلنت أن هذه الأشياء هي نفسها في الأساس عند تجاوزك لـ Equals حتى لا ترفض لا تريد كلاهما كمفاتيح مختلفة في بنية البيانات التي تفترض أن يكون لها مفتاح فريد ، ولكن لأن لديهم رمزًا مختلفًا ، فسيتم إدخال المفتاح "نفسه" على أنه مفتاح مختلف.


كيف حول:

public override int GetHashCode()
{
    return string.Format("{0}_{1}_{2}", prop1, prop2, prop3).GetHashCode();
}

افتراض أن الأداء ليس مشكلة :)


من الصعب جداً تطبيق GetHashCode() بشكل صحيح لأنه ، بالإضافة إلى القواعد التي ذكرها مارك مسبقاً ، لا يجب تغيير رمز التجزئة خلال عمر الكائن. لذلك يجب أن تكون الحقول المستخدمة لحساب شفرة التجزئة غير قابلة للتغيير.

وأخيرا وجدت حلا لهذه المشكلة عندما كنت أعمل مع NHibernate. طريقي هو حساب رمز التجزئة من معرف الكائن. يمكن تعيين المعرّف فقط من خلال المُنشئ حتى إذا كنت تريد تغيير المعرّف ، وهو أمر بعيد الاحتمال ، يجب عليك إنشاء كائن جديد يحتوي على معرف جديد وبالتالي رمز تجزئة جديد. يعمل هذا الأسلوب بشكل أفضل مع المعرفات الفريدة العمومية (GUIDs) لأنه يمكنك توفير منشئ بدون معلمات ينشئ معرفًا بشكل عشوائي.


نعم ، من المهم أن يتم استخدام العنصر الخاص بك كمفتاح في القاموس ، أو HashSet<T> ، إلخ - حيث يتم استخدام هذا (في حالة عدم وجود مخصص IEqualityComparer<T> ) لتجميع العناصر في مجموعات. إذا كان رمز التجزئة لعنصرين غير متطابقين ، فقد لا يتم اعتبارهما متساويين مطلقًا (لن يطلق الاسم على المساواة).

يجب أن تعكس أسلوب GetHashCode() منطق Equals ؛ القواعد هي:

  • إذا تساوت شيئين ( Equals(...) == true ) فعليهم إعادة نفس القيمة لـ GetHashCode()
  • إذا كان GetHashCode() يساوي ، ليس من الضروري أن تكون هي نفسها؛ هذا هو الاصطدام ، وسيتم استدعاء Equals لمعرفة ما إذا كانت مساواة حقيقية أم لا.

في هذه الحالة ، يبدو أن " return FooId; " هو تطبيق GetHashCode() مناسب. إذا كنت تختبر خصائص متعددة ، فمن الشائع دمجها باستخدام الكود مثل أدناه ، لتقليل الاصطدامات القطرية (أي بحيث تحتوي new Foo(3,5) على كود هاش مختلف على new Foo(5,3) ):

int hash = 13;
hash = (hash * 7) + field1.GetHashCode();
hash = (hash * 7) + field2.GetHashCode();
...
return hash;

أوه - للراحة ، قد تفكر أيضًا في توفير == و != المشغلين عند تجاوز Equals و GetHashCode .

عرض ما يحدث عندما تحصل على هذا الخطأ here .





hashcode