Object.GetHashCode () için varsayılan uygulama


162

Varsayılan uygulama nasıl GetHashCode()çalışır? Ve yapıları, sınıfları, dizileri, vb. Verimli ve yeterince iyi idare ediyor mu?

Hangi durumlarda kendi paketlemem gerektiğine ve hangi durumlarda güvenli bir şekilde varsayılan uygulamalara güvenebileceğime karar vermeye çalışıyorum. Mümkünse tekerleği yeniden icat etmek istemiyorum.


Makalede bıraktığım yoruma bir göz atın: stackoverflow.com/questions/763731/gethashcode-extension-method
Paul Westcott


34
Kenara: yapabilecekleriniz elde (hatta varsayılan karma kodunun GetHashCode()kullanarak geçersiz kılınan olmuştur)System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj)
Marc Gravell

@MarcGravell buna katkıda bulunduğunuz için teşekkür ederim, tam olarak bu cevabı arıyordum.
Andrew Savinykh

@MarcGravell Ama bunu diğer yöntemlerle nasıl yapabilirim?
Tomáš Zato - Monica'yı iade et

Yanıtlar:


86
namespace System {
    public class Object {
        [MethodImpl(MethodImplOptions.InternalCall)]
        internal static extern int InternalGetHashCode(object obj);

        public virtual int GetHashCode() {
            return InternalGetHashCode(this);
        }
    }
}

InternalGetHashCode , CLR'de aşağıdaki gibi görünen bir ObjectNative :: GetHashCode işleviyle eşlenir :

FCIMPL1(INT32, ObjectNative::GetHashCode, Object* obj) {  
    CONTRACTL  
    {  
        THROWS;  
        DISABLED(GC_NOTRIGGER);  
        INJECT_FAULT(FCThrow(kOutOfMemoryException););  
        MODE_COOPERATIVE;  
        SO_TOLERANT;  
    }  
    CONTRACTL_END;  

    VALIDATEOBJECTREF(obj);  

    DWORD idx = 0;  

    if (obj == 0)  
        return 0;  

    OBJECTREF objRef(obj);  

    HELPER_METHOD_FRAME_BEGIN_RET_1(objRef);        // Set up a frame  

    idx = GetHashCodeEx(OBJECTREFToObject(objRef));  

    HELPER_METHOD_FRAME_END();  

    return idx;  
}  
FCIMPLEND

GetHashCodeEx'in tam uygulaması oldukça büyüktür, bu yüzden sadece C ++ kaynak koduna bağlantı vermek daha kolaydır .


5
Bu dökümantasyonun çok erken bir versiyonundan gelmiş olması gerekir. Muhtemelen oldukça yanlış olduğundan, şu anki MSDN makalelerinde bu şekilde yazılmamaktadır.
Hans Passant

4
İfadeyi değiştirdiler, evet, ama yine de temelde aynı şeyi söylüyor: "Sonuç olarak, bu yöntemin varsayılan uygulaması karma amaçlar için benzersiz bir nesne tanımlayıcısı olarak kullanılmamalıdır."
David Brown

7
Belgeler neden uygulamanın karma işlem için özellikle yararlı olmadığını iddia ediyor? Bir nesne kendisine eşit ve başka hiçbir şeye eşit değilse, belirli bir nesne örneği için her zaman aynı değeri döndürecek ve genellikle farklı örnekler için farklı değerler döndürecek herhangi bir karma kod yöntemi, sorun nedir?
supercat

3
@ ta.speot.is: İstediğiniz şey belirli bir örneğin önceden bir sözlüğe eklenmiş olup olmadığını belirlemekse , referans eşitliği mükemmeldir. Dizelerle, not ettiğiniz gibi, genellikle aynı karakter dizisini içeren bir dizenin zaten eklenmiş olup olmadığı daha fazla ilgilenir . Bu yüzden stringgeçersiz kılar GetHashCode. Öte yandan, çeşitli kontrollerin Paintolayları kaç kez işlediğini saymak istediğinizi varsayalım . Bir kullanabilirsiniz Dictionary<Object, int[]>( int[]depolanan her bir öğe bir öğeye sahip olabilir).
supercat

