Jaka jest różnica między IEqualityComparer <T> i IEquatable <T>?

151

Chcę zrozumieć scenariusze, w których IEqualityComparer<T>i IEquatable<T>należy je stosować. Dokumentacja MSDN dla obu wygląda bardzo podobnie.

Tilak
źródło
1
MSDN sez: „Ten interfejs umożliwia implementację niestandardowego porównania równości dla kolekcji ”, co oczywiście wynika z wybranej odpowiedzi. MSDN zaleca również dziedziczenie EqualityComparer<T>zamiast implementowania interfejsu ", ponieważ EqualityComparer<T>testuje równość przy użyciuIEquatable<T>
radarbob
... powyższe sugeruje, że powinienem stworzyć własną kolekcję dla każdego Twdrożenia IEquatable<T>. Czy w przeciwnym razie kolekcja List<T>miałaby jakiś subtelny błąd?
radarbob
dobre wyjaśnienie anotherchris.net/csharp/…
Ramil Shavaleev
@RamilShavaleev link jest uszkodzony
ChessMax

Odpowiedzi:

117

IEqualityComparer<T>jest interfejsem dla obiektu, który przeprowadza porównanie dwóch obiektów tego typu T.

IEquatable<T>dotyczy obiektu typu, Taby mógł porównać się z innym obiektem tego samego typu.

Justin Niessner
źródło
1
Ta sama różnica jest między IComparable / IComparer
boctulus
3
Kapitan oczywisty
Stepan Ivanenko
(Edytowano) IEqualityComparer<T>to interfejs dla obiektu (który jest zwykle lekką klasą różną od T), który zapewnia funkcje porównawcze, które działająT
rwong
61

Decydując się na użycie IEquatable<T>lub IEqualityComparer<T>, można zapytać:

Czy istnieje preferowany sposób testowania dwóch przypadków Trówności, czy też istnieje kilka równorzędnych sposobów?

  • Jeśli istnieje tylko jeden sposób testowania dwóch wystąpień Trówności lub jeśli preferowana jest jedna z kilku metod, IEquatable<T>byłby to właściwy wybór: ten interfejs powinien być implementowany tylko Tsamodzielnie, tak aby jedna instancja Tmiała wewnętrzną wiedzę o jak porównać się z innym wystąpieniem T.

  • Z drugiej strony, jeśli istnieje kilka równie rozsądnych metod porównywania dwóch Tdla równości, IEqualityComparer<T>wydaje się bardziej odpowiedni: Ten interfejs nie ma być implementowany Tsamodzielnie, ale przez inne „zewnętrzne” klasy. Dlatego podczas testowania dwóch instancji Trówności, ponieważ Tnie ma wewnętrznego zrozumienia równości, będziesz musiał dokonać wyraźnego wyboru IEqualityComparer<T>instancji, która przeprowadza test zgodnie z określonymi wymaganiami.

Przykład:

Rozważmy te dwa typy (które mają mieć semantykę wartości ):

interface IIntPoint : IEquatable<IIntPoint>
{
    int X { get; }
    int Y { get; }
}

interface IDoublePoint  // does not inherit IEquatable<IDoublePoint>; see below.
{
    double X { get; }
    double Y { get; }
}

Dlaczego tylko jeden z tych typów miałby dziedziczyć IEquatable<>, a drugi nie?

W teorii istnieje tylko jeden rozsądny sposób porównania dwóch wystąpień każdego typu: są one równe, jeśli właściwości Xi Yw obu przypadkach są równe. Zgodnie z tym myśleniem oba typy powinny wdrożyć IEquatable<>, ponieważ nie wydaje się prawdopodobne, że istnieją inne sensowne sposoby przeprowadzania testu równości.

Problem polega na tym, że porównywanie liczb zmiennoprzecinkowych pod kątem równości może nie działać zgodnie z oczekiwaniami z powodu drobnych błędów zaokrągleń . Istnieją różne metody porównywania liczb zmiennoprzecinkowych pod kątem niemal równości , z których każda ma określone zalety i kompromisy, i możesz chcieć samodzielnie wybrać, która metoda jest odpowiednia.

