Запись запросов LINQ C# для запроса данных

Большинство запросов в вводной документации к LINQ написано с использованием декларативного синтаксиса запросов LINQ. Однако синтаксис запроса должен быть преобразован в вызовы методов для среды CLR .NET, когда код компилируется. Эти вызовы метода вызывают стандартные операторы запросов, которые имеют такие имена, как Where, Select, GroupBy, Join, Max и Average. Вместо синтаксиса запросов для их вызова можно использовать синтаксис методов.

Синтаксис запросов и синтаксис методов семантически идентичны, но синтаксис запросов часто проще и проще читать. Некоторые запросы должны быть выражены как вызовы методов. Например, необходимо использовать вызов метода для выражения запроса, который возвращает число элементов, соответствующих указанным критериям. Вызов метода также необходимо использовать для запроса, который получает элемент с максимальным значением в исходной последовательности. В справочной документации по стандартным операторам запросов в пространствах имен System.Linq обычно применяется синтаксис методов. Вы должны ознакомиться с тем, как использовать синтаксис метода в запросах и выражениях запросов.

Стандартные методы расширения оператора запросов

В следующем примере показано простое выражение запроса и семантически эквивалентный ему запрос, написанный как запрос, основанный на методе.

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 + " ");
}

Оба примера дают одинаковый результат. Видно, что тип переменной запроса в обеих формах одинаковый: IEnumerable<T>.

Чтобы разобраться в запросе, основанном на методе, изучим его более подробно. В правой части выражения обратите внимание, что where предложение теперь выражается как метод экземпляра объекта numbers , который имеет тип IEnumerable<int>. Если вы знакомы с универсальным IEnumerable<T> интерфейсом Where , вы знаете, что у него нет метода. Однако при вызове списка завершения IntelliSense в интегрированной среде разработки Visual Studio вы увидите не только Where метод, но и многие другие методы, такие как Select, JoinSelectManyи Orderby. Эти методы реализуют стандартные операторы запросов.

Снимок экрана, показывающий все стандартные операторы запросов в Intellisense.

Хотя он выглядит так, как будто IEnumerable<T> включает в себя больше методов, это не так. Стандартные операторы запросов реализуются как методы расширения. Эти методы "расширяют" существующий тип и могут вызываться так, как если бы они являлись методами экземпляра для этого типа. Стандартные операторы запроса расширяют IEnumerable<T>, и поэтому вы можете написать numbers.Where(...).

Чтобы использовать методы расширения, их можно перенести в область с using директивами. С точки зрения приложения метод расширения и обычные методы экземпляров одинаковы.