6
It'sNotALie @. Sonra bir
kopyamız

88

Bir sınıf için varsayılanlar esasen referans eşitliğidir ve bu genellikle iyidir. Bir yapı yazıyorsanız, eşitliği geçersiz kılmak daha yaygındır (en azından boksu önlemek için değil), ancak yine de bir yapı yazmanız çok nadirdir!

Eşitliği geçersiz kılarken, her zaman bir eşleştirme olmalıdır Equals()ve GetHashCode()(eğer yani iki değer için, Equals()gerçek döner onlar gerekir aynı hash-kodu döndürebilir, ancak tersi olduğunu değil gerekli) - ve aynı zamanda sağlamak için ortak ==/ !=operatörler ve genellikle için uygulamak IEquatable<T>.

Karma kodunu oluşturmak için, eşleştirilmiş değerlerde çarpışmaları önlediğinden, örneğin temel 2 alan karma için faktörlü bir toplam kullanmak yaygındır:

unchecked // disable overflow, for the unlikely possibility that you
{         // are compiling with overflow-checking enabled
    int hash = 27;
    hash = (13 * hash) + field1.GetHashCode();
    hash = (13 * hash) + field2.GetHashCode();
    return hash;
}

Bunun avantajı:

  • {1,2} 'nin karması {2,1}' nin karması ile aynı değil
  • {1,1} değerinin karması {2,2} öğesinin karması ile aynı değil

vb - sadece ağırlıksız bir toplam veya xor ( ^) vb.


Faktörlü toplam algoritmasının faydası hakkında mükemmel nokta; daha önce fark etmediğim bir şey!
Loophole

Faktörlü toplam (yukarıda yazıldığı gibi) zaman zaman taşma istisnalarına neden olmaz mı?
sinelaw

4
@sinelaw evet, yapılmalıdır unchecked. Neyse ki, uncheckedC # için varsayılan, ancak açık yapmak daha iyi olurdu; editör
Marc Gravell

7

ObjectGetHashCode yönteminin belgeleri , "bu yöntemin varsayılan uygulamasının karma amaçlar için benzersiz bir nesne tanımlayıcısı olarak kullanılmaması gerektiğini" belirtir. ve için bir ValueType diyor "sen türetilmiş türünün GetHashCode yöntemini çağırırsanız, dönüş değeri karma tablodaki bir anahtar olarak kullanıma uygun olması muhtemel değildir." .

Temel veri tipleri gibi byte, short, int, long, charve stringiyi bir GetHashCode yöntemi uygulamak. PointÖrneğin, bazı diğer sınıflar ve yapılar, GetHashCodeözel ihtiyaçlarınıza uygun olabilecek veya olmayabilecek bir yöntem uygular . Yeterince iyi olup olmadığını görmek için denemek zorundasınız.

Her sınıf veya yapıya ilişkin belgeler, varsayılan uygulamayı geçersiz kılıp kılmayacağını size söyleyebilir. Geçersiz kılmazsa kendi uygulamanızı kullanmalısınız. Kendiniz oluşturduğunuz GetHashCodeyöntemi veya yöntemi kullanmanız gereken herhangi bir sınıf veya yapı için, karma kodunu hesaplamak üzere uygun üyeleri kullanan kendi uygulamanızı yapmanız gerekir.


2
Kendi uygulamanızı rutin olarak eklemeniz gerektiğine katılmıyorum . Basitçe, sınıfların büyük çoğunluğu (özellikle) asla eşitlik açısından test edilmeyecektir - ya da nerede oldukları, dahili referans eşitliği iyi değildir. Bir yapı yazma (zaten nadir) durumunda daha yaygın ve doğru olur.
Marc Gravell

@ Marc Gravel: Tabii ki demek istediğim bu değildi. Son paragrafı ayarlayacağım. :)
Guffa

