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 Wherejsou , Select, GroupBy, Join, Maxa 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 Selectje , SelectMany, Joina Orderby. Tyto metody implementují standardní operátory dotazu.

Snímek obrazovky zobrazující všechny standardní operátory dotazů v IntelliSense

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. Wherevytvoří 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í s výrazem var, 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í varná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 truefalse.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 nulla 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.

Viz také