Общие сведения о запросах LINQ в C#
запрос — это выражение, которое извлекает данные из источника данных. Различные источники данных имеют разные собственные языки запросов, например SQL для реляционных баз данных и XQuery для XML. Разработчики должны узнать новый язык запросов для каждого типа источника данных или формата данных, который они должны поддерживать. LINQ упрощает эту ситуацию, предлагая согласованную языковую модель C# для типов источников данных и форматов. В запросе LINQ вы всегда работаете с объектами C#. Для запроса и преобразования данных в XML-документах, базах данных SQL, коллекциях .NET и любом другом формате при наличии поставщика LINQ используются те же базовые шаблоны программирования.
Три части операции запроса
Все операции запроса LINQ состоят из трех отдельных действий:
- Получите источник данных.
- Создайте запрос.
- Выполните запрос.
В следующем примере показано, как три части операции запроса выражаются в исходном коде. В примере для удобства используется целочисленный массив в качестве источника данных; однако те же понятия применяются и к другим источникам данных. Этот пример упоминается на протяжении всей оставшейся части этой статьи.
// The Three Parts of a LINQ Query:
// 1. Data source.
int[] numbers = [ 0, 1, 2, 3, 4, 5, 6 ];
// 2. Query creation.
// numQuery is an IEnumerable<int>
var numQuery = from num in numbers
where (num % 2) == 0
select num;
// 3. Query execution.
foreach (int num in numQuery)
{
Console.Write("{0,1} ", num);
}
На следующем рисунке показана полная операция запроса. В LINQ выполнение запроса отличается от самого запроса. Другими словами, вы не извлекаете данные, создавая переменную запроса.
Источник данных
Источник данных в предыдущем примере представляет собой массив, который поддерживает универсальный интерфейс IEnumerable<T>. Этот факт означает, что запрос может выполняться с помощью LINQ. Запрос выполняется в инструкции foreach
, и для foreach
требуется IEnumerable или IEnumerable<T>. Типы, поддерживающие IEnumerable<T> или производный интерфейс, такой как универсальный IQueryable<T>, называются типами для запросов .
Запрашиваемый тип не требует изменения или специального лечения для использования в качестве источника данных LINQ. Если исходные данные еще не в памяти в качестве запрашиваемого типа, поставщик LINQ должен представлять его как таковое. Например, LINQ to XML загружает XML-документ в запрашиваемый тип XElement:
// Create a data source from an XML document.
// using System.Xml.Linq;
XElement contacts = XElement.Load(@"c:\myContactList.xml");
С помощью EntityFrameworkсоздается реляционное сопоставление объектов между классами C# и схемой базы данных. Вы пишете запросы по объектам, а во время выполнения Entity Framework обрабатывает взаимодействие с базой данных. В следующем примере Customers
представляет определенную таблицу в базе данных, а тип результата запроса IQueryable<T>является производным от IEnumerable<T>.
Northwnd db = new Northwnd(@"c:\northwnd.mdf");
// Query for customers in London.
IQueryable<Customer> custQuery =
from cust in db.Customers
where cust.City == "London"
select cust;
Дополнительные сведения о создании определенных типов источников данных см. в документации для различных поставщиков LINQ. Однако базовое правило является простым: источник данных LINQ — это любой объект, поддерживающий универсальный интерфейс IEnumerable<T>, или интерфейс, наследуемый от него, обычно IQueryable<T>.
Примечание.
Такие типы, как ArrayList, поддерживающие не универсальный интерфейс IEnumerable, также можно использовать в качестве источника данных LINQ. Дополнительные сведения см. в статье Запрос массива с помощьюLINQ (C#).
Запрос
Запрос указывает, какие сведения следует извлечь из источника данных или источников. При необходимости запрос также указывает, как следует отсортировать, сгруппировать и сформировать данные перед возвратом. Запрос хранится в переменной запроса и инициализирован с помощью выражения запроса. Вы используете синтаксис запросов C# для написания запросов.
Запрос в предыдущем примере возвращает все четные числа из целочисленного массива. Выражение запроса содержит три предложения: from
, where
и select
. (Если вы знакомы с SQL, вы заметили, что порядок предложений отменяется от порядка в SQL.) Предложение from
указывает источник данных, предложение where
применяет фильтр, а предложение select
указывает тип возвращаемых элементов. Все условия запросов подробно рассматриваются в этом разделе. Сейчас важно, чтобы в LINQ переменная запроса не принимает никаких действий и не возвращает данные. Он просто сохраняет сведения, необходимые для получения результатов при выполнении запроса в какой-то момент. Дополнительные сведения о том, как создаются запросы, см. в обзоре стандартных операторов запросов (C#) .
Примечание.
Запросы также можно выразить с помощью синтаксиса метода. Дополнительные сведения см. в разделе Синтаксис запросов и синтаксис метода в LINQ.
Классификация стандартных операторов запросов по способу выполнения
Реализация методов стандартных операторов запроса LINQ to Objects выполняется одним из двух основных способов: немедленный или отложенный. Операторы запросов, использующие отложенное выполнение, можно также разделить на две категории: потоковая и непотоковая.
Немедленно
Немедленное выполнение означает, что источник данных считывается и операция выполняется один раз. Все стандартные операторы запросов, возвращающие скалярный результат, выполняются немедленно. Примерами таких запросов являются Count
, Max
, Average
и First
. Эти методы выполняются без явной инструкции foreach
, так как сам запрос должен использовать foreach
для возврата результата. Эти запросы возвращают одно значение, а не коллекцию IEnumerable
. Любой запрос можно принудительно выполнить немедленно с помощью методов Enumerable.ToList или Enumerable.ToArray. Немедленное выполнение позволяет повторно использовать результаты запроса, а не сам запрос. Результаты извлекаются один раз, а затем хранятся для дальнейшего использования. Следующий запрос возвращает количество четных чисел в исходном массиве:
var evenNumQuery = from num in numbers
where (num % 2) == 0
select num;
int evenNumCount = evenNumQuery.Count();
Чтобы принудительно выполнить любой запрос и кэшировать результаты, можно вызвать методы ToList или ToArray.
List<int> numQuery2 = (from num in numbers
where (num % 2) == 0
select num).ToList();
// or like this:
// numQuery3 is still an int[]
var numQuery3 = (from num in numbers
where (num % 2) == 0
select num).ToArray();
Вы также можете принудительно выполнить, поместив цикл foreach
сразу после выражения запроса. Однако вызов ToList
или ToArray
также позволяет кэшировать все данные в единый объект коллекции.
Действие отложено
Отложенное выполнение означает, что операция не выполняется в точке кода, в котором объявлен запрос. Операция выполняется только при перечислении переменной запроса, например с помощью инструкции foreach
. Результаты выполнения запроса зависят от содержимого источника данных при выполнении запроса, а не при определении запроса. Если переменная запроса перечисляется несколько раз, результаты могут отличаться каждый раз. Почти все стандартные операторы запросов, тип возвращаемого результата которых IEnumerable<T> или IOrderedEnumerable<TElement>, выполняются в отложенном режиме. Отложенное выполнение обеспечивает повторное использование запроса, так как запрос получает обновленные данные из источника данных при каждом итерации результатов запроса. В следующем коде показан пример отложенного выполнения:
foreach (int num in numQuery)
{
Console.Write("{0,1} ", num);
}
Оператор foreach
также определяет, где извлекаются результаты запроса. Например, в предыдущем запросе переменная итерации num
содержит каждое значение (по одному за раз) в возвращаемой последовательности.
Так как переменная запроса никогда не содержит результаты запроса, ее можно многократно выполнять для получения обновленных данных. Например, отдельное приложение может постоянно обновлять базу данных. В приложении можно создать один запрос, который извлекает последние данные, и его можно выполнить через интервалы, чтобы получить обновленные результаты.
Операторы запросов, использующие отложенное выполнение, можно дополнительно классифицировать как потоковые или непотоковые.
Стриминг
Операторы потоковой передачи не должны считывать все исходные данные, прежде чем они получают элементы. Во время выполнения оператор потоковой передачи выполняет свою операцию над каждым исходным элементом по мере считывания и возвращает элемент, если это уместно. Оператор потоковой передачи продолжает считывать исходные элементы до тех пор, пока не будет создан результирующий элемент. Это означает, что для получения одного результирующего элемента могут быть прочитаны несколько исходных элементов.
Нестриминговое
Непотоковые операторы должны считывать все исходные данные, чтобы получить результирующий элемент. Такие операции, как сортировка или группирование, попадают в эту категорию. Во время выполнения непотоковые операторы запросов считывают все исходные данные, помещают его в структуру данных, выполняют операцию и дают результирующий элемент.
Таблица классификации
В следующей таблице классифицируется каждый стандартный метод оператора запроса в соответствии с методом выполнения.
Примечание.
Если оператор помечается двумя столбцами, в операции участвуют две входные последовательности, и каждая последовательность оценивается по-разному. В таких случаях это всегда первая последовательность в списке параметров, которая оценивается в отложенном, потоковом режиме.
Объекты LINQ
"LINQ to Objects" ссылается на использование запросов LINQ с любой коллекцией IEnumerable или IEnumerable<T> напрямую. Вы можете использовать LINQ для запроса любых перечисляемых коллекций, таких как List<T>, Arrayили Dictionary<TKey,TValue>. Коллекция может быть определяемой пользователем или типом, возвращаемым API .NET. В подходе LINQ вы пишете декларативный код, описывающий то, что требуется получить. LINQ to Objects предоставляет отличные общие сведения о программировании с помощью LINQ.
Запросы LINQ предлагают три основных преимущества по сравнению с традиционными циклами foreach
:
- Они более краткие и читаемые, особенно при фильтрации нескольких условий.
- Они предоставляют мощные возможности фильтрации, упорядочивания и группировки с минимальным кодом приложения.
- Их можно перенести в другие источники данных без изменений.
Чем сложнее операция, которую вы хотите выполнить с данными, тем больше преимуществ вы понимаете, используя LINQ вместо традиционных методов итерации.
Хранение результатов запроса в памяти
В основном запрос — это набор инструкций по получению и упорядочению данных. Запросы выполняются лениво, так как каждый последующий элемент в результате запрашивается. При использовании foreach
для итерации результатов элементы возвращаются по мере доступа. Чтобы оценить запрос и сохранить результаты без выполнения цикла foreach
, просто вызовите один из следующих методов в переменной запроса:
Возвращаемый объект коллекции следует назначить новой переменной при хранении результатов запроса, как показано в следующем примере:
List<int> numbers = [ 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 ];
IEnumerable<int> queryFactorsOfFour = from num in numbers
where num % 4 == 0
select num;
// Store the results in a new variable
// without executing a foreach loop.
var factorsofFourList = queryFactorsOfFour.ToList();
// Read and write from the newly created list to demonstrate that it holds data.
Console.WriteLine(factorsofFourList[2]);
factorsofFourList[2] = 0;
Console.WriteLine(factorsofFourList[2]);