Temel veri türleri, en azından benim durumumda, iyi bir GetHashCode yöntemi uygulamaz. Örneğin, int için GetHashCode sayının kendisini döndürür: (123) .GetHashCode () 123 döndürür.
fdermishin

5
@ user502144 Ve bunun nesi var? Eşitlik konusunda yanlış pozitifler olmadan hesaplanması kolay mükemmel bir benzersiz tanımlayıcı ...
Richard Rast

@Richard Rast: Bir Hashtable içinde kullanıldığında anahtarlar kötü bir şekilde dağıtılabilmesi dışında sorun yoktur. Şu cevaba bir göz atın: stackoverflow.com/a/1388329/502144
fdermishin

5

Ben açıklıyor bir cevap bulamadık yana neden biz geçersiz kılmak gerekir GetHashCodeve Equalsözel yapılar için ve neden varsayılan uygulamadır "değil muhtemel bir karma tablodaki bir anahtar olarak kullanıma uygun olması için" ben bir bağlantı bırakacağım bu blog Bu sorunun nedenini gerçek bir örnekle açıklayan yazı .

Tüm yazıyı okumanızı tavsiye ederim, ama işte bir özet (vurgu ve açıklamalar eklendi).

Yapılar için varsayılan karmanın yavaş olması ve çok iyi olmaması nedeni:

CLR'nin tasarlanma şekli, tanımlanmış bir üyeye System.ValueTypeveya System.Enumtiplere [[]] bir boks tahsisine neden olabilir [...]

Bir karma işlevinin uygulayıcısı bir ikilemle karşı karşıyadır: karma işlevinin iyi bir dağılımını yapın veya hızlı hale getirin. Bazı durumlarda, bu ikisini de elde etmek mümkündür, ama öyle jenerik bunu yapmak zor içinde ValueType.GetHashCode.

Bir yapının standart karma işlevi, tüm alanların karma kodlarını "birleştirir". Ancak bir ValueTypeyöntemde bir alanın karma kodunu almanın tek yolu yansıma kullanmaktır . Bu nedenle, CLR yazarları dağıtım hızı üzerinden ticaret yapmaya karar verdiler ve varsayılan GetHashCodesürüm sadece bir ilk boş olmayan alanın karma kodunu döndürür ve bunu bir tür id ile "munges" döndürür [...] . Örneğin, yeterince şanssızsanız ve yapınızın ilk alanı çoğu örnek için aynı değere sahipse, bir karma işlevi her zaman aynı sonucu sağlayacaktır . Tahmin edebileceğiniz gibi, bu örnekler bir karma kümesinde veya bir karma tablosunda saklanırsa, bu ciddi bir performans etkisine neden olacaktır.

[...] Yansıtma tabanlı uygulama yavaş . Çok yavaş.

[...] Her iki ValueType.Equalsve ValueType.GetHashCodeözel optimizasyon var. Bir tür "işaretçiler" içermiyorsa ve uygun şekilde paketlenmişse [...] daha uygun sürümler kullanılır: GetHashCodebir örnek üzerinden yinelenir ve 4 baytlık XORs blokları ve Equalsyöntem kullanılarak iki örneği karşılaştırır memcmp. [...] Ama optimizasyon çok zor. Birincisi, optimizasyonun ne zaman etkinleştirildiğini bilmek zordur [...] İkincisi, bir bellek karşılaştırması size doğru sonuçları vermez . İşte basit bir örnek: [...] -0.0ve +0.0eşittir ancak farklı ikili gösterimlere sahiptirler.

Postada açıklanan gerçek dünya sorunu:

private readonly HashSet<(ErrorLocation, int)> _locationsWithHitCount;
readonly struct ErrorLocation
{
    // Empty almost all the time
    public string OptionalDescription { get; }
    public string Path { get; }
    public int Position { get; }
}

