čtvrtek 28. ledna 2016

Hrátky s Linqem aneb Distinct pětkrát jinak


Dotazovací jazyk LINQ není asi nutné nějak detailněji představovat - je součástí .NETu už pěkně dlouho. Použití našel nejen jako součást Entity Frameworku, kde umožňuje přístup k datům v databázi i bez znalosti SQL, ale prakticky všude, kde je potřeba zpracovávat kolekce. 
Jak se píše na české Wikipedii, "LINQ přináší nový způsob pro dotazování nad jakýmikoliv daty, usnadňuje jejich tvorbu, třídění, jejich propojování i vyhledávání v nich.... je možné v něm manipulovat s různými daty."
Jedním z častých úkolů je odstranit v dané kolekci duplicity. To je jednoduché pro kolekce hodnot, stačí použít operátor Distinct a duplicitní položky jsou odstraněny:

var numbers = new int[] { 1, 2, 3, 1, 2, 3, 4, 5 };
 
var cleanedNumbers = numbers.Distinct();
Jakmile je kolekce tvořena objekty, je úkol obtížnější. Existuje třeba kolekce autorů, kde se ale někteří autoři vyskytují díky nějaké chybě dvakrát. Cílem je mít kolekci bez duplicit, tedy kolekci, ve které se každý autor vyskytuje jen jednou.

         var authors = new Author[] {
             new Author { FirstName ="Joseph", Surname="Albahari" },
             new Author { FirstName ="Ben", Surname ="Albahari" },
             new Author { FirstName = "Andrew", Surname = "Troelsen" },
             new Author { FirstName ="Joseph", Surname="Albahari" },
             new Author { FirstName ="Ben", Surname ="Albahari" },
         };

Nabízí se několik možností:

1. Upravit třídu

To většinou není ani vhodné a nebo ani možné. Bylo by nutné přepsat metodu Equals dané třídy a metody generující hash code. A tomu bych se raději vyhnul  - existuje více důvodů, proč to nedělat a v praxi se mi to většinou vymstilo. Takže to tady ani raději nebudu doprovázet ukázkou kódu.

2. Použít vlastní porovnávač - třídu implementující IEqualityComparer<T>

Pokud nám nevadí napsat si vlastní třídu, která implementuje rozhraní IEqulaityCompararer, tak to lze udělat tak, že si nejpreve napíšeme vlastní porovnávač:

class AuthorComparer : IEqualityComparer<Author>
{
   public bool Equals(Author x, Author y)
   {
      return 
         x.FirstName.Equals(y.FirstName) 
         && 
         x.Surname.Equals(y.Surname);
   }
 
   public int GetHashCode(Author obj)
   {
      unchecked
      {
         int hash = 17;
         hash = hash * 23 + obj.FirstName.GetHashCode();
         hash = hash * 23 + obj.Surname.GetHashCode();
         return hash;
      }
   }
}
A pak ho použijeme:

var cleanedAuthors = authors.Distinct(new AuthorComparer());
Nevýhodou je nepřehlednost kódu, pro zjištění, na základě čeho se zjišťují duplicity, je nutné přejít do třídy AuthorCompare. Navíc je nutné podobnou třídu vytvořit pro každou třídu, respektive kolekci, kterou chceme vytřídit.

3. Anonymní typ

Anonymní typ je definován jako bezejmenná třída. Pro vytvoření stačí v inicialitoru určit názvy vlastností a jejich hodnoty  - vlastnosti budou nadále jen read only. Anonymní typ je potomkem třídy System.Object.
Důležitou vlastností je, že při porovnání anonymního typu probíhá porovnání na základě hodnot všech veřejných vlastností. Stačí tedy vygenerovat kolekci anonymních typů:

var cleanedAuthors = authors
         .Select(a => new { FirstName = a.FirstName, Surname = a.Surname })
         .Distinct();


Nevýhodou ovšem je, že dostaneme kolekci anonymních typů. To nutně nemusí být na závadu, pokud nám stačí dále pracovat jen s vlastnostmi bez ohledu na typ, ale pokud chceme mít kolekci autorů, je nutné mít volání složitější:

var cleanAuthors = authors
         .Select(a => new { FirstName = a.FirstName, Surname = a.Surname }).Distinct()
         .Select(a => new Author { FirstName = a.FirstName, Surname = a.Surname });

4. Seskupení

Můžeme také využít seskupení a vybrat první element z každé skupiny:

var cleanAuthors = authors
      .GroupBy(a => new { a.FirstName, a.Surname })
      .Select(g => g.First());

Určitou nevýhodu tohoto postupu může být  mírná nepřehlednost, kdy se za použitím GroupBy operátor vlastně skrývá operace Distinct a v budoucnu to může vést k nepřehlednosti a obtížnému chápání takového kódu. Dobré je tedy doplnit do kódu poznámku o co vlasně ve skutečnosti jde.

5. Zkombinovat 2 a 3

A co zkusit výhody, které by nám nabídla kombinace rozšiřujících metod, anonymního typu, predikátu a vlastního porovnávače.  Nejprve je nutné implementovat třídu implementující IEqualityComparer rozhraní:

public class ByKeyComparer<T> : IEqualityComparer<T>
{
   private readonly Func<T, object> keySelector;
 
   public ByKeyComparer(Func<T, object> keySelector)
   {
      this.keySelector = keySelector;
   }
 
   public bool Equals(T x, T y)
   {
      return this.keySelector(x).Equals(this.keySelector(y));
   }
 
   public int GetHashCode(T obj)
   {
      return this.keySelector(obj).GetHashCode();
   }
}
Poté napsat extension metodu pro Linq:

public static class LinqExtension
{
   public static IEnumerable<T> Distinct<T>(this IEnumerable<T> input, Func<T, object> keySelector)
   {
      return input.Distinct(new ByKeyComparer<T>(keySelector));
   }
}
A nyní je při zápisu příkazu dostupná další implementaci volání Distinct:



Jenže pokud zadáme jen jednu vlastnost, tedy přijmení, nebude výsledný seznam autorů přesný - pokud by kolekce obsahovala autory Joseph AlbahariBen Albahari, tak se nám ve výsledku objeví jen jeden z nich v závislosti na svém pořadí v kolekci. Což je špatně.  A tak nyní přichází na řadu anonymní typ, stačí totiž zapsat toto:

var cleanedAuthors = authors.Distinct(a => new { a.FirstName, a.Surname });
a díky vlastnostem anonymního typu (viz výše) a implementaci třídy ByKeyComparer dojde k porovnávání těchto dvou vlastností nové anonymní třídy.

Možná existují další metody, ale výše uvedené jsou ty, které používám já.

Spustitelný kód je dostupný na .NET Fiddle:

Žádné komentáře:

Okomentovat