2015-02-10 19 views
20

Jestem świadomy faktu, że zawsze muszę zastąpić Equals(object) i GetHashCode() podczas implementacji IEquatable<T>.Equals(T).Dlaczego Equals (obiekt) wygrywa powyżej Equals (T) podczas korzystania z odziedziczonego obiektu w Hashset lub innych kolekcjach?

Jednak nie rozumiem, dlaczego w niektórych sytuacjach Equals(object) wygrywa z generycznym Equals(T).

Na przykład, dlaczego następujące czynności się zdarzają? Jeśli zadeklaruję IEquatable<T> dla interfejsu i zaimplementuję dla niego konkretny typ X, ogólne Equals(object) jest wywoływane przez Hashset<X> podczas porównywania elementów tego typu względem siebie. We wszystkich innych sytuacjach, w których co najmniej jedna ze stron jest rzutowana na interfejs, wywoływana jest poprawna Equals(T).

Oto przykładowy kod do wykazania:

public interface IPerson : IEquatable<IPerson> { } 

//Simple example implementation of Equals (returns always true) 
class Person : IPerson 
{ 
    public bool Equals(IPerson other) 
    { 
     return true; 
    } 

    public override bool Equals(object obj) 
    { 
     return true; 
    } 

    public override int GetHashCode() 
    { 
     return 0; 
    } 
} 

private static void doEqualityCompares() 
{ 
    var t1 = new Person(); 

    var hst = new HashSet<Person>(); 
    var hsi = new HashSet<IPerson>(); 

    hst.Add(t1); 
    hsi.Add(t1); 

    //Direct comparison 
    t1.Equals(t1);     //IEquatable<T>.Equals(T) 

    hst.Contains(t1);    //Equals(object) --> why? both sides inherit of IPerson... 
    hst.Contains((IPerson)t1);  //IEquatable<T>.Equals(T) 

    hsi.Contains(t1);    //IEquatable<T>.Equals(T) 
    hsi.Contains((IPerson)t1);  //IEquatable<T>.Equals(T) 
} 
+1

nie powinien "hst.Contains ((IPerson) t1);" nie kompilować? –

+0

@Dennis_E Zobacz drugą połowę mojej odpowiedzi. – Servy

+1

@Dennis_E: to nie jest 'HashSet .Contains', ale' Enumerable.Contains', który akceptuje interfejs. Tak więc jest to powolna wersja, która wylicza wszystkie elementy i porównuje je z 'Equals (IPerson other)' zamiast używać 'GetHashCode'. –

Odpowiedz

21

HashSet<T> rozmowy EqualityComparer<T>.Default uzyskać domyślny porównywarka równości gdy nie comparer jest świadczona.

EqualityComparer<T>.Default określa, czy T implementuje IEquatable<T>. Jeśli tak, używa tego, jeśli nie, używa object.Equals i object.GetHashCode.

Twój obiekt Person implementuje IEquatable<IPerson> nie IEquatable<Person>.

Kiedy masz HashSet<Person> kończy się sprawdzenie czy Person jest IEquatable<Person>, który jej nie, więc używa metody object.

Kiedy masz HashSet<IPerson> sprawdza, czy IPerson jest IEquatable<IPerson>, który to jest, więc używa tych metod.


chodzi o pozostałe sprawy, dlaczego ten wiersz:

hst.Contains((IPerson)t1); 

Wywołać IEquatableEquals metoda choć jego wezwał HashSet<Person>. Tutaj dzwonisz pod numer Contains na HashSet<Person> i przechodzisz w IPerson. HashSet<Person>.Contains wymaga parametru jako Person; IPerson nie jest prawidłowym argumentem. Jednakże, HashSet<Person> jest również IEnumerable<Person>, a ponieważ jest kowariantem, to znaczy, że może być traktowany jako IEnumerable<IPerson>, który ma sposób wydłużenia Contains (przez LINQ), który akceptuje IPerson jako parametr.

IEnumerable.Contains również używa EqualityComparer<T>.Default, aby uzyskać porównywarkę równości, gdy żadna nie została podana. W przypadku wywołania tej metody w rzeczywistości wywołujemy Contains na IEnumerable<IPerson>, co oznacza, że ​​EqualityComparer<IPerson>.Default sprawdza, czy IPerson jest IEquatable<IPerson>, którym jest, tak że wywoływana jest metoda Equals.

+0

Rozumiem - więc 'EqualityComparer .Default' może w rzeczywistości otrzymać tylko metodę" Equals (T) ", jeśli jest ona dokładnie typu T - nie byłaby w stanie uzyskać jej typu T ', który odziedziczył T. Myślałem, że w grze jest polimorfizm, ale oczywiście całkowicie się myliłem. Dziękuję za wyjaśnienie. – Marwie

+0

Należy zauważyć, że wybrali * nie *, aby uczynić 'IEquatable ' contravariant w swoim typie argumentu 'T'. Gdyby zrobili "IEquatable <>" contravariant (byłoby "IEquatable "), coś, co było "IEquatable ", również byłoby "IEquatable ". Jednak myślę, że uczynienie tego contravariant doprowadziłoby do jeszcze gorszych problemów (niż to, co już mamy), gdy ludzie wywodzą się z klas, które implementują 'IEquatable <>. *** Edycja: *** Ah, widzę, że supercat już wspomniał o tym w innej odpowiedzi. –

2

Chociaż IComparable<in T> jest kontrawariantny względem T, tak że każdy rodzaj który realizuje IComparable<Person> będą automatycznie klasyfikowane implementację IComparable<IPerson>, typ IEquatable<T> jest przeznaczony do stosowania w zamkniętych typów, zwłaszcza struktur. Wymaganie, że Object.GetHashCode() jest zgodne zarówno z IEquatable<T>.Equals(T) jak i, ogólnie oznacza, że ​​te ostatnie dwie metody powinny zachowywać się identycznie, co z kolei oznacza, że ​​jeden z nich powinien łączyć się z drugim. Chociaż istnieje duża różnica w wydajności między przekazywaniem struktury bezpośrednio do implementacji odpowiedniego typu w porównaniu z budową instancji typu boxed-strop-object obiektu, a implementacja Equals(Object) kopiuje z tego dane struktury, nie ma takich wydajność różni się w zależności od typów referencji. Jeśli IEquatable<T>.Equals(T) i Equals(Object) będą równoważne i T jest dziedziczna typu odniesienia, nie ma znaczących różnic między:

bool Equals(MyType obj) 
{ 
    MyType other = obj as MyType; 
    if (other==null || other.GetType() != typeof(this)) 
    return false; 
    ... test whether other matches this 
} 

bool Equals(MyType other) 
{ 
    if (other==null || other.GetType() != typeof(this)) 
    return false; 
    ... test whether other matches this 
} 

Ten ostatni może uratować jedną typecast, ale to jest mało prawdopodobne, aby wystarczającą różnicę wydajności uzasadniają dwa metody.