2016-03-21 24 views
7

Mam projekt, w którym intensywnie używam generycznego C# dictionary. Potrzebuję kluczy złożonych, więc jako klucze używam tuples. W pewnym momencie zastanawiałem się, czy nie byłoby korzystne, aby użyć niestandardowej klasy, który buforuje kod skrótu:Dlaczego słownik C# nie wywołuje ukrytej metody GetHashCode

public class CompositeKey<T1, T2> : Tuple<T1, T2> 
{ 
    private readonly int _hashCode; 

    public CompositeKey(T1 t1, T2 t2) : base(t1, t2) 
    { 
     _hashCode = base.GetHashCode(); 
    } 

    public new int GetHashCode() 
    { 
     return _hashCode; 
    } 
} 

użyłem new zamiast override bo myślałem, że nie będzie mieć znaczenie dla tego testu, bo było określenie słownika przy użyciu betonu typ:

var dict = new Dictionary<CompositeKey<string, int>, int>(); 

zauważyłem jednak, że moja metoda zwyczaj GetHashCode nie nazywa się w ogóle. Po zmianie new na override został on wywołany zgodnie z oczekiwaniami.

Czy ktoś może wyjaśnić, dlaczego ukryta metoda GetHashCode nie jest wywoływana? Spodziewam się takiego zachowania, czy chciałbym zdefiniować słownika jak ten

var dict = new Dictionary<Tuple<string, int>, int>(); 

ale gdybym określić typ CompositeKey wyraźnie jak w moim przykładzie.

P.S. Wiem, że ukrywanie metody GetHashCode prawdopodobnie nie jest dobrym pomysłem.

+1

Ukrywanie jest złym pomysłem, ponieważ powoduje ten problem! – AndyJ

+2

Oto post wyjaśniający różnicę między nowymi i override ... http://stackoverflow.com/questions/1399127/difference-between-new-and-override – AndyJ

+0

Odkładając na bok, jeśli uważasz, że Twój algorytm hashcode musi zostać zbuforowany, to prawdopodobnie zły algorytm lub niepotrzebnie buforujesz. –

Odpowiedz

9

Może ktoś wyjaśnić, dlaczego ukryte metoda GetHashCode nie nazywa? Spodziewam się takiego zachowania, czy chciałbym zdefiniować słownika jak to

aby móc zadzwonić CompositeKey.GetHashCode metody, trzeba mieć odniesienie do instancji CompositeKey wpisany jako CompositeKey w czasie kompilacji.

Ale baza kodów Dictionary<TKey,TValue> nie jest świadoma twojej klasy CompositeKey (oczywiście). Wszystko, co wie, to TKey (ogólny typ parametru), który jest równoważny z System.Object bez żadnych ograniczeń. Ponieważ nie można wywoływać żadnych metod, które są zadeklarowane w System.Object bez ograniczenia.

Słownik kończy więc na wywoływaniu Object.GetHashCode, który nie jest nadpisany w klasie - i dlatego nie jest wywoływany.

3

Rozdzielczość przeciążeniowa dla wywołań metod w typach ogólnych ma miejsce, gdy skompilowany jest niezwiązany typ ogólny (na przykład Dictionary<TKey, TValue>), a nie, gdy konstruowany jest typ zamknięty (na przykład Dictionary<CompositeKey<string, int>, int>).

Ponieważ nie ma ograniczenia na TKey w Dictionary<,> jedynym przeciążenie GetHashCode() dostępny jest object.GetHashCode(). Konstruowanie typu, w którym występuje lepsze przeciążenie GetHashCode(), nie zmienia początkowej rozdzielczości przeciążania.

Nie ogranicza się tylko do metod ukrytych pod new. To samo dzieje się z przeciążonych metod:

class Generic<T> 
{ 
    public bool Equal(T t1, T t2) 
    { 
     return t1.Equals(t2); 
    } 
} 

class X : IEquatable<X> 
{ 
    public override bool Equals(object obj) 
    { 
     Console.WriteLine("object.Equals"); 
     return true; 
    } 

    public bool Equals(X other) 
    { 
     Console.WriteLine("IEquatable.Equals"); 
     return true; 
    } 
} 

Przeciążenie X.Equals(X) nigdy nie zostaną wykorzystane