sealed class DoublePointNearEqualityComparerByTolerance : IEqualityComparer<IDoublePoint>
{
    public DoublePointNearEqualityComparerByTolerance(double tolerance) {  }
    
    public bool Equals(IDoublePoint a, IDoublePoint b)
    {
        return Math.Abs(a.X - b.X) <= tolerance  &&  Math.Abs(a.Y - b.Y) <= tolerance;
    }
    
}

Zauważ, że strona, do której odsyłam (powyżej), wyraźnie stwierdza, że ​​ten test bliskiej równości ma pewne słabości. Ponieważ jest to IEqualityComparer<T>implementacja, możesz ją po prostu zamienić, jeśli nie jest wystarczająco dobra do twoich celów.

stakx - już nie wnoszący wkładu
źródło
6
Sugerowana metoda porównawcza do testowania równości dwupunktowej jest zepsuta, ponieważ każda prawowita IEqualityComparer<T>implementacja musi implementować relację równoważności, co oznacza, że ​​we wszystkich przypadkach, gdy dwa obiekty są porównywane jako równe trzeciemu, muszą być porównywane równo ze sobą. Każda klasa, która wdraża IEquatable<T>lub IEqualityComparer<T>w sposób sprzeczny z powyższym, jest zepsuta.
supercat
5
Lepszym przykładem mogą być porównania między napisami. Sensowne jest posiadanie metody porównywania ciągów, która traktuje łańcuchy jako równe tylko wtedy, gdy zawierają one tę samą sekwencję bajtów, ale istnieją inne przydatne metody porównania, które również stanowią relacje równoważności , takie jak porównania bez rozróżniania wielkości liter. Zwróć uwagę, że a, IEqualityComparer<String>które uważa „hello” za równe zarówno „Hello”, jak i „hElLo”, musi traktować „Hello” i „hElLo” jako równe sobie, ale w przypadku większości metod porównawczych nie byłoby to problemem.
supercat,
@supercat, dzięki za cenne uwagi. Jest prawdopodobne, że w ciągu najbliższych kilku dni nie przejdę do poprawiania mojej odpowiedzi, więc jeśli chcesz, możesz edytować według własnego uznania.
stakx - nie publikuje już
To jest dokładnie to, czego potrzebowałem i co robię. Istnieje jednak potrzeba zastąpienia GetHashCode, w którym zwróci false, gdy wartości będą różne. Jak radzisz sobie z GetHashCode?
teapeng
@teapeng: Nie jestem pewien, o jakim konkretnym przypadku użycia mówisz. Ogólnie rzecz biorąc, GetHashCodepowinno umożliwić szybkie określenie, czy dwie wartości się różnią. Reguły są z grubsza następujące: (1) GetHashCode musi zawsze generować ten sam kod skrótu dla tej samej wartości. (2) GetHashCode powinno być szybkie (szybsze niż Equals). (3) GetHashCode nie musi być precyzyjne (nie tak dokładne jak Equals). Oznacza to, że może generować ten sam kod skrótu dla różnych wartości. Im dokładniej możesz to zrobić, tym lepiej, ale prawdopodobnie ważniejsze jest, aby to robić szybko.
stakx - nie publikuje już
27

Masz już podstawową definicję tego, czym one są . Krótko mówiąc, jeśli zaimplementujesz IEquatable<T>w klasie T, Equalsmetoda na obiekcie typu Tinformuje, czy sam obiekt (testowany pod kątem równości) jest równy innemu wystąpieniu tego samego typu T. Natomiast IEqualityComparer<T>służy do testowania równości dowolnych dwóch wystąpień T, zwykle poza zakresem wystąpień T.

Co do tego, do czego służą, może być początkowo mylące. Z definicji powinno być jasne, że stąd IEquatable<T>(zdefiniowana w Tsamej klasie ) powinna być de facto standardem reprezentacji unikalności jej obiektów / instancji. HashSet<T>, Dictionary<T, U>( GetHashCodebranie pod uwagę również jest nadpisywane), Containsna List<T>etc korzystają z tego. Wdrożenie IEqualityComparer<T>na Tnie pomaga powyższe ogólne przypadki. W związku z tym implementacja IEquatable<T>w innej klasie niż T. To:

