Chcę zrozumieć scenariusze, w których IEqualityComparer<T>
i IEquatable<T>
należy je stosować. Dokumentacja MSDN dla obu wygląda bardzo podobnie.
T
wdrożenia IEquatable<T>
. Czy w przeciwnym razie kolekcja List<T>
miałaby jakiś subtelny błąd?
Chcę zrozumieć scenariusze, w których IEqualityComparer<T>
i IEquatable<T>
należy je stosować. Dokumentacja MSDN dla obu wygląda bardzo podobnie.
T
wdrożenia IEquatable<T>
. Czy w przeciwnym razie kolekcja List<T>
miałaby jakiś subtelny błąd?
Odpowiedzi:
IEqualityComparer<T>
jest interfejsem dla obiektu, który przeprowadza porównanie dwóch obiektów tego typu T
.
IEquatable<T>
dotyczy obiektu typu, T
aby mógł porównać się z innym obiektem tego samego typu.
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
Decydując się na użycie IEquatable<T>
lub IEqualityComparer<T>
, można zapytać:
Czy istnieje preferowany sposób testowania dwóch przypadków
T
równości, czy też istnieje kilka równorzędnych sposobów?
Jeśli istnieje tylko jeden sposób testowania dwóch wystąpień T
ró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 T
samodzielnie, tak aby jedna instancja T
miał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 T
dla równości, IEqualityComparer<T>
wydaje się bardziej odpowiedni: Ten interfejs nie ma być implementowany T
samodzielnie, ale przez inne „zewnętrzne” klasy. Dlatego podczas testowania dwóch instancji T
równości, ponieważ T
nie 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 X
i Y
w 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.
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.
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.
GetHashCode
powinno 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.
Masz już podstawową definicję tego, czym one są . Krótko mówiąc, jeśli zaimplementujesz IEquatable<T>
w klasie T
, Equals
metoda na obiekcie typu T
informuje, 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 T
samej klasie ) powinna być de facto standardem reprezentacji unikalności jej obiektów / instancji. HashSet<T>
, Dictionary<T, U>
( GetHashCode
branie pod uwagę również jest nadpisywane), Contains
na List<T>
etc korzystają z tego. Wdrożenie IEqualityComparer<T>
na T
nie 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 Person
pewnym 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 T
nie 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ź :)
public int GetHashCode(Person obj)
powinien wrócićobj.GetHashCode()
obj.Age.GetHashCode
. będzie edytować.
person.GetHashCode
nigdzie bezpośrednio nie zadzwoniliśmy … dlaczego miałbyś to zmienić? - Nadpisujemy, bo chodzi o IEqualityComparer
to, żeby mieć inną implementację porównania według naszych reguł - reguł, które naprawdę dobrze znamy.
Age
wł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.GetHashCode
psuje rzeczy, to jest to problem z implementatorami typu someObject
, to nie jest nasz problem.
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).
EqualityComparer<T>
zamiast implementowania interfejsu ", ponieważEqualityComparer<T>
testuje równość przy użyciuIEquatable<T>