Přejít k navigační liště

Zdroják » Různé » LINQ a lambda expressions

LINQ a lambda expressions

Články Různé

Článek představuje technologii LINQ, která umožňuje unifikované dotazování do datových zdrojů z prostředí .NET. LINQ přišel s verzí .NET Frameworku 3.5 a jazyky C# 3.0 a Visual Basic 9. Protože je LINQ velmi úzce svázán s novinkami v těchto verzích programovacích jazyků, podíváme se také na jednu z nejzásadnější a zároveň netriviálních novinek – lambda expressions.

LINQ (Language Integrated Query) je technologie, která umožňuje v prostředí .NET unifikovaný způsob, jak se dotazovat do „libovolného“ zdroje (nejen) pomocí syntaxe velmi podobné SQL. LINQ tvoří tři hlavní části:

  1. sada operátorů
  2. rozšíření jazyků C# a VB.NET
  3. LINQ providery

Všechny tyto částo mohutně využívají novinky .NET Frameworku verze 3.5 a jazyků, které přišly s touto verzí .NET Frameworku. Vesměs se jedná o syntaktický cukr (proto je stále možné kompilovat vůči .NET Frameworku 2.0), v jednom případě ale velmi pokročilý – lambda expressions a expression trees.

Lambda expressions

Začněme trošku obšírněji – co je možné přiřadit do delegáta („ukazatele na funkci“) ? V prvních verzích C# byla jediná možnost – vytvořit metodu se stejnou signaturou a do delegáta přiřadit její jméno:

public delegate int ExampleDelegate(int i1, int i2);
public static int ExampleMethod(int i, int j)
{
    return i + j;
}
static void Main(string[] args)
{
    ExampleDelegate del = ExampleMethod;
}

V C# 2.0 přibyly anonymní metody, které nám ušetřily nějaké to psaní:

ExampleDelegate del = delegate(int i, int j)
{
    return i + j;
};

Na pozadí se vždy vygenerovala metoda s odpovídající signaturou (příp. v třídě reprezentující scope), jednalo se tedy o syntaktický cukr. Ale díky ošetření scope dost dobrý!

No a konečně ve verzi C# 3.0 přišly lambda expressions:

ExampleDelegate del = (i, j) => i + j;

Jak je vidět, lambda expressions můžeme použít jen tehdy, když můžeme vyjádřit výpočet návratové hodnoty pomocí jednoho výrazu (v mnoha situacích tedy jistě oceníte ternární operátor ?:). Uvedení typů parametrů je volitelné. Pokud potřebujete dělat něco složitějšího („imperativněj­šího“), přichází ke slovu statement lambdas, které poznáte podle složených závorek:

ExampleDelegate del = (i, j) =>
{
    Console.WriteLine("delegate called");
    return i + j;
};