class MyClass : IEquatable<T>

rzadko ma sens.

Z drugiej strony

class T : IEquatable<T>
{
    //override ==, !=, GetHashCode and non generic Equals as well

    public bool Equals(T other)
    {
        //....
    }
}

tak należy to zrobić.

IEqualityComparer<T>może być przydatne, gdy potrzebujesz niestandardowej weryfikacji równości, ale nie jako ogólna zasada. Na przykład, w Personpewnym momencie w klasie może być konieczne przetestowanie równości dwóch osób na podstawie ich wieku. W takim przypadku możesz:

class Person
{
    public int Age;
}

class AgeEqualityTester : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        return x.Age == y.Age;
    }

    public int GetHashCode(Person obj)
    {
        return obj.Age.GetHashCode;
    }
}

Aby je przetestować, spróbuj

var people = new Person[] { new Person { age = 23 } };
Person p = new Person() { age = 23 };

print people.Contains(p); //false;
print people.Contains(p, new AgeEqualityTester()); //true

Podobnie IEqualityComparer<T>na Tnie ma sensu.

class Person : IEqualityComparer<Person>

To prawda, że ​​to działa, ale nie wygląda dobrze dla oczu i podważa logikę.

Zwykle to, czego potrzebujesz IEquatable<T>. Idealnie byłoby, gdybyś miał tylko jeden, IEquatable<T>podczas gdy wiele IEqualityComparer<T>jest możliwych na podstawie różnych kryteriów.

IEqualityComparer<T>I IEquatable<T>są dokładnie analogiczne do Comparer<T>i IComparable<T>które są wykorzystywane do celów porównawczych zamiast zrównując; dobry wątek , w którym napisałem tę samą odpowiedź :)

nawfal
źródło
public int GetHashCode(Person obj)powinien wrócićobj.GetHashCode()
hyankov
@HristoYankov powinien wrócić obj.Age.GetHashCode. będzie edytować.
nawfal
Proszę wyjaśnić, dlaczego osoba porównująca musi przyjąć takie założenie co do wartości skrótu obiektu, który porównuje? Twoja funkcja porównująca nie wie, w jaki sposób Osoba oblicza swój skrót, dlaczego miałbyś to zastąpić?
hyankov
@HristoYankov Twoja osoba porównująca nie wie, w jaki sposób Osoba oblicza swój Hash - Jasne, dlatego person.GetHashCodenigdzie bezpośrednio nie zadzwoniliśmy … dlaczego miałbyś to zmienić? - Nadpisujemy, bo chodzi o IEqualityComparerto, żeby mieć inną implementację porównania według naszych reguł - reguł, które naprawdę dobrze znamy.
nawfal
W moim przykładzie muszę oprzeć moje porównanie na Agewłaściwości, więc dzwonię Age.GetHashCode. Wiek jest typu int, ale niezależnie od typu jest to kontrakt z kodem skrótu w .NET different objects can have equal hash but equal objects cant ever have different hashes. To kontrakt, w który ślepo wierzymy. Na pewno nasze niestandardowe porównywarki również powinny przestrzegać tej zasady. Jeśli kiedykolwiek dzwonienie someObject.GetHashCodepsuje rzeczy, to jest to problem z implementatorami typu someObject, to nie jest nasz problem.
nawfal
11

IEqualityComparer jest używany, gdy równość dwóch obiektów jest implementowana zewnętrznie, np. Jeśli chcesz zdefiniować funkcję porównującą dla dwóch typów, dla których nie masz źródła, lub w przypadkach, w których równość między dwiema rzeczami ma sens tylko w pewnym ograniczonym kontekście.

IEquatable służy do zaimplementowania samego obiektu (tego, który jest porównywany pod kątem równości).

Chris Shain
źródło
Jesteś jedyną osobą, która faktycznie poruszyła kwestię „podłączania równości” (użycie zewnętrzne). plus 1.
Royi Namir
4

Jeden porównuje dwa Ts. Drugi może porównać się do innych T. Zwykle wystarczy użyć tylko jednego na raz, a nie obu.

Matt Ball
źródło