Дополнительные сведения о методах расширения см. в разделе Методы расширения. Дополнительные сведения о стандартных операторах запросов см. в разделе Общие сведения о стандартных операторах запроса (C#). Некоторые поставщики LINQ, такие как Entity Framework и LINQ to XML, реализуют собственные стандартные операторы запросов и методы расширения для других типов, кроме IEnumerable<T>.

Лямбда-выражения

В предыдущем примере обратите внимание, что условное выражение (num % 2 == 0) передается как встроенный аргумент Enumerable.Where методу: Where(num => num % 2 == 0). это встроенное выражение является лямбда-выражением. Это удобный способ написания кода, который в противном случае должен быть написан в более сложной форме. num Слева от оператора — входная переменная, соответствующая num выражению запроса. Компилятор может вывести тип num, поскольку известно, что numbers является универсальным типом IEnumerable<T>. Текст лямбда-выражения совпадает с выражением в синтаксисе запроса или в любом другом выражении или операторе C#. Он может включать вызовы методов и другую сложную логику. Возвращаемое значение — это только результат выражения. Некоторые запросы можно выразить только в синтаксисе метода, а некоторые из них требуют лямбда-выражений. Лямбда-выражения — это мощный и гибкий инструмент на панели элементов LINQ.

Возможность создания запросов

В предыдущем примере Enumerable.OrderBy кода метод вызывается с помощью оператора dot в вызове Where. Where создает отфильтрованную последовательность, а затем Orderby сортирует последовательность, созданную Where. Поскольку запросы возвращают IEnumerable, объедините их в синтаксис метода, собрав вызовы методов в цепочку. Компилятор выполняет эту композицию при написании запросов с помощью синтаксиса запросов. Так как переменная запроса не хранит результаты запроса, ее можно изменить или использовать в качестве основы для нового запроса в любое время, даже после его выполнения.

Следующие примеры демонстрируют некоторые простые запросы LINQ, составленные с использованием каждого из указанных выше методов.

Примечание.

Эти запросы обращаются к простым коллекциям в памяти, однако базовый синтаксис при этом точно такой же, как в запросах LINQ to Entities и LINQ to XML.

Пример синтаксиса запросов

Большинство запросов записываются с синтаксисом запросов для создания выражений запросов. В следующем примере показаны три выражения запросов. Первое выражение запроса показывает, как фильтровать или ограничивать результаты, применяя условия в предложении where. Оно возвращает все элементы исходной последовательности, значения которых больше 7 или меньше 3. Второе выражение показывает, как сортировать возвращаемые результаты. Третье выражение показывает, как группировать результаты по ключу. Этот запрос возвращает две группы на основе первой буквы слова.

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];

Тип запросов .IEnumerable<T> Все эти запросы можно написать с помощью var, как показано в следующем примере:

var query = from num in numbers...

В каждом предыдущем примере запросы фактически не выполняются, пока не выполняется итерацию по переменной запроса в foreach инструкции или другой инструкции.

Пример синтаксиса метода

Некоторые операции запросов должны быть выражены как вызов метода. Наиболее распространенными такими методами являются те методы, которые возвращают однотонные числовые значения, такие как Sum, Max, Minи Averageт. д. Эти методы всегда должны вызываться последним в любом запросе, так как они возвращают одно значение и не могут служить источником для дополнительной операции запроса. В следующем примере показан вызов метода в выражении запроса:

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);

Если метод имеет System.Action или System.Func<TResult> параметры, эти аргументы предоставляются в виде лямбда-выражения, как показано в следующем примере:

// Query #6.
IEnumerable<int> largeNumbersQuery = numbers2.Where(c => c > 15);

В предыдущих запросах выполняется только запрос #4, так как он возвращает одно значение, а не универсальную IEnumerable<T> коллекцию. Сам метод использует foreach или аналогичный код для вычисления значения.

Каждый из предыдущих запросов можно записать с помощью неявного ввода с помощью var, как показано в следующем примере:

// var is used for convenience in these queries
double average = numbers1.Average();
var concatenationQuery = numbers1.Concat(numbers2);
var largeNumbersQuery = numbers2.Where(c => c > 15);

Пример смешанного синтаксиса запроса и метода

В этом примере показано, как применять синтаксис метода к результатам предложения запроса. Просто заключите выражение запроса в круглые скобки, а затем примените оператор точки и вызовите метод. В следующем примере запрос 7 возвращает количество чисел со значением от 3 до 7. В общем случае лучше использовать вторую переменную для хранения результата вызова метода. В этом случае меньше вероятность спутать запрос с его результатами.

// 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();

Поскольку запрос 7 возвращает одно значение, а не коллекцию, он выполняется незамедлительно.

Предыдущий запрос можно написать, используя неявную типизацию с переменной var, как показано ниже:

