A C# programozási nyelv

LINQ lekérdezések

LINQ lekérdezések

Bevezetés

LINQ, azaz Language Integrated Queries (nyelvbe épített lekérdezések) a C# 3.0 nyelv és keretrendszer újdonsága, mellyel struktúrált típusbiztos lekérdézeseket készíthetünk lokális objektum gyűjteményeken és távoli data source-okon.

LINQ lekérdezéseket bármelyik IEnumerable<> interfészt implementáló gyűteményen végre lehet hajtani, legyen az tömb, lista, XML DOM vagy esetleg egy SQL Server táblája. A LINQ előnyei még a forditás idei típusellenőrzés és a dinamikus lekérdezés kompozíció.

A LINQ alaptípusok a System.Linq illetve System.Linq.Expressions névterekben definiáltak.

A lekérdezések alap egységei a sorozatok és az elemek. Sorozatok lehetnek az előbb említett IEnumerable<> generikus interfész által implementált gyűjtemények, és az elem ennek a sorozatnak bármely tagja. Pl. az alábbi példában a names a sorozat és Tom, Dick, Harry az elemek (a továbbiakban ezt a példát fogjuk használni):

string[] names = { ”Tom”, ”Dick”, ”Harry” };

Ezt lokális sorozatnak hívjuk, mivel egy lokális objektum-gyűjteményt reprezentál a memóriában.

A lekérdező operátor egy olyan metódus, ami transzformálja sorozatunkat. Egy lekérdező operátor egy bemenő sorozatot kap, és egy transzformált kimenő sorozatot ad vissza. A System.Linq Enumerable osztályában közel 40 lekérdező operátor van (mindegyik statikus kiterjesztett metódusként van implementálva). Ezek a standard lekérdező operátorok.

A legegyszerűbb lekérdezés egy sorozatból és egy operátorból áll. Például alkalmazhatjuk a Where operátort egy sima tömbön, azokat lekérdezve, melyeknek a hossza legalább 4:

string[] names = { ”Tom”, ”Dick”, ”Harry” }; IEnumerable filteredNames = System.Linq.Enumerable.Where (names, n => n.Length >= 4); foreach (string n in filteredNames) Console.Write (n + "|"); //Dick|Harry|

Mivel a standard query operátorok kiterjesztesztett metódusok, a Where-t közvetlenül a names-en is meghívhatjuk:

IEnumerable filteredNames = names.Where (n => n.Length >= 4);

A legtöbb lekérdező operátor argumentumként elfogad lambda kifejezést is. A lambda kifejezések segítenek irányítani és formában tartani a lekérdezést. Így néz ki egy lamdba lekérdezés pl.:

n => n.Length >= 4

A bemenő argumentum egy bemenő elemnek felel meg. Esetünkben az n bemenő argumentum reprezentálja az összes nevet a tömbben, melyek string típusúak. A Where operátor egy bool típusú értéket vár el a lamdba kifejezéstől, amely, ha igazat ad vissza, akkor a kimenő sorozatba bele kell rakni. A szignatúra:

public static IEnumerable Where (this IEnumerable source, Func predicate)

Pl. a következő metódus visszaadja az összes olyan nevet, amiben van a betű:

IEnumerable filteredNames = names.Where(n => n.Contains("a")); foreach (string name in query) Console.Write(name + "|"); // Harry|

Eddig a lekérdezéseket kiterjesztett metódusok és lambda kifejezések segítségével végeztük el. De a C#-ban egy speciális szintaxis is definiálva van lekérdezések írásához, az úgynevezett query comprehension szintaxis. Itt van az előbbi lekérdezésünk ebben a szintaxisban:

IEnumerable filteredNames = from n in names where n.Contains("a") select n;

A lambda szintaxist és a comprehension szintaxist részletezzük a továbbiakban.

Lambda lekérdezések

A lambda lekérdezések a legrugalmasabbak és legalapvetőbbek. Megmutatjuk, hogyan lehet az operátorokat láncolni, hogy összetettebb lekérdezéseket tudjunk csinálni, és megmutatjuk, hogy a kiterjesztett metódus szintaxis miért fontos a folyamatban. Még azt is tárgyaljuk, hogyan kell lambda kifejezéseket megírni egy lekérdező operátornak, és bemutatunk néhány új operátort.

Lekérdező operátorok láncolása

Összetettebb lekérdezések építéséhez több lekérdező operátort használunk, ezzel láncot alkotva. Az alábbi példa kiszűri az ’a’-t tartalmazó stringeket, hossz szerint rendezi őket, és az eredményt nagybetűssé alakítja:

