Kilka funkcji Linq.Enumerable przyjmuje rozszerzenie IEqualityComparer<T>
. Czy istnieje wygodna klasa opakowująca, która dostosowuje a delegate(T,T)=>bool
do implementacji IEqualityComparer<T>
? Łatwo jest go napisać (jeśli zignorujesz problemy ze zdefiniowaniem poprawnego hashcode), ale chciałbym wiedzieć, czy istnieje rozwiązanie gotowe do użycia.
W szczególności chcę wykonać operacje ustawiania na Dictionary
s, używając tylko kluczy do definiowania członkostwa (zachowując wartości według różnych reguł).
IEqualityComparer<T>
co pomijaGetHashCode
, jest po prostu złamane.O znaczeniu
GetHashCode
Inni już skomentowali fakt, że każda niestandardowa
IEqualityComparer<T>
implementacja powinna naprawdę zawieraćGetHashCode
metodę ; ale nikt nie zadał sobie trudu, aby szczegółowo wyjaśnić dlaczego .Dlatego. Twoje pytanie konkretnie wspomina o metodach rozszerzających LINQ; Prawie wszystkie z nich polegają na kodach skrótu, aby działać poprawnie, ponieważ wewnętrznie wykorzystują tabele skrótów w celu zwiększenia wydajności.
Weźmy
Distinct
na przykład. Rozważ konsekwencje tej metody rozszerzającej, jeśli wszystko, co wykorzystała, byłoEquals
metodą. Jak ustalisz, czy element został już zeskanowany w sekwencji, jeśli tylko to zrobiłeśEquals
? Wyliczasz wszystkie wartości, które już przeglądałeś, i sprawdzasz, czy są zgodne. Spowodowałoby toDistinct
użycie algorytmu O (N 2 ) w najgorszym przypadku zamiast algorytmu O (N)!Na szczęście tak nie jest.
Distinct
nie tylko używaEquals
; używaGetHashCode
również. W rzeczywistości absolutnie nie działa poprawnie bezIEqualityComparer<T>
dostarczenia właściwegoGetHashCode
. Poniżej znajduje się wymyślony przykład ilustrujący to.Powiedzmy, że mam następujący typ:
Teraz powiedz, że mam
List<Value>
i chcę znaleźć wszystkie elementy o różnych nazwach. Jest to doskonały przypadekDistinct
użycia niestandardowego modułu porównującego równość. Skorzystajmy więc zComparer<T>
klasy z odpowiedzi Aku :Teraz, jeśli mamy kilka
Value
elementów o tej samejName
właściwości, wszystkie powinny zwinąć się w jedną wartość zwróconą przezDistinct
, prawda? Zobaczmy...Wynik:
Hmm, to nie zadziałało, prawda?
O co chodzi
GroupBy
? Spróbujmy tego:Wynik:
Ponownie: nie zadziałało.
Jeśli się nad tym zastanowić, sensowne
Distinct
byłoby użycie aHashSet<T>
(lub odpowiednika) wewnętrznie iGroupBy
użycie czegoś takiego jakDictionary<TKey, List<T>>
wewnętrznie. Czy to mogłoby wyjaśnić, dlaczego te metody nie działają? Spróbujmy tego:Wynik:
Tak ... zaczyna to mieć sens?
Mamy nadzieję, że z tych przykładów jasno wynika, dlaczego uwzględnienie odpowiedniego rozwiązania
GetHashCode
w każdejIEqualityComparer<T>
implementacji jest tak ważne.Oryginalna odpowiedź
Rozszerzanie odpowiedzi Oripa :
Można tu wprowadzić kilka ulepszeń.
Func<T, TKey>
zamiastFunc<T, object>
; Zapobiegnie to umieszczaniu kluczy typu wartości wkeyExtractor
samej rzeczywistości .where TKey : IEquatable<TKey>
ograniczenie; zapobiegnie to opakowaniu wEquals
wywołaniu (object.Equals
pobieraobject
parametr; potrzebujeszIEquatable<TKey>
implementacji, aby pobraćTKey
parametr bez pakowania go w ramki). Oczywiście może to stanowić zbyt poważne ograniczenie, więc można utworzyć klasę bazową bez ograniczenia, a wraz z nią klasę pochodną.Oto, jak może wyglądać wynikowy kod:
źródło
StrictKeyEqualityComparer.Equals
metoda wydaje się być taka sama jakKeyEqualityComparer.Equals
. Czy toTKey : IEquatable<TKey>
ograniczenie sprawia, żeTKey.Equals
działa inaczej?TKey
może to być dowolny typ, kompilator zastosuje metodę wirtualną,Object.Equals
która będzie wymagała pakowania parametrów typu wartości, npint
. Jednak w tym drugim przypadku, ponieważTKey
jego implementacja jest ograniczonaIEquatable<TKey>
,TKey.Equals
zostanie użyta metoda, która nie będzie wymagała pakowania.StringKeyEqualityComparer<T, TKey>
.Jeśli chcesz dostosować sprawdzanie równości, w 99% przypadków jesteś zainteresowany zdefiniowaniem kluczy do porównania, a nie samym porównaniem.
To mogłoby być eleganckie rozwiązanie (koncepcja zaczerpnięta z metody sortowania listy w Pythonie ).
Stosowanie:
KeyEqualityComparer
Klasa:źródło
Func<T, int>
aby dostarczyć kod skrótu dlaT
wartości (jak zasugerowano np. w odpowiedzi Rubena ). W przeciwnym razieIEqualityComparer<T>
implementacja, z którą zostaniesz, jest dość zepsuta, szczególnie w odniesieniu do jej przydatności w metodach rozszerzających LINQ. Zobacz moją odpowiedź w dyskusji na temat tego, dlaczego tak jest.Obawiam się, że nie ma takiego opakowania po wyjęciu z pudełka. Jednak stworzenie takiego nie jest trudne:
źródło
private readonly Func<T, int> _hashCodeResolver
który również musi zostać przekazany w konstruktorze i użyty wGetHashCode(...)
metodzie.obj.ToString().ToLower().GetHashCode()
zamiastobj.GetHashCode()
?IEqualityComparer<T>
niezmiennie używają haszowania za kulisami (np. LINQ's GroupBy, Distinct, Except, Join, itp.) I umowa MS dotycząca haszowania są zepsute w tej implementacji. Oto fragment dokumentacji MS: „Implementacje są wymagane, aby zapewnić, że jeśli metoda Equals zwróci true dla dwóch obiektów x i y, to wartość zwrócona przez metodę GetHashCode dla x musi być równa wartości zwróconej dla y”. Zobacz: msdn.microsoft.com/en-us/library/ms132155Taka sama jak odpowiedź Dana Tao, ale z kilkoma ulepszeniami:
Polega na
EqualityComparer<>.Default
przeprowadzaniu rzeczywistych porównań, aby uniknąć pakowaniastruct
zaimplementowanych typów wartościIEquatable<>
.Odkąd jest
EqualityComparer<>.Default
używany, nie eksplodujenull.Equals(something)
.Dostarczone statyczne opakowanie, wokół
IEqualityComparer<>
którego będzie istniała statyczna metoda tworzenia instancji funkcji porównującej - ułatwia wywoływanie. Porównaćz
Dodano przeciążenie do określenia
IEqualityComparer<>
dla klucza.Klasa:
możesz go używać w ten sposób:
Osoba to prosta klasa:
źródło
Z rozszerzeniami: -
źródło
Odpowiedź oripa jest świetna.
Oto mała metoda rozszerzenia, aby było to jeszcze łatwiejsze:
źródło
Odpowiem na własne pytanie. Aby traktować słowniki jako zestawy, najprostszą metodą wydaje się być zastosowanie operacji na zestawach do dict.Keys, a następnie konwersja z powrotem do słowników za pomocą Enumerable.ToDictionary (...).
źródło
Implementacja w (tekst niemiecki) Implementing IEqualityCompare with lambda expression dba o wartości null i używa metod rozszerzających do generowania IEqualityComparer.
Aby utworzyć IEqualityComparer w unii Linq, wystarczy napisać
Osoba porównująca:
Musisz również dodać metodę rozszerzenia, aby obsługiwać wnioskowanie o typie
źródło
Tylko jedna optymalizacja: zamiast delegowania możemy użyć gotowego rozwiązania EqualityComparer do porównań wartości.
To również uczyniłoby implementację czystszą, ponieważ rzeczywista logika porównawcza pozostaje teraz w GetHashCode () i Equals (), które być może już zostały przeciążone.
Oto kod:
Nie zapomnij przeładować metod GetHashCode () i Equals () na swoim obiekcie.
Ten post mi pomógł: c # porównać dwie wartości ogólne
Sushil
źródło
Odpowiedź oripa jest świetna. Rozszerzanie odpowiedzi Oripa:
Myślę, że kluczem do rozwiązania jest użycie „metody rozszerzenia”, aby przenieść „typ anonimowy”.
Stosowanie:
źródło
Dzięki temu można wybrać właściwość z lambdą w następujący sposób:
.Select(y => y.Article).Distinct(x => x.ArticleID);
źródło
Nie wiem o istniejącej klasie, ale coś takiego:
Uwaga: tak naprawdę jeszcze tego nie skompilowałem i nie uruchomiłem, więc może być literówka lub inny błąd.
źródło