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.

0 0 votes
Article Rating
7 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
(Wojtek)szogun1987
(Wojtek)szogun1987
14 years ago

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> realComparer)
{
this.converter = converter;
this.realComparer = realComparer;
}

#endregion Constructors

#region IEqualityComparer<T> Members

public bool Equals(T x, T y)
{
return realComparer.Equals(converter(x), converter(y));
}

public int GetHashCode(T obj)
{
return realComparer.GetHashCode(converter(obj));
}

#endregion
}

Można by go wtedy użyć np. do porównywania pracowników po Nazwisku bez rozróżnienia wielkości liter:

LambdaEqualityComparer<User, string> comparer = new LambdaEqualityComparer<User, string>(user => user.Name, StringComparer.CurrentCultureIgnoreCase);

procent
14 years ago

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

bodziec
bodziec
14 years ago

proste a jakie fajne ;-) dzięki

rafalb
14 years ago

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
14 years ago

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
14 years ago

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

procent
14 years ago

@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

Książka

Zobacz również