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.
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);
@(Wojtek)szogun1987:
Jak najbardziej masz rację – dodatkowy pokazany przez Ciebie ctor może z pewnością być przydatny. Dzięki za sugestię.
proste a jakie fajne ;-) dzięki
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.
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.
Metody Any() i Contains() działają tak samo, sprawdzając elementy kolekcji jeden po drugim.
@rafalb:
W takim razie można sobie wyobrazić identyczny przykład, tylko z Distinct() zamiast Contains() :)