var test = new Generic<X>(); 
test.Equal(new X(), new X()); 

// prints "object.Equals" 
+0

Dziękuję za twój przykład. Nie wiedziałem, że ograniczenia mają jakikolwiek inny efekt, z wyjątkiem sprawdzania typu, gdy próbujesz zbudować nową instancję typu ogólnego. – fknx

0

Dzieje się tak ze względu na ograniczenia typów generycznych. Oto uproszczony program pokazujący problem.

public class Program 
{ 
    public static void Main(string[] args) 
    { 
     var bar = new Bar(); 
     TestMethod(bar); 
     TestMethod2(bar); 
    } 

    public static void TestMethod<T>(T obj) where T : Foo 
    { 
     obj.Test(); 
     obj.Test2(); 
    } 

    public static void TestMethod2<T>(T obj) where T : Bar 
    { 
     obj.Test(); 
     obj.Test2(); 
    } 
} 

public class Foo 
{ 
    public virtual void Test() 
    { 
     Debugger.Break(); 
    } 

    public virtual void Test2() 
    { 
     Debugger.Break(); 
    } 
} 

public class Bar : Foo 
{ 
    public new void Test() 
    { 
     Debugger.Break(); 
    } 

    public override void Test2() 
    { 
     Debugger.Break(); 
    } 
} 

W TestMethod() trafisz przerwania w Foo.Test() i Bar.Test2() ale w TestMethod2() trafisz przerwania w Bar.Test() i Bar.Test2(), to dlatego, że w pierwszej metodzie jesteś zmuszony do pisania Foo lub niższy więc gdy kompilator kompiluje wiąże się z rozmowy na Foo, jest taka sama jak wtedy, gdy funkcja została zapisana jako

public static void TestMethod<T>(T obj) 
{ 
    ((Foo)obj).Test(); //You would expect this to call Foo.Test() b\c of shadowing 
    ((Foo)obj).Test2(); //You would expect this to call Bar.Test2() b\c of overloading 
} 

Teraz, do problemu, comparer który jest używany written as

[Serializable] 
internal class ObjectEqualityComparer<T>: EqualityComparer<T> 
{ 
    [Pure] 
    public override bool Equals(T x, T y) { 
     if (x != null) { 
      if (y != null) return x.Equals(y); 
      return false; 
     } 
     if (y != null) return false; 
     return true; 
    } 

    [Pure] 
    public override int GetHashCode(T obj) { 
     if (obj == null) return 0; 
     return obj.GetHashCode(); 
    } 
    //... 
} 

Nie ma ograniczenia na T więc te dwie metody zachowują się tak, jakby zostały napisane jako

public override bool Equals(T x, T y) { 
     if (x != null) { 
      if (y != null) return ((object)x).Equals(y); 
      return false; 
     } 
     if (y != null) return false; 
     return true; 
    } 

    [Pure] 
    public override int GetHashCode(T obj) { 
     if (obj == null) return 0; 
     return ((object)obj).GetHashCode(); 
    } 

Dlatego czynność nazywa się tylko wtedy, gdy zagłuszył ją, a nie po to zasłonięta .

1

Wystarczy opracować poprzednie odpowiedzi. Problem z nowością polega na tym, że TYLKO nadpisuje on metodę, gdy konsument bezpośrednio działa na klasie (w tym przypadku w klasie CompositeKey). Każde wywołanie dowolnej klasy bazowej, z której pochodzi Twój klucz CompositeKey, NIE wywoła twojego nowego członka.

Więc jeśli w poniższym:

  • CompositeKey.GetHashCode() < --- wywoła nową metodę.
  • Tuple.GetHashCode() < --- Czy nie zadzwoń do nowej metody.
  • Object.GetHashCode() < --- Czy nie zadzwoń do nowej metody.

Jak podkreśliły poprzednich odpowiedzi, ponieważ EqualityComparer (Dictionary klasa używa) określa, że ​​T jest nie ograniczony generycznych, wówczas kompilator będzie wspierać tylko najniższy wspólny mianownik dla wszystkich t, które mogą być przekazywane do to, czyli metody bezpośrednio w Object.

Dlatego połączenie jest skuteczne: (klawisz (Object)) .GetHashCode(). Z powyższego widać, że nie wywoła to twojej nowej metody.