using System; using System.Linq; class LinqDemo { static void Main() { string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" }; IEnumerable query = names .Where (n => n.Contains("a")) .OrderBy (n => n.Length) .Select (n => n.ToUpper()); foreach (string name in query) Console.Write(name + "|"); } }
JAY|MARY|HARRY

Már bemutattuk a Where operátort, ami a bemenő sorozat szűrt változatát adja vissza. Az OrderBy operátor egy rendezett változatát adja vissza a bemenő sorozatnak, míg a Select metódus az, ahol a bemenő sorozat összes elemét transzformáljuk vagy leképezzük egy adott lambda kifejezéssel (esetünkben mindent nagybetűssé tettünk). A láncolásban az adatok jobbról balra haladnak, először szűrjük, rendezzük, utána képezzük le őket.

Megadhatjuk a lekérdezéseket progresszíven is:

// A System.Linq névteret importálni kell fordítás előtt IEnumerable filtered = names .Where (n => n.Contains("a")); IEnumerable sorted = filtered.OrderBy (n => n.Length); IEnumerable finalQuery = sorted .Select (n => n.ToUpper());

Az utolsó változóban tárolt érték megegyezik a láncolás után keletkező változóban levő operátorral.

Miért fontosak a kiterjesztett metódusok?

A kiteresztett metódus szintaxisa helyett használhatjuk a hagyományos statikus metódus szintaxist is (előző példa). A fordító erre az alakra hozza a kiterjesztett metódus hívásokat.

Lambda kifejezések komponálása

A lambda kifejezés célja az egyes lekérdező operátoroktól függ. pl. A Where operátor azt mondja meg egy elemről, hogy az benne legyen vagy ne a kimenetben, az OrderBy-ban a lambda kifejezés átcsoportosítja azt a kulcsnak megfelelően, míg a Select operátor meghatározza, hogy egyes elemeken mit kell végrehajtani a kimenetre küldés előtt. A lamdba kifejezésre lehet úgy gondolni, mint egy visszacsatolásra. A lekérdező operátor a lamdba kifejezést igény esetén értékeli ki (tipikusan egyszer elemenként a bemenetben). A lambda kifejezések megengedik, hogy a saját logikádat beépítsd a lekérdező operátorokba. Ez teszi a lekérdező operátorokat sokoldalúvá (és emellett még egyszerűvé is). Itt van példának a Where metódus teljes implementációja (hibakezelés nélkül):

public static IEnumerable Where (this Enumerable source, Func predicate) { foreach (TSource element in source) if (predicate(element)) yield return element; }

Természetes sorrend

Az elemek természetes sorrendje lényeges a LINQ-ben. Pár lekérdező operátor erre támaszkodik, mint pl. Take, Skip és Reverse.

A Take az első x elemet veszi ki, a Skip elhagyja, míg a Reverse megfordítja az összes sorrendjét:

int[] numbers = { 10, 9, 8, 7, 6 }; IEnumerable firstThree = numbers.Take(3); // { 10, 9, 8 } IEnumerable lastTwo = numbers.Skip(3); // { 7, 6 } IEnumerable reversed = numbers.Reverse(); // { 6, 7, 8, 9, 10 }

A Where és Select megőrzi a sorozatunk eredeti sorrendjét. A LINQ ahol tudja, megtartja az kezdősorozat sorrendjét.

Egyéb operátorok

Nem minden operátor ad vissza egy sorozatot. Az egyelemes operátorok egy elemet vesznek ki a bemenő sorozatból. Pl. First, Last, Single, ElementAt :

int[] numbers = { 10, 9, 8, 7, 6 }; int firstNumber = numbers.First(); // 10 int lasNumber = numbers.Last(); // 6 int secondNumber = numbers.ElementAt(1); // 9 int lowestNumber = numbers.OrderBy(n => n).First(); // 6

Az aggregációs operátorok skaláris értéket adnak vissza (általában numerikusat):

int count = numbers.Count(); // 5 int min = numbers.Min(); // 6

A mennyiségi operátorok egy bool értéket:

bool hasTheNumberNine = numbers.Contains(9); // true bool hasMoreThanZeroElements = numbers.Any(); // true bool hasAnOddElement = numbers.Any(n => n % 2 == 1); // true

Mivel nem egy sorozatot ad vissza, több operátort már nem hívhatunk meg rajta, ezért általában a lánc végén szoktuk használni.

Néhány lekérdező operátornak két bemenő sorozatot kell megadnunk. Pl a Concat, ami az egyik sorozat után fűzi a másikat. Másik példa az Union , amely ugyanazt csinálja, de az azonos elemekből csak egy szerepel:

int[] seq1 = { 1, 2, 3 }; int[] seq2 = { 3, 4, 5 }; IEnumerable concat = seq1.Concat(seq2); // { 1, 2, 3, 3, 4, 5 } IEnumerable union = seq1.Union(seq2); // { 1, 2, 3, 4, 5 }

Comprehension lekérdezések

A C# szintaktikus gyorslehetőséget ad LINQ lekérdezések írásához a query comprehension szintaxis révén. Az előző fejezetben felhozott példa comprehension szintaxisban:

using System; using System.Collections.Generic; using System.Linq; class LinqDemo { static void Main() { string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" }; IEnumerable query = from n in names where n.Contains("a") // Elemek szűrése orderby n.Length // Elemek rendezése select n.ToUpper(); // Minden elem leképzése foreach (string name in query) Console.Write(name + "|"); } }
JAY|MARY|HARRY|

A comprehension lekérdezések egy frommal kezdődnek és egy select vagy group típusú lekérdezéssel zárul. A from egy iterációs változó, esetünkben n, amit úgy lehet felfogni, mint a bemenő gyűjtemény végigfutása – úgymint foreach.

Az comprehension lekérdezéseket a fordító lamdba szintaxisra hozza. Eléggé mechanikus módon csinálja – hasonlóan, mint a foreach kifejezéseket GetEnumerator és MoveNext hívásokra alakítja. Ez azt jelenti, hogy amit megírunk comprehension szintaxisban, megírható lamdba szintaxisban is. A forditó az alábbi formára hozza a példát:

IEnumerable query = names.Where (n => n.Contains("a")) .OrderBy (n => n.Length) .Select (n => n.ToUpper());

Iterációs változók

A from kulcsszót követő azonosítót hívjuk így (példánkban n). Esetünkben a változó más sorozaton megy végig minden egyes hívásban:

from n in names // n az iterációs változónk where n.Contains("a") // n = egyenesen a tömbből orderby n.Length // n = a szűrt n select n.ToUpper() // n = a rendezetett n

Ez akkor lesz tiszta, ha megvizsgáljuk a fordító mechanikus fordítását lambda szintaxisra:

names.Where (n => n.Contains("a") // n saját hatákörében .OrderBy (n => n.Length) // n saját hatákörében .Select (n => n.ToUpper()) // n saját hatákörében

Így az iterációs változó mindig a megelőző kifejezés eredményein fut végig. Az egyetlen kivétel a szabály alól, ha egy kifejezés egy új változót vezet be. Három eset lehetséges:

Comprehension szintaxis vs. SQL szintaxis

A LINQ comprehension szintaxis látszólag úgy néz ki, mint az SQL szintaxisa, de a kettő nagyon is különbözik. LINQ lekérdezés C# kifejezés besűrítése, azaz a C# szabályait követi. Például a LINQ-ben nem használható változó a deklarációja előtt. SQL-ben a SELECT kifejezésnél egy tábla alias-ra hivatkozunk, mielőtt definiálnánk egy from kifejezésben.

Egy allekérdezés a LINQ-ben csupán még egy C# kifejezés és nincs szüksége speciális C# szintaxisra. SQL-ben az allekérdezésekre specális szabályok érvényesek.

A LINQ-kel az adatok balról jobbra haladnak a lekérdezésben. SQL-ben a sorrend többnyire véletlen.

Comprehension szintaxis vs. Lambda szintaxis

Mind a lambda mind a comprehension szintaxisnak megvan a maga előnye.

A comprehension szintaxis sokkal egyszerűbb, ami az alábbiakat foglalja magába:

A középső része a lekérdezéseknek, amelyekben Where, OrderBy és Select van. Mindegyik szintaxis jól működik; a választás az egyénen múlik.

Egy operátort használó lekérdezéshez a lambda szintaxis rövidebb és kevésbé zsúfolt.

Kevert szintaxisú lekérdezések

Ha egy lekérdező operátor nem támogatja a comprehension szintaxist, akkor keverhetjük a két szintaxist. Az egyetlen megkötés a comprehension szintaxis teljessége (emlékeztetőül: from-mal kezdődik és select-re vagy group-ra végződik)

Feltételezzük az alábbi tömböt:

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

Az alábbi példa megszámolja azoknak a neveknek a számát, melyek tartalmaznak ’a’ betűt:

int matches = (from n in names where n.Contains("a") select n).Count(); // 3

A következő lekérdezés pedig ábécé sorrendben visszaadja az első nevet:

int first = (from n in names orderby n select n).First(); // Dick

A kevert szinaxisú megközelítés néhány esetben hasznosabb az összetettebb lekérdezésekben. Az alábbi egyszerű példában lambda szintaxishoz kapcsolhatjuk bármilyen hátrányos következmény nélkül:

int matches = names.Where(n => n.Contains("a")).Count(); // 3 int first = names.OrderBy(n => n.First(); // Dick

Késleltetett végrehajtás

Az egyik legfontosabb tulajdonság, hogy a lekérdező operátorok nagy része nem akkor kerül végrehajtásra, amikor elkészítjük, hanem akkor, amikor bejárjuk (vagyis amikor meghívjuk rajta a MoveNext-et). Nézzük az alábbi lekérdezést:

var numbers = new List(); numbers.Add(1); IEnumerable query = numbers.Select(n => n * 10); // Lekérdezés elkészítése numbers.Add(2); // Még egy elem becsempészése foreach(int n in query) Console.Write(n + "|"); // 10|20|

A plusz szám, amit belecsempésztünk a listába a lekérdezés építése után, benne van az eredményben, mivel a foreach lekérdezés vagy bármilyen rendező illetve szűrő parancs előtt van. Ezt hívjuk késleltetett vagy lusta kiértékelésnek. Majdnem mindegyik standard lekérdező operátornak van késleltetett végrehajtása, néhány kivétellel:

Ezek egyből végrehajtódnak a lekérdezésen, mivel a visszatérési típusuk miatt nem lehetséges késleltetett végrehajtás. Pl. a Count metódus egy sima integert ad vissza, amit azután nem lehet bejárni. Az alábbi lekérdezés egyből végrehajtódik:

int matches = numbers.Where(n => n < 2).Count(); // 1

A késleletett végrehajtás azért fontos, mert kettéválik a lekérdezés építés és végrehajtás. Ezzel lehetővé válik lekérdezés több lépésben történő építése és LINQ 2 SQL lekérdezések építése.

Újrakiértékelés

A késleltetett végrehajtásnak van másik következménye is: a késleltetett lekérdezés újra kiértékelődik, ha megint bejárjuk:

var numbers = new List() { 1, 2 }; IEnumerable query = numbers.Select(n => n * 10); foreach(int n in query) Console.Write(n + "|"); // 10|20| numbers.Clear(); foreach(int n in query) Console.Write(n + "|"); // semmi

Van pár ok, ami miatt az újrakiértékelés nem túl előnyös:

Az újrakiértékelést elkerülhetjük egy konverziós operátor hívásával, mint pl. a ToArray vagy ToList. ToArray: a lekérdezés a kimenetet egy tömbbe rakja; ToList: egy generikus listába másol:

var numbers = new List() { 1, 2 }; List int = timesTen = numbers .Select(n => n * 10) .ToList(); // Egyből végrehajtódik egy List-be numbers.Clear(); Console.Write(timesTen.Count); // még mindig 2

Külső változók

A késleltetett végrehajtásnak van egy mellékhatása. Ha a lekérdezés lambda kifejezése lokális változóra hivatkozik, akkor külső változó szemantika vonatkozik rá. Azaz ha később megváltozik az értéke, megváltozik a lekérdezés is:

int[] numbers = { 1, 2 }; int factor = 10; IEnumerable query = numbers.Select(n => n * factor); factor = 20; foreach(int n in query) Console.Write(n + "|"); // 20|40| !!

A késleltetett végrehajtás működése

A lekérdező operátorokat késleltetett végrehatást dekorátor sorozatok visszaadásával működtetik. A hagyományos gyűjteményosztállyal ellentétben (mint pl. egy tömb vagy egy láncolt lista), a dekorátor sorozatnak nincs támogatott struktúrája önmagában az elemek tárolására. Ehelyett becsomagol egy sorozatot, ami futási időben adtunk meg, amihez permanens hozzáköti. Amikor a dekorátortól adatot kérünk, neki cserében a csomagolt bemenő sorozattól kell adatot kérnie.

A Where meghívása csupán a dekorátor csomagoló sorozatot hoz létre a bemenő sorozatra referenciával, a lambda kifejezést, és pár egyéb argumentumot. A bemenő sorozat akkor lesz bejárva, amikor a dekorátor sorozat.

IEnumerable lessThanTen = new int[] { 5, 12, 3 }.Where(n => n < 10);

Amikor bejárjuk a lessThanTen-t, valójában a tömböt a Where dekorátorán keresztül tesszük.

Dekorátorok láncolása

A lekérdező operátorok láncolása dekorátorok rétegződését hozza létre. Nézzük az alábbi lekérdezést:

IEnumerable query = new int[] { 5, 12, 3 }.Where (n => n < 10) .OrderBy (n => n) .Select (n => n * 10);

Minden lekérdező operátor egy új dekorátort hoz létre, ami az előző sorozatot csomagolja be (olyan, mint egy Matrioska baba). Amikor bejárjuk a query -t, az eredeti tömbön fut a lekérdezés, és a dekorátorok rétegein vagy -láncán transzformálodik. A korábban említett példánkban a Select dekorátora az OrderBy dekorátorára hivatkozik, ami a Where dekorátorára hivatkozik, ami pedig az eredeti tömbre hivatkozik.

Allekérdezések

Egy allekérdezés egy lekérdezés egy másik lekérdezés lambda kifejezésében. Az alábbi példa egy allekérdezést használ a zenészek vezetékneveivel való rendezéshez:

string[] musos = { "David Gilmour", "Roger Waters", "Rick Wright" }; IEnumerable query = musos.OrderBy(m => m.Split().Last());

m.Split mindegyik stringet szavak gyűjteményévé konvertálja, amin meghívuk a Last operátort. A Last egy allekérdezés, a query a külső lekérdezésre hivatkozik.

Az allekérdezések engedélyezettek, mert bármilyen érvényes C# kifejezést használhatunk a lambda kifejezés jobb oldalán. Az allekérdezés csak egy újabb C# kifejezés. Ez azt jelenti, hogy az allekérdezések szabályai a lamdba kifejezések szabályainak következményei (és a lekérdező operátorok viselkedésének).

Egy allekérdezés az őt bezáró kifejezés saját hatáskörében van és a külső lambda argumentumra hivatkozhat (vagy iterációs változóra a comprehension szintaxisban).

Last egy nagyon egyszerű allekérdezés. A következő lekérdezés az összes stringet visszaadja, melyek hossza megegyezik a legrövidebb string hosszával:

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" }; IEnumerable outerQuery = names .Where (n => n.Length == names.OrderBy (n2 => n2.Length) .Select (n2 => n2.Length).First());
Tom, Jay

Ugyanez comprehension szintaxisban:

IEnumerable comprehension = from n in names where n.Length == (from n2 in names orderby n2.Length select n2.Length).First() select n;

Allekérdezések és késleltetett végrehatás

Egy elem vagy aggregációs operátor, mint pl. a First vagy Count egy allekérdezésben nem kényszeríti rá a külső lekérdezést azonnali végrehajtásra (a késleltetett végrehajtást továbbra is a külső lekérdezésnek tartja meg). Ez azért van, mert az allekérdezések indirekt hívottak (lokális lekérdezésnél egy delegálton keresztül vagy egy kifejezésfán keresztül egy interpretált lekérdezésben).

Kompozíciós stragégiák

Ebben a fejezetben 3 stratégiát tárgyalunk az összetettebb lekérdezésekhez:

Mindegyik láncolt stratégia, és futási időben ugyanazt produkálják.

Progresszív lekérdezés építés

A fejezet elején található példa:

var filtered = names .Where (n => n.Contains("a")); var sorted = filtered .OrderBy (n => n); var query = sorted .Select (n => n.ToUpper());

Több lehetséges előnye is van a progresszív lekérdezés-építésnek:

A progresszív megközelítés gyakran hasznos a comprehension lekérdezésekben. Képzeljük el, hogy egy szövegből el akarjuk távolítani az összes magánhangzót, és utána ábécé sorrendbe rakni azokat, melyeknek a hossza továbbra is nagyobb, mint 2. Lambda szintaxisban ez így írható meg:

IEnumerable query = names .Select (n => n.Replace("a", "")..Replace("e", "").Replace("i", "") .Replace("o", "").Replace("u", "")) .Where (n => n.Length > 2) .OrderBy(n => n);
{ "Dck", "Hrry", "Mry" }

Ezt comprehension szintaxisba átrakni problémás, mivel ez a szintaxis a where-orderby-select rendet követi annak érdekében, hogy a fordító felismerje. Ha megpróbáljuk átírni hasonlóan, az eredmény különböző lesz:

IEnumerable query = from n in names where n.Length > 2 orderby n select n.Replace("a", "").Replace("e", "").Replace("i", "") .Replace("o", "").Replace("u", "");
{ "Dck", "Hrry", "Jy", "Mry", "Tm" }

Szerencsére, több mód is van rá, hogy visszakapjuk az eredeti eredményt comprehension szintaxisban. Az első a progresszív lekérdezés:

IEnumerable query = from n in names select n.Replace("a", "").Replace("e", "").Replace("i", "") .Replace("o", "").Replace("u", ""); query = from n in query where n.Length > 2 orderby n select n;
{ "Dck", "Hrry", "Mry" }

Az into kulcsszó

Az into kulcsszó lehetőséget nyújt a lekérdezés „folytatására”, egy leképezés és egy rövidítés a progresszív lekérdezésben. Az előző lekérdezés újraírható into-val:

IEnumerable query = from n in names select n.Replace("a", "").Replace("e", "").Replace("i", "") .Replace("o", "").Replace("u", "") into noVowel where noVowel.Length > 2 orderby noVowel select noVowel;

Csak select vagy group után használható. Az into „újraindítja” a lekérdezést, lehetőséget adva egy új where, orderby és select kifejezésnek.

Lambda szintaxisban az into megfelelője egyszerűen az operátorok egy hosszabb lánca.

Hatóköri szabályok:

Az összes lekérdező változó az into után a hatókörön kívűlre kerül. A következő nem fog fordulni:

var query = from n1 in names select n1.ToUpper() into n2 // Innentől csak az n2 látszik. where n1.Contains("x") // Illegális: n1 nincs a hatókörben. select n2;

Hogy lássuk miért, nézzük meg, hogy lambda szintaxisban hogy néz ki:

var query = names .Select (n1 => n1.ToUpper()) .Where (n2 => n1.Contains("x")); // Hiba: n1 nincs már a hatókörben

Az eredeti név (n1) már nincs , amikor a Where fut. A Where bemenete csak a nagybetűs változatot tartalmazza, szóval nem tud az n1 alapján szűrni.

Lekérdezések csomagolása

A progresszíven épített lekérdezést egy kifejezésbe is besűríthetjük az egyik lekérdezés másikba csomagolásával. Általában véve:

var tempQuery = tempQueryExpr var finalQuery = from ... in tempQuery ...

átírható:

var finalQuery = from ... in (tempQueryExpr)

A burkolás szemantikusan ugyanaz, mint a progresszív lekérdezés vagy az into kulcsszavas használat (átmeneti változó nélkül). A végeredmény minden esetben lekérdező operátorok lánca. Pl. nézzük a következő lekérdezést:

IEnumerable query = from n in names select n.Replace("a", "").Replace("e", "").Replace("i", "") .Replace("o", "").Replace("u", "") query = from n in query wheren.Length > 2 orderby n select n;

Csomagolt formába átírva:

IEnumerable query = from n1 in ( from n2 in names select n2.Replace("a", "").Replace("e", "").Replace("i", "") .Replace("o", "").Replace("u", "") ) where n1.Length > 2 orderby n1 select n1;

Amikor lambda szintaxisá konvertálódik, az eredmény továbbra is operátorok láncolata, mint az előző példában:

IEnumerable query = names .Select (n => n.Replace("a", "").Replace("e", "").Replace("i", "") .Replace("o", "").Replace("u", "")) .Where (n => n.Length > 2) .OrderBy (n => n);

A csomagolt lekérdezések megtévesztőek lehetnek, mert hasonlítanak az allekérdezésekhez. Mindkettő közös koncepciója, hogy van egy külső illetve egy belső lekérdezés. Lambda szintaxisra konvártálásnál látható, hogy a csomagolás csak egy stratégia az operátorok láncolásához. Az eredmény nem hasonlít egy allekérdezéshez, amely eredetilegi magában foglal egy belső lekérdezést egy másik lambda kifejezésen belül.

Leképezési stratégiák

Eddig a select kifejezések skaláris típust képeztek le. C# objektum inicializálókkal sokkal összetettebb típusokba képezhetünk le. Pl. tételezzük fel, hogy a lekérdezés első részében a magánhangzókat akarjuk kivágni egy névlistából, emellett megtartjuk a régi listát is. Segítségképpen írjuk meg az alábbi osztályt:

class TempProjectionItem { public string Original; // Eredeti név public string Vowelless; // Magánhangzó nélküli név }

és ezután képezzük ebbe az objektum inicializátorral:

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" }; IEnumerable temp = select new TempProjectionItem { Original = n, Vowelless = n.Replace("a", "").Replace("e", "").Replace("i", "") .Replace("o", "").Replace("u", "")) };

Az eredmény IEnumerable típusú, amiben lekérdezhetünk:

IEnumerable query = from item in temp where item.Vowelless.Length > 2 select item.Original;
Dick Harry Mary

Névtelen típusok

A névtelen típusokkal azonnal kaphatunk eredményt anélkül, hogy bármilyen speciális osztályt definiálnánk. Elhagyhatjuk a TempProjectionItem osztályunkat az előző példából:
var intermediate = select new { Original = n, Vowelless = n.Replace("a", "").Replace("e", "").Replace("i", "") .Replace("o", "").Replace("u", "")) }; IEnumerable query = from item in intermediate where item.Vowelless.Length > 2 select item.Original;

Ugyanazt az eredményt kapjuk, mint az előbbi esetben, de nem kell megírni egy felesleges osztályt. A fordító elvégzi a munkát úgy, hogy egy ideiglenes osztályt ír ugyanolyan stuktúrával, mint a leképezés. Ez azt jelenti, hogy az átmeneti leképezés az alábbi típusú lesz:

IEnumerable

Csak a var kulcsszóval tudunk létrehozni ilyen típusú változót. Esetünkben a var több mint zsúfoltságcsökkentés, ez szükséges.

Megírhatjuk a lekérdezést még tömörebben az into kulcsszóval:

var query = from n in names select { Original = n, Vowelless = n.Replace("a", "").Replace("e", "").Replace("i", "") .Replace("o", "").Replace("u", "") } into temp where temp.Vowelless.Length > 2 select temp.Original;

A query comprehension szintaxissal egyszerűbben tudunk ilyen típusú lekérdezést írni: let kulcsszóval.

A let kulcsszó

A let egy új változót vezet be az iterációs változó mellett.

a let-tel a magánhangzómentes, legalább kettő hosszú stringeket így irhatuk meg:

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" }; IEnumerable query = from n in names let voweless = n.Replace("a", "").Replace("e", "").Replace("i", "") .Replace("o", "").Replace("u", "") where voweless.Length > 2 orderby voweless select n; // A let-nek köszönhetően n még mindig a hatókörben van

A fordító a let-et egy névtelen típusba képzi le, amely tartalmazza az iterációs változót és ezt az új változót is. Más szóval az előbbi példába transzformálja a fordító a lekérdezést.

A let a következő kettőt teljesíti:

A let-es megközelítés különösen hasznos ebben a példában, mert megengedi a select kifejezésben az eredeti nevek leképezését (n) és a magánhangzó nélkülit is.

Bármennyi let-et lehet használni a where kifejezés előtt. A let kifejezés hivatkozhat egy korábbi let-ben definiált változóra. A let újraképezi a már létező változókat láthatatlanul.

Interpretált lekérdezések

A LINQ-ben két párhuzamos architektúra van: lokális lekérdezések lokális objektum gyűjtemények számára illetve interpretált lekérdezések távoli data source-ok számára. Eddig a lokális résszel foglalkoztunk, ami az IEnumerable<> interfészt implementáló objektumokról szólt. A delegáltak, amiket elfogadtak (legyen az comprehension sztinaxis, lambda szintaxis, vagy hagyományos delegáltak) teljesen lokális IL kód volt, mint bármely másik C# metódus.

Ezzel ellentétben az interpretált lekérdezések leíró elemek. Olyan sorozatokkal operálnak, melyek az IQueryable<> generikus interfészt implementálják, és az operátorokat a Queryable osztályból veszik, amelyek kifejezésfákat adnak vissza futásidőben interpretálva.

Kétféle implementációja van az IQueryable-nak a .NET Frameworkben:

Emellett az AsQueryable kiterjesztett metódus egy IQueryable csomagolót generál a megszokott bejárható gyűjtemény köré.

Tegyük fel, hogy generálunk egy egyszerű Customer táblát az SQL adatbázisunkba, és feltöltjük nevekkel:

create table Customer { ID int not null primary key, Name varchar(30) } insert into Customer values (1, 'Tom') insert into Customer values (2, 'Dick') insert into Customer values (3, 'Harry') insert into Customer values (4, 'Mary') insert into Customer values (5, 'Jay')

E tábla birtokában már irhatunk interpretált LINQ lekérdezést C#-ban, hogy megkapjuk az ’a’-t tartalmazó neveket:

using System using System.Linq; using System.Data.Linq; using System.Data.Linq.Mapping; [Table] public class Customer { [Column(IsPrivateKey=true)] public in ID; [Column] public string name; } class Test { static void Main() { DataContext dataContext = new DataContext("connection string"); Table customers = dataContext.GetTable (); IQueryable query = from c in customers where c.Name.Contains("a") orderby c.Name.Length select c.Name.ToUpper(); foreach (string name in query) Console.WriteLine (name); } }

A LINQ 2 SQL a lekérdezést az alábbi SQL-be generálja:

SELECT UPPER([t0].[Name]) AS [value] FROM [Customer] AS [t0] WHERE [t0].[Name] LIKE '%a%' ORDER BY LEN([t0],[Name])

Ezzel az eredménnyel:

JAY MARY HARRY

Az interpretált lekérdezések működése

Nézzük meg, miként működik az előző lekérdezés.

Először is a fordító a comprehension-ből lambda szintaxisra konvertálja. Úgyanúgy csinálja, mint helyi lekérdezéseknél:

IQueryable query = customers.Where (n => n.Name.Contains("a")) .OrderBy (n => n.Name.Length) .Select (n => n.Name.ToUpper());

Következő lépésben feloldja a lekérdező operátorok metódusait. Ez az a pont, amiben a lokális és a távoli lekérdezések különböznek (az interpretált lekérdezések a Queryable class-ból kapják a metódusaikat az Enumerable osztály helyett).

Ahhoz, hogy lássuk, meg kell nézni a customers változót, a forrást, ahonnan az egész lekérdezés épít. A customers Table<> típusú, ami az IQueryable<> interfészt implementálja (az IEnumerable<> résztípusa). Ez azt jelenti, hogy a fordítónak megvan a lehetősége a Where befogadására: meghívhatja a kiterjesztett metódust az Enumerable-ből vagy az alábbi kiterjesztett metódust a Queryable-ből:

public static IQueryable Where (this IQueryable source, Expression > predicate)

A fordító a Queryable.Where-t választja, mert szignatúrája jobban egyezik.

Interpretált és lokális lekérdezések keverése

Egy lekérdezésben lehet mindkét féle operátor. Tipikus mintái a külső lokális operátorok és a belsők intepretáltak. Más szóval a lokális lekérdezéseket az interpretáltakkal töltjük meg. Ez a minta jól működik a LINQ 2 SQL lekérdezéseknél.

Például, ha egy saját kiterjesztett metódust akarunk írni stringek párosítására:

public string IEnumerable Pair(this Enumerable source) { string firstHalf = null; foreach(string element in source) { if (firstHalf == null) firstHalf = element; else { yield return firstHalf + ", " + element; firstHalf = null; } } }

Ezt a metódust felhasználhatjuk egy kevert LINQ 2 SQL és lokális operátorokból álló lekérdezésben:

DataContext dataContext = new DataContext("connection string"); Table customers = dataContext.GetTable(); IEnumerable q = customers .Select(c => c.Name.ToUpper()) .Pair() // Ettől a ponttól lokális .OrderBy(n => n); foreach(string element in q) Console.WriteLine(element);
HARRY, MARY TOM, DICK

Mivel a customers IQueryable<> interfészt implementálja, ezért a Select operátor a Queryable.Select-et oldja fel. Ez egy IQueryable típusú kimenő sorozatot ad vissza. De a Pair operátornak nincs IQueryable<>-t implementáló túlterhelése (csak a kevésbé specifikus IEnumerable<>). Tehát a lokális Pair metódust oldja fel, becsomagolva ezzel egy lokális metódusba a lekérdezést. A Pair IEnumerable-t ad vissza, tehát az OrderBy szintén egy lokális operátorba csomagolódik.

A LINQ 2 SQL oldalon az SQL kifejezés:

SELECT UPPER (Name) FROM Customer

A többi munka lokálisan történik.

AsEnumerable

Enumerable.AsEnumerable a legegyszerűbb lekérdező operátor. Itt a teljes definíciója:
public static IEnumerable AsEnumerable (this IEnumerable source) { return source; }

A célja, hogy egy IQueryable sorozatot IEnumerable-vé konvertáljon, amivel a belső operátorokra az Enumerable operátorokat erőlteti rá a Queryable helyett. Ezzel a lekérdezés megmaradt részét helyben hajtja végre.

LINQ 2 SQL

LINQ 2 SQL Entity osztályok

A LINQ 2 SQL-ben bármilyen osztályt felhasználhatunk adatok reprezentálására, ha megfelelő attribútumokkal szereljük fel őket. Egy egyszerű példa:

[Table] public class Customer { [Column(IsPrivateKey=true)] public int ID; [Column] public string Name; }

A [Table] attibútum a System.Data.Linq.Mapping névtérben található, megmondja a LINQ 2 SQL-nek, hogy egy ilyen típusú objektum reprezentál egy sort az adatbázis táblából. Alapértelmezetten azt feltételezi, hogy a tábla neve megegyezik az osztály nevével. Hogyha nem így akarjuk, megadhatjuk az alábbi módon:

[Table (Name="Customer")] public string Name;

Egy class a [Table] attribútummal fémjelezve egy entitás a LINQ 2 SQL-ben. Hogy használható legyen, a struktúrája közel (vagy teljesen) meg kell egyezzen az adatbázis táblájával.

A [Column] attribútum egy mező vagy property, ami a tábla egy oszlopára mutat. Szintén meg lehet adni eltérő névvel:

[Column (Name="FullName")] public string Name;

A [Column] IsPrimaryKey propertyvel lehet utalni a tábla elsődleges kulcsára, emellett kell az objektum azonosításához, illetve a változott adatok adatbázisba történő visszaírásához.

Publikus mezők definiálása helyett publikus propertyket használunk privát mezőkkel. Ezzel lehet validációs logikát írni a property elérésekbe. Ha ezt az utat választjuk, tájékoztathatjuk a LINQ 2 SQL-t a property elérés felülírására, hogy egyből a privát mezőbe írjuk az adatbázisból:

string _name; [Column (Storage="_name")] public string Name { get { return _name; } set { _name = value; } }

A Column(Storage=”_name”) mondja meg a LINQ 2 SQL-nek, hogy egyből a _name mezőbe írja (a Name property helyett) amikor feltölti az entitást. LINQ 2 SQL reflection-t használ, tehát a mező lehet privát.

DataContext

Miután definiáltuk az entitás osztályokat, a lekérdezést egy DataContext objektum példányosítása után a GetTable hívással kezdhetjük meg. A következő példa az előbb definiált Customer osztályt használja fel:

DataContext dataContext = new DataContext("connection string"); Table customers = dataContext.GetTable(); Console.WriteLine(customers.Count()); // a tábla sorainak száma Customer cust = customers.Single(c => c.ID == 2); // a 2-es ID-jű Customer

A DataContext objektum két dolgot csinál: Először egy factory-ként működik, ami legenerálja a táblákat, melyekből lekérdezhetünk. Másodszor nyomon követi a változtatásokat az entitásokban, amit vissza lehet majd írni:

DataContext dataContext = new DataContext("connection string"); Table customers = dataContext.GetTable(); Customer cust = customers.OrderBy(c => c.Name).First(); cust.Name = "Updated Name"; dataContext.SubmitChanges();

Automatikus entitás generálás

Mivel a LINQ 2 SQL entitás osztályoknak követniük kell az alattuk fekvő táblát, ezért automatikusan szeretnénk generálni őket egy meglévő adatbázis sémából. Erre lehetőség van SqlMetal parancssorából illetve a Visual Studio LINQ 2 SQL designeréből. Az entitásokat részleges osztályokként generálja le, így plusz logikát is írhatunk hozzájuk.

Pluszként kapunk egy erősen típusos DataContext osztályt. Ez csak egy leszármaztatott DataContext osztály az összes entitás típus tábláját visszaadó property-kel. Megspórolható vele a GetTable:

var dataContext = new MyTypedDataContext("connection string"); Table customers = dataContext.Customers; Console.WriteLine(customers.Count());

vagy egyszerűen:

Console.WriteLine(dataContext.Customers.Count());

A LINQ 2 SQL designer automatikusan többszörösíti az azonosítókat, ahol szükséges; a példánkban dataContext.Customers és nem dataContext.Customer.

Asszociációk

Az entitásgeneráló eszközök még egy hasznos munkát elvégeznek. Valamennyi összefüggéshez az adatbázisban property-k generálódnak automatikusan az összefüggés mindkét oldalán. Pl. nézzük a customer és purchase táblákat egy a többhöz relációval:

create table Customer ( ID int not null primary key, Name varchar(30) not null ) create table Purchase ( ID int not null primary key, CustomerID int references Customer (ID), Description varchar(30) not null, Price decimal not null )

Ha automatikusan generált entitásosztályokat használunk, a lekérdezést így kell megírni:

var dataContext = new MyTypedDataContext("connection string"); // Az első vásárló által kezdemnényezett összes váráslást visszaadjuk (ábécé sorrendben): Customer cust1 = dataContext.Customers.OrderBy(c => c.Name).First(); foreach (Purchase p in cust1.Purchases) Console.WriteLine(p.Price); // legolcsóbb cucc vásárlója: Purchase cheapest = dataContext.Purchases.OrderBy(p => p.Price).First(); Customer cust2 = cheapest.Customer;

Továbbá, ha a cust1 és cust2 ugyanarra a customer-re hivatkozik, a c1 és c2 ugyanarra az objektumra mutat, azaz cust1==cust2 igazat ad vissza.

Nézzük az automatikusan generált Purchases property-t a Customer entitáson:

[Asszociation(Storage="_Purchases", OtherKey="CustomerID")] public EntitySet Purchases { get {...} set {...} }

Az EntitySet olyan, mint egy előre definiált lekérdezés, egy beépített Where-rel, ami lekérdezi a hivatkozott entitásokat. Az [Association] attribútum adja a LINQ 2 SQL-nek az információkat a lekérdezés megírásához. Mint bármely másik lekérdezésben, a késleltetett végrehajtás lesz rá érvényes. Ez azt jelenti, hogy kerül végrehajtásra, amíg be nem járjuk a hivatkozott gyűjteményt.

Itt a Purchases.Customer property, a reláció másik oldalán:

[Association(Storage="_Customer",ThisKey="CustomerID",IsForeignKey=true)] public Customer Customer { get {...} set {...} }

Habár a property Customer típusú, a benne foglalt mező (_Customer) EntityRef típusú. Ez a típus késleltetett betöltést implementál, így a kapcsolódó Customer típus nem kerül lekérésre az adatbázisból, amíg mi nem kérjük.

Késleltetett végrehajtás LINQ 2 SQL-ben

A LINQ 2 SQL lekérdezések késleltetett végrehajtással működnek, ugyanúgy, mint a lokális lekérdezések. Ezért a lekérdezéseket írhatjuk progresszíven is. Azonban van egy aspektus, amiben a LINQ 2 SQL-nek speciális késleltetett végrehajtó szemantikája van, ez az, amikor egy allekérdezés van egy Select-en belül:

Például az alábbi lekérdezés egyetlen kör futásával eléri a foreach parancsot:

var dataContext = new MyTypedDataContext("connection string"); var query = from c in dataContext.Customers select from p in c.Purchases select new { c.Name, c.Price }; foreach (var customerPurchaseResults in query) foreach (var namePrice in customerPurchaseResults) Console.WriteLine(namePrice.Name + " spent " + namePrice.Price);

Bármilyen EntitySet, melyet explicit leképezünk, egy kör futásával teljesen feltöltődik:

var query = from c in dataContext.Customers select new { c.Name, c.Purchases }; foreach(var row in query) foreach( Purchase p in row.Purchases) // Nincs extra körök futása Console.WriteLine(row.Name + " spent " + p.Price);

De ha egy EntitySet property-t bejárunk, mielőtt leképezzük, a késleltetett végrehajtás szabályai érvényesülnek. A következő példában, a LINQ 2 SQL végrehajt még egy Purchases lekérdezést mindegyik ciklus iteráción:

foreach (Customer c in dataContext.Customers) foreach(Purchase p in c.Purchases) // Plusz egy SQL kör Console.WriteLine(c.Name + " spent " + p.Price);

DataLoadOptions

Két különböző használata van:

Szűrő meghatározása: Refaktoráljuk az előző példánkat:

foreach (Customer c in dataContext.Customers) if (myWebService.HasBadCreditHistory(c.ID)) ProcessCustomer(c);

Definiáljuk a ProcessCustomer-t így:

void ProcessCustomer(Customer c) { Console.WriteLine(c.ID + " " + c.Name); foreach(Purchase p in c.Purchases) Console.WriteLine(" - purchased a " + p.Description); }

Tegyük fel, hogy a ProcessCustomer-t mindegyik valamennyi vevő vásárolt tárgyainak csak egy részhalmazával akarjuk táplálni. Itt egy megoldás:

foreach (Customer c in dataContext.Customers) if (myWebService.HasBadCreditHistory(c.ID)) ProcessCustomer (c.ID, c.Name, c.Purchases.Where(p => p.Price > 1000)); ... void ProcessCustomer (int custID, string custName, IEnumerable purchases) { Console.WriteLine(custID + " " + custName); foreach (Purchase p in purchases) Console.WriteLine(" - purchased a " + p.Description); }

Ez így rendezetlen. Még rendetlenebb lesz, ha a ProcessCustomer még több Customer mezőt tartalmaz. Egy sokkal jobb megoldás a DataLoadOptions AssociateWith metódusának használata:

DataLoadOptions options = new DataLoadOptions(); options.AssociateWith (c => c.Purchases.Where(p => p.Price > 1000); dataContext.LoadOptions = options;

Ez a DataContext példányt arra utasítja, hogy a Customer Purchases-en mindig a megadott feltételt hajtsa végre. Most már használhatjuk az eredeti ProcessCustomer metódust. Az AssociateWith nem változtatja meg a késleltetett végrehajtás szemantikáját. Amikor egy bizonyos összefüggést használunk, azt mondja meg, hogy implicit hozzá kell adni egy bizonyos szűrőt az egyenlőséghez.

Mohó betöltés:

A DataLoadOptions második felhasználási lehetősége egyes EntitySet-ek mohó betöltésének állítása a szülein keresztül. Pl. be akarjuk tölteni az összes vevőt és vásárlásaikat egyetlen SQL körrel. Az alábbi pont ezt teszi:

DataLoadOptions options = new DataLoadOptions(); options.LoadWith (c => c.Purchases); dataContext.LoadOptions = options; foreach (Customer c in dataContext.Customers) // egy kör futása foreach (Purchase p in c.Purchases) Console.WriteLine(c.Name " bought a " + p.Description);

Amikor egy Customer-t lekérünk, a Purchases-ek is lekérésre kerülnek ugyanabban az időben. Sőt még az unokákat (v. gyerekek gyerekeit) is bevonhatjuk: //szülő-gyerek reláció SQLben

options.LoadWith (c => c.Purchases); options.LoadWith (p => p.PurchaseItems);

Lehet kombinálni a kettőt. Az alábbi lekérdezésben, amikor lekérünk egy vevőt, a magas árú vásárlásait akarjuk lekérni egy kör futásával:

options.LoadWith (c => c.Purchases); options.AssociateWith (c => c.Purchases.Where(p => p.Price > 1000));

Frissítések

A LINQ 2 SQL nyilvántartja a frissítéseket, melyeket végrehajtottunk az entitásokon, és lehetővé teszi, hogy visszaírjuk az adatbázisba a SubmitChanges metódussal a DataContext objektumon. A Table<> osztály InsertOnSubmit és DeleteOnSubmit metódusai teszik lehetővé sorok beszúrását illetve törlését a táblából. Itt van egy sor beszúrása:

var dataContext = new MyTypedDataContext("connection string"); Customer cust = new Customer { ID=1000, Name="Bloggs" }; dataContext.Customers.InsertOnSubmit(cust);

Később lekérdezhetjük, frissíthetjük, majd kitörölhetjük ezt a sort:

var dataContext = new MyTypedDataContext("connection string"); Customer cust = dataContext.Customers.Single(c => c.ID == 1000); cust.Name = "Bloggs2"; dataContext.SubmitChanges(); // frissiti a customer-t dataContext.Customers.DeleteOnSubmit(cust); dataContext.SubmitChanges(); // törli a customer-t

A DataContext.SubmitChanges összegyűjti az összes változtatást, amit végrehajtottunk az entitásainkon a DataContext létrehozása óta (vagy a legutolsó SubmitChanges óta), utána végrehajt egy SQL utasítást az adatbázisba íráshoz. Bármelyik TransactionScope-t elfogadja; ha nem volt egy sem, akkor az utasításokat egy új tranzakcióba csomagolja.

Az Add-dal is hozzáadhatunk új vagy már létező sorokat egy EntitySet-hez. A LINQ 2 SQL automatikusan legenerálja az idegen kulcsokat, amikor ezt használjuk:

Purchase p1 = new Purchase { ID=100, Description="Bike", Price=500 }; Purchase p1 = new Purchase { ID=101, Description="Tools", Price=100 }; Customer cust = dataContext.Customers.Single(c => c.ID == 1); cust.Purchases.Add(p1); cust.Purchases.Add(p2); dataContext.SubmitChanges(); // Berakja a két purchase-t

Amikor egy sort eltávolítunk egy EntitySet-ből, az idegen kulcs mezője automatikusan null-ra áll. Az alábbi példában szétválasztjuk a két vásárolt tárgyat a vevőjétől:

var dataContext = new MyTypedDataContext("connection string"); Customer cust = dataContext.Customers.Single(c => c.ID == 1); cust.Purchases.Remove(cust.Purchases.Single(p => p.ID == 100)); cust.Purchases.Remove(cust.Purchases.Single(p => p.ID == 101)); dataContext.SubmitChanges(); // SQL parancs submit

Mivel ezzel megpróbálja a két vásárolt tárgy CustomerID-jét null-ra állítani, a Purchase.CustomerID-nek nullable-nek kell lennie az adatbázisban, különben hibát kapunk. Ahhoz, hogy a gyerekeket teljesen töröljük, vegyük ki őket a Table<>-ből:

var dc = dataContext; dc.Purchases.DeleteOnSubmit(dc.Purchases.Single(p => p.ID == 100)); dc.Purchases.DeleteOnSubmit(dc.Purchases.Single(p => p.ID == 100)); dataContext.SubmitChanges(); // SQL parancs submit

Lekérdező kifejezések építése

Amikor dinamikusan akartunk lekérdezéseket építeni, azt lekérdező operátorok láncolásával oldottuk meg. Habár ez sok esetben elegendő, néha egy sokkal kifinomultabb szinten kell dolgoznunk, és dinamikusan kell azokat a lambda kifejezéseket kifejezéseket komponálnunk, amivel az operátorokat tápláljuk.

Ebben a fejezetben az alábbi Product osztállyal dolgozunk:

[Table] public partial class Product { [Column(IsPrimaryKey=true)] public int ID; [Column] public string Description; [Column] public bool Discontinued; [Column] public DateTime LastSale; }

Delegáltak vs. kifejezésfák

Emlékezzünk vissza:

A Where operátor szignatúrájának összehasonlításával láthatjuk a különbséget az Enumerable és Queryable között:

public static IEnumerable (this IEnumerable source, Func predicate) public static IEnumerable (this IQueryable source, Expression> predicate)

Amikor beágyazzuk egy lekérdezésbe, a lambda kifejezés ugyanúgy néz ki:

IEnumerable q1 = localProducts.Where(p => !p.Discontinued); IQueryable q2 = sqlProducts.Where (p => !p.Discontinued);

Viszont amikor egy lambda kifejezést egy közbenső változóhoz kötjük, explicit módon meg kell adnunk, hogy egy delegáltat (pl. Func<>) vagy egy kifejezésfát (pl. Expression>) adjon-e meg. Az alábbi példában a predicate1 és predicate2 nem cserélhető fel:

Func predicate1 = p => !p.Discontinued; IEnumerable q1 = localProducts.Where(predicate1); Expression> predicate2 = p => !p.Discontinued; IQueryable q2 = sqlProducts.Where(predicate2);

Kifejezésfák fordítása:

Egy kifejezésfát átkonvertálhatunk egy delegálttá a Compile metódus meghívásával. Ez különleges érték, amikor újrafelhasználható kifejezéseket visszaadó metódusokat írunk. Hogy ábrázoljuk, egy statikus metódust adunk a Product osztályhoz, ami egy predikátumot ad vissza, mely igazat ad, ha a termék nem Discontinued és az elmúlt 30 napban adták el:

public partial class Product { public static Expression> IsSelling() { return p => !p.Discontinued && p.LastSale > DateTime.Now.AddDays(-30); } }

Ezt a metódust mind interpretált, mind lokális lekérdezéseknél is használhatjuk:

void Test() { var dataContext = new MyTypedDataContext("connection string"); Product[] localProducts = dataContext.Products.ToArray(); IQueryable sqlQuery = dataContext.Products.Where(Product.IsSelling()); IEnumerable localQuery = dataContext.Where(Product,IsSelling.Compile()); }

AsQueryable:

Az AsQueryable operátorral teljes lekérdezéseket írhatunk, amit lokális illetve távoli sorozatokon is lefuttathatunk:

IQueryable FilterSortProducts (IQueryable input) { return from p in input where ... orderby ... select p; } void Test() { vard dataContext = new MyTypedDataContext("connection string"); Product[] localProducts = dataContext.Products.ToArray(); var sqlQuery = FilterSortProducts (dataContext.Products); var localQuery = FilterSortProducts (localProducts.AsQueryable()); ... }

Az AsQueryable IQueryable<> "ruhába" csomagol egy lokális lekérdezést így a későbbi lekérdező operátorok kifejezésfákká. Később, amikor bejárjuk az eredményt, a kifejezésfák implicit lefordítódnak, és a lokális sorozat normálisan lesz bejárva.

Kifejezésfák

Már említettük, hogy egy lambda kifejezés Expression-hez kötése egy kifejezésfát ad vissza. Némi programozási erőfeszítéssel elérhetjük ugyanezt saját kezűleg futás közben is (más szóval dinamikusan építhetünk egy kifejezésfát a semmiből). Az eredményt kasztolhatjuk Expression -re és használhatjuk egy LINQ 2 SQL lekérdezésben, vagy egy hétköznapi delegálttá fordíthatjuk a Compile meghívásával.

A kifejezés DOM:

A kifejezésfa egy miniatűr kód DOM. Mindegyik csúcs a fában egy System.Linq.Expressions névtérbeli típussal reprezentált.

Az bázis típusa mindegyik csúcsnak az Expression (nem generikus) osztály. A generikus Expression valódi jelentése „típusos lambda kifejezés” és LambdaExpression-nek kellett volna nevezni, ha nem lett volna ez túl esetlen:

LambdaExpression> f = ....

Az Expression<> alaptípusa a (nemgenerikus) LambdaExpression osztály. A LambdaExpression típusegységesítést nyújt a lambda kifejezésfáknak: bármelyik Expression<> típust LambdaExpression-né lehet kasztolni.

A dolog, ami megkülönböztet egy LambdaExpression-t egy hétköznapi Expression-től, hogy a lambda kifejezéseknek paramétereik vannak.

Ha egy kifejezésfát akarunk létrehozni, ne példányosítsunk csúcstípusokat közvetlenül; ehelyett hívjunk statikus metódusokat hívunk az Expression osztályból.

Építsünk egy kifejezést a semmiből. Az alapelv szerint a fa legaljáról indulunk és haladunk felfele. A legalsó elem a fában egy ParameterExpression, egy lambda kifejezés paramétere, melyet ’s’-nek hívunk:

ParameterExpression p = Expression.Parameter(typeof(string), "s");

A következő lépés, egy MemberExpression és ConstantExpression építése. Az előző esetben definiált ’s’-nek a Length property-jét akarjuk elérni:

MemberExpression stringLength = Expression.Property (p, "Length"); ConstantExpression five = Expression.Constant(5);

Következő lépés a LessThan összehasonlítás:

BinaryExpression comparison = Expression.LessThan(stringLength, five);

Az utolsó lépés a lambda kifejezés szerkesztése, ami egy kifejezés Body-ját a paraméterek gyűjteményéhez kapcsolja:

Expression> lambda = Expression.Lambda> (comparison, p);

Lambda kifejezés kényelmes delegálttá fordításának módja

Func runnable = lambda.Compile(); Console.WriteLine(runnable("kangoroo")); // False Console.WriteLine(runnable("dog")); // True