var numCount = (from num in numbers...

При записи в синтаксисе метода он будет выглядеть следующим образом:

var numCount = numbers.Count(n => n is > 3 and < 7);

При записи с явной типизацией он будет выглядеть следующим образом:

int numCount = numbers.Count(n => n is > 3 and < 7);

Динамическое определение фильтров предикатов во время выполнения

В некоторых случаях количество предикатов, которые нужно применить к исходным элементам в предложении where, неизвестно вплоть до времени выполнения. Одним из способов динамического определения сразу нескольких фильтров предикатов служит метод Contains, как показано в следующем примере. Запрос возвращает различные результаты в зависимости от значения id выполнения запроса.

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
 */

Можно использовать инструкции потока управления, такие как if... else или switch, чтобы выбрать один из предопределенных альтернативных запросов. В следующем примере используется другое where предложение, studentQuery если значение oddYear времени выполнения равно true илиfalse.

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
 */

Обработка значений NULL в выражениях запросов

В этом примере показано, как обрабатывать возможные нулевые значения в исходных коллекциях. Коллекция объектов, например IEnumerable<T>, может содержать элементы со значением NULL. Если исходная коллекция содержит элемент, значение которого равно nullnull, и запрос не обрабатывает null значения, NullReferenceException при выполнении запроса возникает исключение.

Чтобы избежать исключения в связи с пустой ссылкой, можно составить защитный код, как показано в следующем примере:

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
    };

В предыдущем примере предложение where отфильтровывает все пустые элементы в последовательности категорий. Этот метод не связан с проверкой на наличие пустых значений в предложении join. Условное выражение со значением NULL в этом примере работает, поскольку Products.CategoryID имеет тип int?, что является сокращением Nullable<int>.

Если в предложении join только один из ключей сравнения имеет тип, допускающий значение NULL, остальные типы в выражении запроса можно привести к типу, допускающему значение NULL. В следующем примере предполагается, что EmployeeID — это столбец, содержащий значения типа 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 };

В каждом из примеров в запросе используется ключевое слово equals. Вы также можете использовать сопоставление шаблонов, включая шаблоны для is null и is not null. Эти шаблоны не рекомендуется использовать в запросах LINQ, так как поставщики запросов могут неправильно интерпретировать новый синтаксис C#. Поставщик запросов — это библиотека, которая преобразует выражения запросов C# в собственный формат данных, например Entity Framework Core. Поставщики запросов реализуют интерфейс System.Linq.IQueryProvider, чтобы создавать источники данных, реализующие интерфейс System.Linq.IQueryable<T>.

Обработка исключений в выражениях запросов

В контексте выражения запроса можно вызвать любой метод. Не вызывайте какой-либо метод в выражении запроса, который может создать побочный эффект, например изменение содержимого источника данных или исключение. В этом примере показано, как избежать создания исключений при вызове методов в выражениях запросов без нарушения общих рекомендаций .NET относительно обработки исключений. Эти рекомендации гласят, что можно перехватывать определенные исключения, если вы понимаете, почему они создаются в данном контексте. Дополнительные сведения см. в разделе Лучшие методики обработки исключений.

В последнем примере показано, как обрабатывать случаи, когда необходимо создать исключение во время выполнения запроса.

В следующем примере показано, как переместить код обработки исключений за пределы выражения запроса. Это рефакторинг возможно только в том случае, если метод не зависит от локальных переменных запроса. Проще справиться с исключениями за пределами выражения запроса.

// 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) , приведенном в предыдущем примере, обработайте исключение (или не обрабатывайте) таким образом, как это подходит для приложения.

В некоторых случаях лучшей реакцией на исключение, которое вызывается в запросе, будет немедленное прекращение выполнения запроса. В следующем примере демонстрируется обработка исключений, которые могут быть созданы из тела запроса. Предположим, что SomeMethodThatMightThrow потенциально может вызвать исключение, которое требует остановки выполнения запроса.

Блок try заключает foreach цикл, а не сам запрос. Цикл foreach — это точка выполнения запроса. Исключения во время выполнения создаются при выполнении запроса. Поэтому они должны обрабатываться в цикле foreach .

// 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.
 */

Не забудьте поймать любое исключение, который вы ожидаете вызвать и /или выполнить любую необходимую очистку в блоке finally .

См. также