Psaní dotazů LINQ jazyka C# pro dotazování na data
Většina dotazů v úvodní dokumentaci k integrovanému dotazu jazyka (LINQ) je napsaná pomocí deklarativní syntaxe dotazů LINQ. Syntaxe dotazu však musí být při kompilaci kódu přeložena do volání metod modulu CLR (Common Language Runtime) .NET. Tato metoda volá volání standardních operátorů dotazu, které mají názvy, jako Where
jsou , Select
, GroupBy
, Join
, Max
a Average
. Můžete je volat přímo pomocí syntaxe metody místo syntaxe dotazu.
Syntaxe dotazů a syntaxe metody jsou sémanticky identické, ale syntaxe dotazu je často jednodušší a čitelnější. Některé dotazy musí být vyjádřeny jako volání metody. Například je nutné použít volání metody k vyjádření dotazu, který načte počet prvků, které odpovídají zadané podmínce. Musíte také použít volání metody pro dotaz, který načte prvek, který má maximální hodnotu ve zdrojové sekvenci. Referenční dokumentace pro standardní operátory dotazů v System.Linq oboru názvů obecně používá syntaxi metody. Měli byste se seznámit s používáním syntaxe metod v dotazech a v samotných výrazech dotazů.
Metody rozšíření standardního operátoru dotazu
Následující příklad ukazuje jednoduchý výraz dotazu a sémanticky ekvivalentní dotaz napsaný jako dotaz založený na metodě.
int[] numbers = [ 5, 10, 8, 3, 6, 12 ];
//Query syntax:
IEnumerable<int> numQuery1 =
from num in numbers
where num % 2 == 0
orderby num
select num;
//Method syntax:
IEnumerable<int> numQuery2 = numbers.Where(num => num % 2 == 0).OrderBy(n => n);
foreach (int i in numQuery1)
{
Console.Write(i + " ");
}
Console.WriteLine(System.Environment.NewLine);
foreach (int i in numQuery2)
{
Console.Write(i + " ");
}
Výstup z těchto dvou příkladů je shodný. Vidíte, že typ proměnné dotazu je stejný v obou formách: IEnumerable<T>.
Abychom pochopili dotaz založený na metodách, pojďme ho podrobněji prozkoumat. Na pravé straně výrazu si všimněte, že where
klauzule je nyní vyjádřena jako metoda instance objektu numbers
, který má typ IEnumerable<int>
. Pokud znáte obecné IEnumerable<T> rozhraní, víte, že nemá metodu Where
. Pokud však vyvoláte seznam dokončování IntelliSense v integrovaném vývojovém prostředí sady Visual Studio, uvidíte nejen metodu Where
, ale mnoho dalších metod, jako Select
je , SelectMany
, Join
a Orderby
. Tyto metody implementují standardní operátory dotazu.
I když to vypadá, jako by IEnumerable<T> obsahovalo více metod, tak to není. Standardní operátory dotazů se implementují jako rozšiřující metody. Metody rozšíření "rozšiřují" existující typ; lze je volat, jako by se jednalo o metody instance typu. Standardní operátory dotazu rozšiřují IEnumerable<T> a proto můžete psát numbers.Where(...)
.
Pokud chcete použít rozšiřující metody, přenesete je do oboru direktivami using
. Z pohledu vaší aplikace je metoda rozšíření a běžná metoda instance stejné.
Další informace o metodách rozšíření naleznete v tématu Metody rozšíření. Další informace o standardních operátorech dotazů najdete v tématu Přehled operátorů standardních dotazů (C#). Někteří poskytovatelé LINQ, jako je Entity Framework a LINQ to XML, implementují vlastní standardní operátory dotazů a rozšiřující metody pro jiné typy kromě IEnumerable<T>.
Výrazy lambda
V předchozím příkladu si všimněte, že podmíněný výraz (num % 2 == 0
) je předán jako vložený argument Enumerable.Where metodě: Where(num => num % 2 == 0).
Tento vložený výraz je výraz lambda. Je to pohodlný způsob, jak napsat kód, který by jinak musel být napsán v složitější podobě. Nalevo num
od operátoru je vstupní proměnná, která odpovídá num
ve výrazu dotazu. Kompilátor může odvodit typ, num
protože ví, že numbers
je to obecný IEnumerable<T> typ. Tělo výrazu lambda je stejné jako výraz v syntaxi dotazu nebo v jakémkoli jiném výrazu nebo příkazu jazyka C#. Může zahrnovat volání metod a další složitou logiku. Vrácená hodnota je pouze výsledek výrazu. Některé dotazy lze vyjádřit pouze v syntaxi metody a některé z nich vyžadují výrazy lambda. Výrazy lambda jsou výkonným a flexibilním nástrojem v sadě nástrojů LINQ.
Kompozičnost dotazů
V předchozím příkladu Enumerable.OrderBy kódu je metoda vyvolána pomocí tečkového operátoru volání Where
. Where
vytvoří filtrovanou sekvenci a potom Orderby
seřadí sekvenci vytvořenou .Where
Vzhledem k tomu, že dotazy vrací IEnumerable
, vytvoříte je v syntaxi metody zřetězeným volání metody. Kompilátor toto složení provede při psaní dotazů pomocí syntaxe dotazu. Vzhledem k tomu, že proměnná dotazu neukládá výsledky dotazu, můžete ji kdykoli upravit nebo použít jako základ pro nový dotaz, a to i po jeho spuštění.
Následující příklady demonstrují některé jednoduché dotazy LINQ pomocí každého dříve uvedeného přístupu.
Poznámka:
Tyto dotazy pracují s jednoduchými kolekcemi v paměti; Základní syntaxe je však shodná s syntaxí, která se používá v LINQ to Entities a LINQ to XML.
Příklad – syntaxe dotazu
Většinu dotazů se syntaxí dotazu napíšete za účelem vytváření výrazů dotazu. Následující příklad ukazuje tři výrazy dotazu. První výraz dotazu ukazuje, jak filtrovat nebo omezit výsledky použitím podmínek s klauzulí where
. Vrátí všechny prvky ve zdrojové sekvenci, jejichž hodnoty jsou větší než 7 nebo menší než 3. Druhý výraz ukazuje, jak uspořádat vrácené výsledky. Třetí výraz ukazuje, jak seskupit výsledky podle klíče. Tento dotaz vrátí dvě skupiny založené na prvním písmenu slova.
List<int> numbers = [5, 4, 1, 3, 9, 8, 6, 7, 2, 0];
// The query variables can also be implicitly typed by using var
// Query #1.
IEnumerable<int> filteringQuery =
from num in numbers
where num is < 3 or > 7
select num;
// Query #2.
IEnumerable<int> orderingQuery =
from num in numbers
where num is < 3 or > 7
orderby num ascending
select num;
// Query #3.
string[] groupingQuery = ["carrots", "cabbage", "broccoli", "beans", "barley"];
IEnumerable<IGrouping<char, string>> queryFoodGroups =
from item in groupingQuery
group item by item[0];
Typ dotazů je IEnumerable<T>. Všechny tyto dotazy lze napsat pomocí následujícího var
příkladu:
var query = from num in numbers...
V každém předchozím příkladu se dotazy ve skutečnosti neprovedou, dokud ne iterujete proměnnou foreach
dotazu v příkazu nebo jiném příkazu.
Příklad – syntaxe metody
Některé operace dotazu musí být vyjádřeny jako volání metody. Nejběžnějšími metodami jsou metody, které vracejí jednoúčelové číselné hodnoty, například Sum, Max, Min, Averageatd. Tyto metody musí být vždy volána jako poslední v jakémkoli dotazu, protože vracejí jednu hodnotu a nemůžou sloužit jako zdroj pro další operaci dotazu. Následující příklad ukazuje volání metody ve výrazu dotazu:
List<int> numbers1 = [5, 4, 1, 3, 9, 8, 6, 7, 2, 0];
List<int> numbers2 = [15, 14, 11, 13, 19, 18, 16, 17, 12, 10];
// Query #4.
double average = numbers1.Average();
// Query #5.
IEnumerable<int> concatenationQuery = numbers1.Concat(numbers2);
Pokud metoda má System.Action nebo System.Func<TResult> parametry, jsou tyto argumenty uvedeny ve formě výrazu lambda, jak je znázorněno v následujícím příkladu:
// Query #6.
IEnumerable<int> largeNumbersQuery = numbers2.Where(c => c > 15);
V předchozích dotazech se provede okamžitě pouze dotaz č. 4, protože vrátí jednu hodnotu, a ne obecnou IEnumerable<T> kolekci. Samotná metoda používá foreach
nebo podobný kód k výpočtu jeho hodnoty.
Každý z předchozích dotazů lze zapsat pomocí implicitního psaní var
pomocí , jak je znázorněno v následujícím příkladu:
// var is used for convenience in these queries
double average = numbers1.Average();
var concatenationQuery = numbers1.Concat(numbers2);
var largeNumbersQuery = numbers2.Where(c => c > 15);
Příklad – syntaxe smíšených dotazů a metod
Tento příklad ukazuje, jak používat syntaxi metody pro výsledky klauzule dotazu. Stačí uzavřít výraz dotazu do závorek a pak použít tečkovací operátor a volat metodu. V následujícím příkladu vrátí dotaz č. 7 počet čísel, jejichž hodnota je mezi 3 a 7. Obecně je ale lepší použít druhou proměnnou k uložení výsledku volání metody. Tímto způsobem je méně pravděpodobné, že se dotaz zaměňuje s výsledky dotazu.
// Query #7.
// Using a query expression with method syntax
var numCount1 = (
from num in numbers1
where num is > 3 and < 7
select num
).Count();
// Better: Create a new variable to store
// the method call result
IEnumerable<int> numbersQuery =
from num in numbers1
where num is > 3 and < 7
select num;
var numCount2 = numbersQuery.Count();
Vzhledem k tomu, že dotaz č. 7 vrátí jednu hodnotu, a ne kolekci, spustí se dotaz okamžitě.
Předchozí dotaz lze zapsat pomocí implicitního psaní var
následujícím způsobem:
var numCount = (from num in numbers...
Dá se napsat v syntaxi metody následujícím způsobem:
var numCount = numbers.Count(n => n is > 3 and < 7);
Lze ho napsat pomocí explicitního psaní následujícím způsobem:
int numCount = numbers.Count(n => n is > 3 and < 7);
Dynamické určení filtrů predikátu za běhu
V některýchpřípadechch where
Jedním ze způsobů, jak dynamicky zadat více predikátových filtrů, je použít metodu Contains , jak je znázorněno v následujícím příkladu. Dotaz vrátí různé výsledky na základě hodnoty id
při spuštění dotazu.
int[] ids = [111, 114, 112];
var queryNames =
from student in students
where ids.Contains(student.ID)
select new
{
student.LastName,
student.ID
};
foreach (var name in queryNames)
{
Console.WriteLine($"{name.LastName}: {name.ID}");
}
/* Output:
Garcia: 114
O'Donnell: 112
Omelchenko: 111
*/
// Change the ids.
ids = [122, 117, 120, 115];
// The query will now return different results
foreach (var name in queryNames)
{
Console.WriteLine($"{name.LastName}: {name.ID}");
}
/* Output:
Adams: 120
Feng: 117
Garcia: 115
Tucker: 122
*/
Příkazy toku řízení, například if... else
nebo switch
, můžete použít k výběru mezi předem určenými alternativními dotazy. V následujícím příkladu používá jinou klauzuli, studentQuery
pokud je hodnota oddYear
runtime nebo true
false
.where
void FilterByYearType(bool oddYear)
{
IEnumerable<Student> studentQuery = oddYear
? (from student in students
where student.Year is GradeLevel.FirstYear or GradeLevel.ThirdYear
select student)
: (from student in students
where student.Year is GradeLevel.SecondYear or GradeLevel.FourthYear
select student);
var descr = oddYear ? "odd" : "even";
Console.WriteLine($"The following students are at an {descr} year level:");
foreach (Student name in studentQuery)
{
Console.WriteLine($"{name.LastName}: {name.ID}");
}
}
FilterByYearType(true);
/* Output:
The following students are at an odd year level:
Fakhouri: 116
Feng: 117
Garcia: 115
Mortensen: 113
Tucker: 119
Tucker: 122
*/
FilterByYearType(false);
/* Output:
The following students are at an even year level:
Adams: 120
Garcia: 114
Garcia: 118
O'Donnell: 112
Omelchenko: 111
Zabokritski: 121
*/
Zpracování hodnot Null ve výrazech dotazů
Tento příklad ukazuje, jak zpracovat možné hodnoty null ve zdrojových kolekcích. Kolekce objektů, jako IEnumerable<T> je například, může obsahovat prvky, jejichž hodnota je null. Pokud zdrojová kolekce je null
nebo obsahuje prvek, jehož hodnota je null
a váš dotaz nezpracuje null
hodnoty, NullReferenceException vyvolá se při spuštění dotazu.
Můžete kódovat defenzivně, abyste se vyhnuli výjimce nulového odkazu, jak je znázorněno v následujícím příkladu:
var query1 =
from c in categories
where c != null
join p in products on c.ID equals p?.CategoryID
select new
{
Category = c.Name,
Name = p.Name
};
V předchozím příkladu where
klauzule vyfiltruje všechny prvky null v sekvenci kategorií. Tato technika je nezávislá na vrácení hodnoty null v klauzuli join. Podmíněný výraz s hodnotou null v tomto příkladu funguje, protože Products.CategoryID
je typu int?
, což je zkratka pro Nullable<int>
.
Pokud je v klauzuli join pouze jeden z porovnávaných klíčů typ hodnoty null, můžete druhý přetypovat na typ hodnoty nullable ve výrazu dotazu. V následujícím příkladu předpokládejme, že EmployeeID
je sloupec, který obsahuje hodnoty typu int?
:
var query =
from o in db.Orders
join e in db.Employees
on o.EmployeeID equals (int?)e.EmployeeID
select new { o.OrderID, e.FirstName };
V každém příkladu equals
se použije klíčové slovo dotazu. Můžete také použít porovnávání vzorů, které zahrnuje vzory pro is null
a is not null
. Tyto vzory se v dotazech LINQ nedoporučují, protože zprostředkovatelé dotazů nemusí správně interpretovat novou syntaxi jazyka C#. Zprostředkovatel dotazů je knihovna, která překládá výrazy dotazů jazyka C# do nativního datového formátu, jako je Entity Framework Core. Poskytovatelé dotazů implementují System.Linq.IQueryProvider rozhraní pro vytváření zdrojů dat, které implementují System.Linq.IQueryable<T> rozhraní.
Zpracování výjimek ve výrazech dotazů
V kontextu výrazu dotazu je možné volat jakoukoli metodu. Nevolejte žádnou metodu ve výrazu dotazu, která může vytvořit vedlejší efekt, například úpravu obsahu zdroje dat nebo vyvolání výjimky. Tento příklad ukazuje, jak se vyhnout vyvolání výjimek při volání metod ve výrazu dotazu bez porušení obecných pokynů .NET pro zpracování výjimek. Tyto pokyny uvádějí, že je přijatelné zachytit konkrétní výjimku, když pochopíte, proč je vyvolán v daném kontextu. Další informace naleznete v tématu Osvědčené postupy pro výjimky.
Poslední příklad ukazuje, jak tyto případy zpracovat, když při provádění dotazu musíte vyvolat výjimku.
Následující příklad ukazuje, jak přesunout kód zpracování výjimek mimo výraz dotazu. Tento refaktoring je možný pouze v případě, že metoda nezávisí na žádných proměnných, které jsou v dotazu místní. Je jednodušší řešit výjimky mimo výraz dotazu.
// A data source that is very likely to throw an exception!
IEnumerable<int> GetData() => throw new InvalidOperationException();
// DO THIS with a datasource that might
// throw an exception.
IEnumerable<int>? dataSource = null;
try
{
dataSource = GetData();
}
catch (InvalidOperationException)
{
Console.WriteLine("Invalid operation");
}
if (dataSource is not null)
{
// If we get here, it is safe to proceed.
var query =
from i in dataSource
select i * i;
foreach (var i in query)
{
Console.WriteLine(i.ToString());
}
}
catch (InvalidOperationException)
V bloku v předchozím příkladu zpracovat (nebo nezpracovávejte) výjimku způsobem, který je vhodný pro vaši aplikaci.
V některých případech může být nejlepší odpovědí na výjimku, která je vyvoláná z dotazu, okamžité zastavení provádění dotazu. Následující příklad ukazuje, jak zpracovat výjimky, které mohou být vyvolány z textu dotazu. Předpokládejme, že SomeMethodThatMightThrow
může potenciálně způsobit výjimku, která vyžaduje zastavení provádění dotazu.
Blok try
uzavře smyčku foreach
, nikoli samotný dotaz. Smyčka foreach
je bod, ve kterém se dotaz spustí. Při spuštění dotazu se vyvolá výjimky za běhu. Proto je nutné je zpracovat ve foreach
smyčce.
// Not very useful as a general purpose method.
string SomeMethodThatMightThrow(string s) =>
s[4] == 'C' ?
throw new InvalidOperationException() :
@"C:\newFolder\" + s;
// Data source.
string[] files = ["fileA.txt", "fileB.txt", "fileC.txt"];
// Demonstration query that throws.
var exceptionDemoQuery =
from file in files
let n = SomeMethodThatMightThrow(file)
select n;
try
{
foreach (var item in exceptionDemoQuery)
{
Console.WriteLine($"Processing {item}");
}
}
catch (InvalidOperationException e)
{
Console.WriteLine(e.Message);
}
/* Output:
Processing C:\newFolder\fileA.txt
Processing C:\newFolder\fileB.txt
Operation is not valid due to the current state of the object.
*/
Nezapomeňte zachytit jakoukoli výjimku, kterou očekáváte, že vyvoláte nebo provedete jakékoli nezbytné vyčištění v finally
bloku.