fbpx
devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
3 minut

LambdaEqualityComparer


28.07.2010

Szkoda, że w C# nie ma znanych z Javy anonimowych klas. Nie mylić z anonimowymi typami, które nie pozwalają na implementację metod! W internecie jest wiele skarg i próśb próbujących wymusić na Microsofcie dodania tego, jakże wygodnego, ficzera do naszego języka.

Powstaje jednak pytanie: gdzie tak naprawdę byśmy owych klas używali? Jedna odpowiedź szczególnie regularnie przewija się przez praktycznie każdy wątek o tej tematyce: do zaimplementowania inline interfejsu IEqualityComparer<T>. Czy nie irytuje Was konieczność definiowania nowej klasy za każdym razem, gdy jesteście zmuszeni do wykorzystania metody żądającej implementacji tego interfejsu? Oj, mnie irytowała, i to bardzo.

Aż w końcu za którymś razem zatrzymałem się na chwilę i zamiast definiować nowy twór, jakiś zagnieżdżony prywatny typ aby nie zaśmiecał mi API, pomyślałem jak obejść tą durną przeszkodę. Okazało się, że to… proste!

Mając nadużywaną we wszelkich przykładach klasę User:

  1:  public class User
  2:  {
  3:  	public int Id { get; set; }
  4:  	public string Name { get; set; }
  5:  }

chcemy dowiedzieć się za pomocą LINQ czy jakiś zbiór użytkowników zawiera Tego, O Którego Nam Chodzi, jednak nie możemy porównać ich przez referencję. Dotychczas mój kod wyglądałby tak:

  1:  public class User
  2:  {
  3:  	public class EqualityComparerById : IEqualityComparer<User>
  4:  	{
  5:  		public bool Equals(User x, User y)
  6:  		{
  7:  			return GetHashCode(x) == GetHashCode(y);
  8:  		}
  9:  
 10:  		public int GetHashCode(User obj)
 11:  		{
 12:  			return obj.Id.GetHashCode();
 13:  		}
 14:  	}
 15:  
 16:  	//...

i dalej:

  1:  List<User> users = //...
  2:  User searchedUser = //...
  3:  
  4:  bool contains = users.Contains(searchedUser, new User.EqualityComparerById());

Po napisaniu takiego czegoś raz, drugi… miałem dość. Oto do czego doszedłem:

  1:  List<User> users = //...
  2:  User searchedUser = //...
  3:  
  4:  bool contains = users.Contains(searchedUser, new LambdaEqualityComparer<User, int>(u => u.Id));

, gdzie kluczową rolę pełni:

  1:  public class LambdaEqualityComparer<T, TValue> : IEqualityComparer<T>
  2:  {
  3:  	private readonly Func<T, TValue> _extractKey;
  4:  
  5:  	public LambdaEqualityComparer(Func<T, TValue> extractKey)
  6:  	{
  7:  		_extractKey = extractKey;
  8:  	}
  9:  
 10:  	public bool Equals(T x, T y)
 11:  	{
 12:  		return GetHashCode(x) == GetHashCode(y);
 13:  	}
 14:  
 15:  	public int GetHashCode(T obj)
 16:  	{
 17:  		return _extractKey(obj).GetHashCode();
 18:  	}
 19:  }

WRESZCIE mam możliwość zdefiniowania najprostszych implementacji IEqualityComparer<T> inline! Bierzcie i kodujcie z tego wszyscy.

Notify of
(Wojtek)szogun1987
(Wojtek)szogun1987

Wydaje mi się że ten kod nie będzie do końca poprawny, sprawdzi się dla int’ów ale już dla Long’ów nie. Ponieważ zbiór wartości long jest znacznie większy niż zbiór wartości int automatycznie wiele z nich musi mieć identyczny HashCode, a więc opieranie metody Equals na metodzie GetHashCode sprawi że liczba 1234567 będzie "równa" 1 (przykład od pały). Osobiście zaproponowałbym rozwiązanie oparte na dekoratorze które umożliwiłoby wykorzystanie zaawansowanych EqualityComparer’ów z biblioteki standardowej we własnym kodzie: class LambdaEqualityComparer<T, TValue> : IEqualityComparer<T> { #region Private members private Func<T, TValue> converter; private IEqualityComparer<TValue> realComparer; #endregion Private members #region Constructors public LambdaEqualityComparer(Func<T, TValue> converter, IEqualityComparer<TValue>… Read more »

procent

@(Wojtek)szogun1987:
Jak najbardziej masz rację – dodatkowy pokazany przez Ciebie ctor może z pewnością być przydatny. Dzięki za sugestię.

bodziec
bodziec

proste a jakie fajne ;-) dzięki

rafalb

Jest jeszcze metoda Any() (rozszerzenie dla IEnumerable<T>), której jedna z wersji przyjmuje dowolny predykat. Z jej użyciem podany kod wyglądałby tak:

bool contains = users.Any(u => u.Id == searchedUser.Id);

Wydaje się to bardziej czytelnym rozwiązaniem dla tego konkretnego przypadku.

(Wojtek)szogun1987
(Wojtek)szogun1987

rafalb pamiętaj o kwestiach wydajnościowych, metoda Any prawdopodobnie wywołuje predykat dla każdego elementu w kolekcji wykorzystując IEqualityComaparer zyskujemy dzięki wykorzystaniu wyszukiwania koszykowego. W przypadku metody Contains oznacza to wywołanie raz metody GetHashCode oraz kilkukrotnie metody Equals.
Wartość wartości kilkukrotnie zależy od jakości algorytmu Hashującego wartość ta waha się od 1 do n razy – najczęściej około sqrt(n) gdzie n oznacza ilość elementów w kolekcji.

rafalb

Metody Any() i Contains() działają tak samo, sprawdzając elementy kolekcji jeden po drugim.

procent

@rafalb:
W takim razie można sobie wyobrazić identyczny przykład, tylko z Distinct() zamiast Contains() :)

Kurs Gita

Zaawansowany frontend

Szkolenie z Testów

Szkolenie z baz danych

Facebook

Książka

Zobacz również