Lambda expressions (v C# 4.0 pravděpodobně i statement lambdas) umožňují ale i něco víc. Kromě do delegáta je lze totiž přiřadit i do datového typu System.Linq.Ex­pressions.Expres­sion<T>. V takovém případě již překladač vůbec nevytváří metodu, ale místo toho tělo lambda expression naparsuje a vytvoří strom (expression tree), který popisuje zadaný výraz. Uzly tohoto stromu jsou povětšinou potomci třídy Expression. Nic nám tedy nebrání si sestavit expression tree i ručně – pak je možné s výrazy dělat za běhu nejrůznější kejkle.

Jako příklad si uveďme jednoduchou metodu, která nám vrátí ve stringu název zadané property. Výhodou tohoto řešení (oproti přímému zápisu jména property ve stringu) je silná typovost – omezíme tím možnost překlepu a můžeme tím ulehčit práci refaktorovacím nástrojům:

public static string GetPropertyName<TModel, TProperty>(Expression<Func<TModel, TProperty>> expression)
{
    if (expression.Parameters.Count == 0)
    {
        throw new ArgumentException("expression");
    }
    return expression.Body.ToString().Substring(expression.Parameters[0].Name.Length + 1);
}
static void Main(string[] args)
{
    Console.WriteLine(GetPropertyName<DateTime, DateTime>(m => m.Date)); // vypíše "Date"
}

Uznávám, že volání metody je poněkud ukecané, ale pokud je v dané situaci možné využít generic parameters inference (automatické vyhodnocení generických parametrů), tak dostáváme do ruky zajímavý nástroj.

Z předcházejících odstavců byste si měli odnést jedno základní poznání – záleží, zda přiřazujete do SomeDelegate (pak přiřadíte klasicky delegáta) nebo do Expression<So­meDelegate> (přiřadíte expression tree daného výrazu).

Zajímavost: Generická třída Expression<TDe­legate> (: LambdaExpression : Expression) má metodu Compile, která daný expression tree zkompiluje do delegáta (pomocí emitování CIL kódu). Tento delegát se často ihned volá, což vede na v C# netypickou konstrukci expr.Compile()(). Dá se tedy říct, že vede cesta od expression tree k delegátovi. Z delegáta ale expression tree triviálně nesestavíte…

LINQ operátory

Jak jsme si řekli v úvodu, jednou ze součástí technologie LINQ jsou LINQ operátory (Standard LINQ Operators). To jsou metody, které musí být dostupné na datovém zdroji a lze je rozdělit do několika skupin: selektovací (Select, SelectMany), filtrovací (Where), agregační (Sum, Max), joinovací (Join) atd. Jejich seznam se stručným popisem je třeba zde.

Jak jsem psal, metody musí být na daném datovém zdroji dostupné, tzn. datový zdroj je nemusí nutně přímo implementovat, ale stačí, aby existovaly aplikovatelné extension metody (právě to je v praxi ten nejčastější případ).

Jednoduchý příkladek s LINQ operátory:

var result = new[] { 1, 4, 5 }.Where(i => i == 5).Select(i => new { Int = i, Name = "bla" });

Rozšířená syntaxe

To, co bývá při prezentacích LINQu nejvíce na očích, je právě ona rozšířená syntaxe, která v základní verzi vypadá takto (budu uvádět jen pro jazyk C#):

var result = from i in new[] { 1, 4, 5 } where i == 5 select new { Int = i, Name = "bla" };

Za klíčovým slovem from uvedeme identifikátor, který bude představovat jednu položku kolekce, za in je uveden datový zdroj, za where je tělo lambda funkce, která určuje, zda daná položka bude zahrnuta do výsledku (její hodnota tedy musí být typu bool) a za select uvedeme, jak se vytvoří objekt reprezentující jedna položka výsledku (zde používám anonymní třídu). Jedná pouze o syntaktický cukr, protože select je „přeloženo“ na metodu Select, where na Where atd.

Samozřejmě je možné oba způsoby zápisu míchat:

var result = (from i in new[] { 1, 4, 5 } where i == 5 select new { Int = i, Name = "bla" }).Distinct();

Někdy je to dokonce nutné, protože např. metoda Distinct nemá odpovídající klíčové slovo v rozšířené syntaxi.

LINQ

LINQ providery

Z předchozích odstavců vyplývá, že pokud chceme nad nějakou třídou používat LINQ, tak musíme zajistit dostupnost LINQ operátorů na této třídě. Pokud nebude psát vlastní LINQ providery, bude to pro nás většinou znamenat jen nareferencovat správnou assembly a dopsat nějaký using na začátek zdrojového souboru (aby byly dostupné extension metody, které implementují LINQ operátory). Následující tři odstavce jsou spíše mírný pohled pod kapotu než něco, co jako uživatelé LINQu využijete.

Pokud třída implementuje rozhraní IEnumerable<T>., můžeme využít statickou třídu System.Linq.E­numerable<T> z assembly System.Core. V praxi pro to nemusíme udělat nic (při použití Visual Studia 2008), protože assembly System.Core je standardně referencovaná a na začátku *.cs souboru máme uvedeno using System.Linq;, což nám zpřístupní extension metody z třídy System.Linq.E­numerable<T>. Abychom tedy mohli využívat LINQ při práci s poli a běžnými kolekcemi, není třeba dělat nic – jde to samo.
Tato implementace LINQ operátorů pracuje přímo nad objekty v paměti a parametry metod jsou delegáty. Jak už bylo řečeno, používá se především pro práci nad klasickými poli a kolekcemi ze System.Collec­tions.Generic.

Pokud chceme pracovat prostřednictvím LINQu nad datovými zdroji, jejichž položky nejsou přímo uloženy v paměti (např. databáze), máme k dispozici statickou třídu System.Linq.Qu­eryable (opět z assembly System.Core). Tato implementace LINQ operátorů se od předchozí liší ve dvou hlavních bodech – metody místo delegátů přijímají expression trees a implementace pracuje nad rozhraním IQueryable<T>. To je rozhraní odvozené od IEnumerable<T> a přidává především property Provider, která zjednodušeně řečeno slouží ke konverzi expression tree do nějaké vnitřní formy vhodné pro dané použití (něco jako zadní část kompilátoru) a ke spouštění této vnitřní formy. Onou vnitřní formou mohou být např. fragmenty SQL dotazu.

Abychom mohli provádět dotazy nad vlastní třídou (to bude ale asi v praxi řešit málokdo), musí tedy tato třída implementovat rozhraní IQueryable<T>, což je IEnumerable<T> rozšířená především o property typu IQueryProvider. Musíme tedy implementovat ještě toto rozhraní, které je zodpovědné za vlastní vytváření a spouštění dotazů (metody CreateQuery a Execute).

Zpět k běžnému použití LINQu – nejen při implementaci LINQ providerů je důležité si uvědomit, že existují v zásadě dvě skupiny LINQ operátorů:

První skupina LINQ operátorů (je jich většina) zjednodušeně řečeno jen vytváří dotaz do datového zdroje, tj. nepotřebují komunikovat s datovým zdrojem. Např. operátor Where pouze vrátí objekt, který představuje wrapper nad původním zdrojem, přičemž tento wrapper nese také informaci o tom, přes jaké prvky kolekce chceme iterovat. Takovéto LINQ operátory tedy vůbec nekomunikují s datovým zdrojem, ale slouží jen k sestavení dotazu.

Druhá skupina LINQ operátorů již vyžaduje komunikaci s datovým zdrojem. Jsou to především agregační operátory jako Sum, Min apod.

Příkladem pouhého sestaveného dotazu jsou výše uvedené příklady. Jejich vykonáním vůbec nedojde k iteraci pole, výsledkem je pouze proměnná result, která nese informaci o dotazu.

Co když ale chceme, aby došlo ke komunikaci s datovým zdrojem ihned, ale zároveň nechceme použít žádný LINQ operátor z druhé skupiny? Taková situace může nastat třeba tehdy, když chceme získat výsledek dotazu do databáze a ihned uzavřít spojení do ní. Pak máme několik možností, jak LINQ donutit k okamžitému spuštění dotazu – můžeme zavolat metodu GetEnumerator (volá se automaticky při použití cyklu foreach), ToArray nebo ToList.

Použití

Jak bylo uvedeno v úvodu, LINQ můžeme použít tam, kde se potřebujeme na něco dotazovat. Přímo se nabízí využití v rámci O/R mapperu, ale nejsme ničím omezeni. Navíc pokud nenalezneme LINQ provider pro námi používaný datový zdroj, tak si můžeme napsat vlastní.
Pokud se tedy rozhodnete používat LINQ ve svém projektu, stačí obstarat si LINQ provider pro daný datový zdroj a můžete používat jednotný jazyk pro dotazování.

Providery se nejčastěji označují jako „LINQ to …“. Existuje tak LINQ to IEnumerable<T> (LINQ to Objects), které je obsaženo ve standardní třídě System.Linq.E­numerable a umožňuje dotazy nad čímkoliv, co implementuje rozhraní IEnumerable<T>. Tato implementace usnadňuje běžné programátorské úkony.

Dále máme k dispozici LINQ to XML umožňující dotazování do XML dokumentu. Ale pozor, nepracuje nad třídami z namespace System.Xml, ale System.Xml.Linq, např. XElement.

Samozřejmě nesmí chybět providery, které pracují proti databázi: LINQ to DataSet pracuje proti ADO.NET (a umožňuje tak pracovat vůči jakékoliv databázi), LinqToSql je celý O/R mapper pracující proti MS SQL Serveru 2000+, Linq to Entities je součást Entity Frameworku.

Na internetu lze najít spoustu nejrůznějších providerů, pro zajímavost např. Linq to Google nebo Linq To Facebook.

Příklad

Na závěr tu máme příklad, který ukazuje základní použití LINQu. Na hodně místech bych v praxi použil klíčové slovo var, ale pro názornost jsem uvedl přesný typ všude, kde to bylo možné. Abych do příkladu nezatahoval další neznámé v podobně tříd specifických pro nějaký LINQ provider (např. Entity Framework), pracuje příklad nad POCO (obyčejnými objekty).

public enum Gender { Female, Male }
public class Address{
    public string City { get; set; }
}
public class Person{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Gender Gender { get; set; }

    public Address Address { get; set; }
}
static void Main(string[] args)
{
    // Vytvoříme si kolekci lidí, nad kterou budeme pracovat.
    var persons = new List<Person>(new[]
    {
        new Person { FirstName = "Jan", LastName = "Novák",
            Gender = Gender.Male, Address = new Address { City = "Praha" }
        },
        new Person { FirstName = "Jana", LastName = "Nováková",
            Gender = Gender.Female, Address = new Address { City = "Praha" }
        },
        new Person { FirstName = "Lucie", LastName = "Dvořáková",
            Gender = Gender.Female, Address = new Address { City = "Plzeň" }
        },
    });

    // Vybereme z kolekce pouze muže.
    IEnumerable<Person> men = persons.Where(p => p.Gender == Gender.Male);

    // V proměnné men máme pouze uloženo, jak se bude iterovat - iterace přes kolekci ještě neproběhla.
    // Proto můžeme ještě teď přidat další položku do kolekce a ovlivní nám to výsledek.
    persons.Add(new Person    {
        FirstName = "Jiří",
        LastName = "Nováček",
        Gender = Gender.Male,
        Address = new Address { City = "Brno" },
    });

    // Vlastní dotaz můžeme spustit pomocí volání ToArray, ToList, agregačních funkcí nebo iterací.
    foreach (Person m in men) // vypíše Nováka i dodatečně vloženého Nováčka
    {
        Console.WriteLine(m.FirstName + " " + m.LastName);
    }

    // Dotaz uložený v proměnné men můžeme spouštět kolikrát chceme, příp. na něj můžeme vrstvit další dotazy.
    Console.WriteLine(men.Count());
    //
    // Ukázka agregační funkce, která okamžitě spustí dotaz. Vypíše 2.
    // Zkonstruujeme více restriktivní dotaz.
    IEnumerable<Person> oldMen = men.Where(m => m.Address.City == "Praha");
    foreach (Person m in oldMen) // vypíše pouze Nováka    {
        Console.WriteLine(m.FirstName + " " + m.LastName);
    }

    // Od mužů si vyzobeme jen adresy.
    IEnumerable<Address> addresses = men.Select(m => m.Address);
    foreach (Address a in addresses)
    // vypíše Praha a Brno
    {
        Console.WriteLine(a.City);
    }

    // Vyzobeme jen křestní jména a města žen.
    // Pokud nechceme zavádět novou třídu obsahující jen křestní jméno a město, můžeme použít
    // anonymní třídu - pak je nutné použít var.
    var data = persons.Where(p => p.Gender == Gender.Female).
               Select(w => new { w.FirstName, w.Address.City });
    foreach (var d in data)
    // musíme použít var, protože iterujeme přes kolekci anonymních objektů
    {
        Console.WriteLine(d.FirstName + " - " + d.City);
    }
}

Komentáře

Subscribe
Upozornit na
guest
7 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Tuck

Moc pěkně napsáno. Ještě by byl super článek o vlastní implementaci IQueryable<T>.

Díky za článek

René Stein

Pěkný článek Augi.

Jen bych zmínil dvě maličkosti:

Citace:
„V prvních verzích C# byla jediná možnost – vytvořit metodu se stejnou signaturou a do delegáta přiřadit její jméno:“

To je pravda, ale automatická inference typu delegáta nefungovala. V C# 1.x bylo nutné použít takovouto syntaxi.

ExampleDelegate del = new ExampleDelega­te(ExampleMet­hod);

Chápu, že klíčové slovo var jsi nechtěl použít a u kolekcí žádná záludnost nehrozí, ale té explicitní deklarace typu proměnných bych se v tomto případě bál.

Už jen proto, že jsme několikrát viděl v praxi u lidí, kteří se zaklínali tím, že var je zlo, tento děsivý kód.

IEnumerable<Person> men = persons.Where(p ⇒ p.Gender == Gender.Male);

Ale persons byly typu IQueryable (from person in dbContext.Persons select person). Potom to znamena, ze dotazem do databaze byly vyzvednuty vsechny osoby a teprve v aplikace se aplikoval operator where z IEnumerable. IQueryable je v tomto pripade BOHUZEL potomkem IEnumerable, takze implicitni konverze bez problemu projde.

Pokud pouziju:
var men = persons.Where(p ⇒ p.Gender == Gender.Male);
Nechavam spinavou praci na kompilatoru, ktery v danem pripade spravne vytvori promennou typu IQueryable a cely dotaz i s podminkou bude vykonan na serveru (SQL) a vrati se mi jen zaznamy, ktere vyhovuji podmince p.Gender == Gender.Male);

René Stein

IEnumerable<Person> men = persons.Where(p ⇒ p.Gender == Gender.Male);
Tadz ještě upřesním, že person byly původně IQuryable, ale jsou také přetypovány na IEnumerable.

IEnumerable<Person> men = from person in dbContext.Persons select person;

Michal B.

Ahoj Rene.
Zajimave upozorneni, ktere neni vubec „viditelne“ na prvni pohled.
Diky za nej !

Michal

Timy

Skvělý článek, díky.

Petr Minařík

Díky rád jsme si přečetl článeček. Pomohlo mně začátečníkovi a to tak, že vydatně při pochopení delegátů, lambda i linq.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.