Запись запросов 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
, Join
SelectMany
и Orderby
. Эти методы реализуют стандартные операторы запросов.
Хотя он выглядит так, как будто 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. Если исходная коллекция содержит элемент, значение которого равно null
null
, и запрос не обрабатывает 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
.