Linq: IEqualityComparer durch Lambda Expression implementieren 25.06.2013

Matthias Malsy
Matthias Malsy, Chief Expert
Dieser Blog-Beitrag ist eine Zusammenfassung aus einer internen Portal-Diskussion. Das  Problem und verschiedene Lösungsansätze wurden in mehreren Runden diskutiert. Danke für die Unterstützung.

Zum Durchführen von Mengenoperation bietet Linq die Operationen Distinct, Except, Contains, Union und Intersect an. Alle Operationen nutzen per Default den Equals-Operator um die Mengen miteinander zu vergleichen. Soll zur Ausführung ein spezifischer Vergleichsoperator genutzt werden, muss recht aufwändig das Interface IEqualityComparer implementiert werden.


Vorher:

// Implementierung IEqualityComparer
public class MyLastNameEqualityComparer{
public bool Equals(…) { … Vergleich auf LastName }
public int GetHashCode(){ … }
}
// Nutzung
persons1.Union(persons2, new MyLastNameEqualityComparer())

Mit einer kleinen Hilfsklasse kann die Implementierung des IEqualityComparer durch eine Lambda Expression ersetzt werden.

Nachher:

// Union per Lambda Expression 
persons1.Union(persons2, person => person.LastName)




LambdaEqualityComparer

Alle Mengenoperation bieten die Möglichkeiten einen eigenen IEqualityComparer zu definieren. Hierzu müssen im IEqualityComprer die Methoden GetHashCode() und Equals() implementiert werden wie Microsoft in einem Beispiel zeigt. Dabei gelten die Richtlinien zum Überschreiben von Equals. Alles in allem viel Aufwand um mal schnell mit Linq zwei Mengen zu verarbeiten.

So reift die Idee, einen generischen IEqualityComparer zu schreiben, der nur eine Zugriffsmethode (Lambda) benötigt, die den fachlichen Key für Equals() definiert. Auf dem Ergebnis dieser Funktion wird auch der HashCode gebildet.

IEqualityComparer mit Lambda:

   1: public class LambdaEqualityComparer<TSource, TComparable> : IEqualityComparer<TSource>
   2: {
   3:     Func<TSource, TComparable> _keyGetter;
   4:  
   5:     public LambdaEqualityComparer(Func<TSource, TComparable> keyGetter)
   6:     {
   7:         _keyGetter = keyGetter;
   8:     }
   9:  
  10:     public bool Equals(TSource x, TSource y)
  11:     {
  12:         if (x == null || y == null) return (x == null && y == null);
  13:         return object.Equals(_keyGetter(x), _keyGetter(y));
  14:     }
  15:  
  16:     public int GetHashCode(TSource obj)
  17:     {
  18:         if (obj == null) return int.MinValue;
  19:         var k = _keyGetter(obj);
  20:         if (k == null) return int.MaxValue;
  21:         return k.GetHashCode();
  22:     }
  23: }

Der LambdaEqualityComparer kann als normaler IEqualityComparer eingesetzt werden.

Nutzung von LambdaEqualityComparer durch explizite Instanzgenerierung:

   1: personList1.Union(personList2, 
   2:     new LambdaEqualityComparer<Person, string>(p => p.Name));

Mehr Komfort durch Extension Methods

Somit haben wir eine generische und damit wiederverwendbare Implementierung von IEqualityComparer.  Leider funktioniert type inference, d.h. die automatische Erkennung der Typen <Person, string>, beim Aufruf des Konstruktors nicht.

Tipp: Generics arbeiten deshalb so schön in .NET, weil i.d.R. type inference zum Einsatz kommt, und man die Typ-Argumente als Aufrufer nicht hinschreiben muss. Nur bei Konstruktoren klappt das leider nicht. Es bietet sich an statische "Erzeuger" zu schreiben. (thx Alex)

Ein statischer Erzeuger für LambdaEqualityComparer hilft uns an dieser Stelle alleine nicht weiter, da der Typ (<Person>) nicht aus dessen Parametern (dem Lambda p=>p.Name) abgeleitet werden kann. Der gesuchte Typ findet sich ausschließlich in den generischen Parametern der Union-Methode.

Somit wird als Lösung per Extension Method eine neue Union-Methode definiert, die on the fly den LambdaEqualityComparer erzeugt (thx to Sven).

   1: public static class LambdaEqualityComparer
   2: {
   3:     // source1.Union(source2, lambda)
   4:     public static IEnumerable<TSource> Union<TSource, TComparable>(
   5:         this IEnumerable<TSource> source1, 
   6:         IEnumerable<TSource> source2, 
   7:         Func<TSource, TComparable> keySelector)
   8:     {
   9:         return source1.Union(source2, 
  10:             new LambdaEqualityComparer<TSource, TComparable>(keySelector));
  11:     }
  12: }

Die verwendeten Typen können nun über type inference ermittelt werden und müssen beim Aufruf nicht mehr angegeben werden:

   1: persons1.Union(persons2, person => person.LastName)

Fazit

Das Ergebnis aus der Diskussionsrunde hat zu einer sehr eleganten Lösung geführt, die ich seitdem sehr gerne einsetze. Dabei hat es mal wieder viel Spaß gemacht mit Kollegen eine “schöne” – und nicht nur irgendeine - Lösung zu finden.

Share |

0 Kommentare:

Kommentar veröffentlichen