Varsayılan eşitlik uygulamasına sahip özel bir yapı içeren bir demet kullandık. Ne yazık ki, yapı neredeyse her zaman [boş dize] 'ye eşit olan isteğe bağlı bir ilk alana sahipti . Setteki öğelerin sayısı önemli ölçüde artıncaya kadar performans iyiydi, bu da on binlerce öğeden oluşan bir koleksiyonu başlatmak için dakikalar alarak gerçek bir performans sorununa neden oldu.

Bu nedenle, "hangi durumlarda kendimi paketlemeliyim ve hangi durumlarda varsayılan uygulamaya güvenebilirim" sorusunu yanıtlamak için, en azından yapılar söz konusu olduğunda, geçersiz kılmalısınızEquals ve GetHashCodene zaman özel yapınız bir hash tablosundaki anahtar veya Dictionary.
Ayrıca IEquatable<T>boksu önlemek için bu durumda uygulamayı tavsiye ederim .

Diğer cevapların söylediği gibi, bir sınıf yazıyorsanız , referans eşitliğini kullanan varsayılan karma genellikle iyidir, bu yüzden geçersiz kılmanıza gerek kalmadıkça bu durumda rahatsız etmem Equals(o zaman buna GetHashCodegöre geçersiz kılmanız gerekir).


1

Genel olarak, Eşittirleri geçersiz kılarsanız GetHashCode'u geçersiz kılmak istersiniz. Bunun nedeni, her ikisinin de sınıfınızın / yapınızın eşitliğini karşılaştırmak için kullanılmasıdır.

Foo A, B kontrol edilirken eşittir;

eğer (A == B)

İşaretçinin eşleşmeyeceğini bildiğimiz için dahili üyeleri karşılaştırabiliriz.

Equals(obj o)
{
    if (o == null) return false;
    MyType Foo = o as MyType;
    if (Foo == null) return false;
    if (Foo.Prop1 != this.Prop1) return false;

    return Foo.Prop2 == this.Prop2;
}

GetHashCode genellikle karma tablolar tarafından kullanılır. Sınıfın verdiği hashcode, bir sınıf verme durumu için her zaman aynı olmalıdır.

Genelde yaparım,

GetHashCode()
{
    int HashCode = this.GetType().ToString().GetHashCode();
    HashCode ^= this.Prop1.GetHashCode();
    etc.

    return HashCode;
}

Bazıları, karma kodun nesne ömrü başına yalnızca bir kez hesaplanması gerektiğini söyleyecektir, ancak buna katılmıyorum (ve muhtemelen yanılıyorum).

Sınıflarınızdan birine aynı referansa sahip değilseniz, nesne tarafından sağlanan varsayılan uygulamayı kullanarak, bunlar birbirine eşit olmayacaktır. Eşittir ve GetHashCode'u geçersiz kılarak, eşitliği nesnelerin başvurusu yerine iç değerlere göre bildirebilirsiniz.


2
^ = Yaklaşımı, bir karma oluşturmak için özellikle iyi bir yaklaşım değildir - çok sayıda ortak / öngörülebilir çarpışmaya yol açar - örneğin Prop1 = Prop2 = 3 ise
Marc Gravell

Değerler aynı ise, nesneler eşit olduğundan çarpışmada bir sorun görmüyorum. 13 * Hash + NewHash yine de ilginç görünüyor.
Bennett Dill

2
Ben: Obj1 {Prop1 = 12, Prop2 = 12} ve Obj2 {Prop1 = 13, Prop2 = 13} için deneyin
Tomáš Kafka

0

Sadece POCO'larla uğraşıyorsanız, bu yardımcı programı kullanarak hayatınızı biraz basitleştirebilirsiniz:

var hash = HashCodeUtil.GetHashCode(
           poco.Field1,
           poco.Field2,
           ...,
           poco.FieldN);

...

public static class HashCodeUtil
{
    public static int GetHashCode(params object[] objects)
    {
        int hash = 13;

        foreach (var obj in objects)
        {
            hash = (hash * 7) + (!ReferenceEquals(null, obj) ? obj.GetHashCode() : 0);
        }

        return hash;
    }
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.