System. Linq Пространство имен
Некоторые сведения относятся к предварительной версии продукта, в которую до выпуска могут быть внесены существенные изменения. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
Предоставляет классы и интерфейсы, поддерживающие запросы с использованием LINQ.
Классы
Предоставляет набор методов типа static ( Shared в Visual Basic) для выполнения запросов к объектам, реализующим интерфейс IEnumerable .
Представляет дерево выражений и предоставляет функциональные возможности для выполнения дерева выражения после его перезаписи.
Представляет дерево выражений и предоставляет функциональные возможности для выполнения дерева выражения после его перезаписи.
Представляет IEnumerable в виде источника данных EnumerableQuery.
Переопределения методов расширения LINQ, которые более эффективны при работе с объектами ImmutableArray , чем стандартные методы LINQ.
Представляет коллекцию ключей, каждый из сопоставляется с одним или несколькими значениями.
Представляет отсортированную параллельную последовательность.
Предоставляет набор методов для выполнения запросов к объектам, реализующим ParallelQuery. Это параллельный эквивалент объекта Enumerable.
Представляет параллельную последовательность.
Представляет параллельную последовательность.
Предоставляет набор методов типа static ( Shared в Visual Basic) для выполнения запросов к структурам данных, реализующим объект IQueryable .
Интерфейсы
Представляет коллекцию объектов с общим ключом.
Определяет индексатор, свойство размера и метод логического поиска для структур данных, сопоставляющих ключи с последовательностями IEnumerable значений.
Представляет упорядоченную последовательность.
Представляет результат операции сортировки.
Представляет результат операции сортировки.
Предоставляет функциональные возможности для оценки запросов по определенным источникам данных в случае, если тип данных не указан.
Предоставляет функциональные возможности для оценки запросов по определенным источникам данных в случае, если тип данных известен.
Определяет методы создания и выполнения запросов, описываемых объектом IQueryable.
Перечисления
Режим выполнения запросов представляет собой рекомендацию, сообщающую системе, каким образом следует обрабатывать компромиссы производительности при параллелизации запросов.
Указывает предпочтительный тип объединения выходных данных для использования в запросе. Другими словами, указывает, каким образом PLINQ должен объединять результаты из различных разделов в одну итоговую последовательность. Это только рекомендация, которую система может не соблюдать при параллелизации всех запросов.
Комментарии
Пространство System.Linq имен находится в сборке System.Core (в System.Core.dll).
Класс Enumerable содержит стандартные операторы запросов LINQ, которые работают с объектами, реализующими IEnumerable .
Класс Queryable содержит стандартные операторы запросов LINQ, которые работают с объектами, реализующими IQueryable .
Дополнительные сведения см. в разделе LINQ to SQL.
Обратная связь
Были ли сведения на этой странице полезными?
LINQ — язык интегрированных запросов

Ниже представлены разделы, которые входят в данную тему:
LINQ to Objects — название, данное API-интерфейсу IEnumerable для стандартных операций запросов (Standard Query Operators). Именно LINQ to Objects позволяет выполнять запросы к массивам и находящимся в памяти коллекциям данных. Стандартные операции запросов — это статические методы класса System.Linq.Enumerable, которые используются для создания запросов LINQ to Objects.
LINQ to XML — название, назначенное API-интерфейсу LINQ, который ориентирован на работу с XML. В Microsoft не только добавили необходимые библиотеки XML для работы с LINQ, но также восполнили недостатки стандартной модели XML DOM, существенно облегчив работу с XML. Прошли времена, когда нужно было создавать XmlDocument только для того, чтобы поработать с небольшим фрагментом XML-кода. Чтобы воспользоваться преимуществами LINQ to XML, в проект понадобится добавить ссылку на сборку System.Xml.Linq.dll и директиву using System.Xml.Linq.
LINQ to DataSet — название, данное API-интерфейсу LINQ, который предназначен для работы с DataSet. У многих разработчиков есть масса кода, полагающегося на DataSet. Те, кто не хотят отставать от новых веяний, но и не готовы переписывать свой код, благодаря этому интерфейсу могут воспользоваться всей мощью LINQ.
LINQ to SQL — наименование, присвоенное API-интерфейсу IQueryable, который позволяет запросам LINQ работать с базой данных Microsoft SQL Server. Чтобы воспользоваться преимуществами LINQ to SQL в проект понадобится добавить ссылку на сборку System.Data.Linq.dll, а также директиву using System.Data.Linq.
LINQ to Entities — альтернативный API-интерфейс LINQ, используемый для обращения к базе данных. Он отделяет сущностную объектную модель от физической базы данных, вводя логическое отображение между ними двумя. С таким отделением возрастает мощь и гибкость, но также растет и сложность. Если нужна более высокая гибкость, чем обеспечивается LINQ to SQL, имеет смысл рассмотреть эту альтернативу.
В частности, когда необходимо ослабить связь между сущностной объектной моделью и базой данных, если сущностные объекты конструируются из нескольких таблиц или требуется большая гибкость в моделировании сущностных объектов, то в этом случае LINQ to Entities может стать оптимальным выбором.
Формально отдельного продукта LINQ, который нужно было бы получать отдельно, не существует. LINQ полностью интегрирован в .NET Framework, начиная с версии 3.5 и Visual Studio 2008. В NET 4.0 и Visual Studio 2010 добавлена поддержка средств Parallel LINQ, которые рассматриваются в данном разделе.
- C# тест (легкий)
- .NET тест (средний)

Лучший чат для C# программистов
LINQ
LINQ (Language-Integrated Query) представляет простой и удобный язык запросов к источнику данных. В качестве источника данных может выступать объект, реализующий интерфейс IEnumerable (например, стандартные коллекции, массивы), набор данных DataSet, документ XML. Но вне зависимости от типа источника LINQ позволяет применить ко всем один и тот же подход для выборки данных.
Существует несколько разновидностей LINQ:
- LINQ to Objects : применяется для работы с массивами и коллекциями
- LINQ to Entities : используется при обращении к базам данных через технологию Entity Framework
- LINQ to XML : применяется при работе с файлами XML
- LINQ to DataSet : применяется при работе с объектом DataSet
- Parallel LINQ (PLINQ) : используется для выполнения параллельных запросов
В этой главе речь пойдет прежде всего о LINQ to Objects , но в последующих материалах также будут затронуты и другие разновидности LINQ. Основная часть функциональности LINQ сосредоточена в пространстве имен System.LINQ . В проектах под .NET 6 данное пространство имен подключается по умолчанию.
В чем же удобство LINQ? Посмотрим на простейшем примере. Выберем из массива строки, которые начинаются на определенную букву, например, букву «T», и отсортируем полученный список:
string[] people = < "Tom", "Bob", "Sam", "Tim", "Tomas", "Bill" >; // создаем новый список для результатов var selectedPeople = new List(); // проходим по массиву foreach (string person in people) < // если строка начинается на букву T, добавляем в список if (person.ToUpper().StartsWith("T")) selectedPeople.Add(person); >// сортируем список selectedPeople.Sort(); foreach (string person in selectedPeople) Console.WriteLine(person);
Для отфильтрованных строк создается специальный список. Затем в цикле проходим по всем элементам массива и, если они соответствуют условию (начинаются на букву T), то добавляем их в этот список. Затем сортируем список по возрастанию. И в конце элементы полученного списка выводим на консоль:
Tim Tom Tomas
Хотя подобный подход вполне работает, однако LINQ позволяет значительно сократить код с помощью интуитивно понятного и краткого синтаксиса.
Для работы с колекциями можно использовать два способа:
- Операторы запросов LINQ
- Методы расширений LINQ Рассмотрим оба способа
Операторы запросов LINQ
Операторы запросов LINQ в каком-то роде частично напоминают синтаксис запросов SQL, поэтому если вы работали когда-нибудь с sql-запросами, то будет проще понять общую концепцию. Итак, изменим предыдущий пример, применив операторы запросов LINQ:
string[] people = < "Tom", "Bob", "Sam", "Tim", "Tomas", "Bill" >; // создаем новый список для результатов var selectedPeople = from p in people // передаем каждый элемент из people в переменную p where p.ToUpper().StartsWith("T") //фильтрация по критерию orderby p // упорядочиваем по возрастанию select p; // выбираем объект в создаваемую коллекцию foreach (string person in selectedPeople) Console.WriteLine(person);
Прежде всего, как мы видим, код стал меньше и проще, а результат будет тем же. В принципе все выражение можно было бы записать в одну строку:
var selectedPeople = from p in people where p.ToUpper().StartsWith("T") orderby p select p;
Но для более понятной логической разбивки я поместил каждое отдельное подвыражение на отдельной строке. Простейшее определение запроса LINQ выглядит следующим образом:
from переменная in набор_объектов select переменная;
Итак, что делает этот запрос LINQ? Выражение from p in people проходит по всем элементам массива people и определяет каждый элемент как p . Используя переменную p мы можем проводить над ней разные операции. Несмотря на то, что мы не указываем тип переменной p , выражения LINQ являются строго типизированными. То есть среда автоматически распознает, что набор people состоит из объектов string, поэтому переменная p будет рассматриваться в качестве строки. Далее с помощью оператора where проводится фильтрация объектов, и если объект соответствует критерию (в данном случае начальная буква должна быть «T»), то этот объект передается дальше. Оператор orderby упорядочивает по возрастанию, то есть сортирует выбранные объекты. Оператор select передает выбранные значения в результирующую выборку, которая возвращается LINQ-выражением. В данном случае результатом выражения LINQ является объект IEnumerable
Методы расширения LINQ
Кроме стандартного синтаксиса from .. in .. select для создания запроса LINQ мы можем применять специальные методы расширения, которые определены для интерфейса IEnumerable . Как правило, эти методы реализуют ту же функциональность, что и операторы LINQ типа where или orderby . Например:
string[] people = < "Tom", "Bob", "Sam", "Tim", "Tomas", "Bill" >; var selectedPeople = people.Where(p => p.ToUpper().StartsWith("T")).OrderBy(p => p); foreach (string person in selectedPeople) Console.WriteLine(person);
Запрос people.Where(p=>p.ToUpper().StartsWith(«T»)).OrderBy(p => p) будет аналогичен предыдущему. Он состоит из цепочки методов Where и OrderBy. В качестве аргумента эти методы принимают делегат или лямбда-выражение. И хотя ряд действий мы можем реализовать как с помощью операторов запросов LINQ, так и с помощью методов расширений LINQ, но не каждый метод расширения имеет аналог среди операторов LINQ. И в этом случае можно сочетать оба подхода. Например, используем стандартный синтаксис linq и метод расширения Count(), который возвращает количество элементов в выборке:
int number = (from p in people where p.ToUpper().StartsWith("T") select p).Count(); Console.WriteLine(number); // 3
Список используемых методов расширения LINQ
- Select : определяет проекцию выбранных значений
- Where : определяет фильтр выборки
- OrderBy : упорядочивает элементы по возрастанию
- OrderByDescending : упорядочивает элементы по убыванию
- ThenBy : задает дополнительные критерии для упорядочивания элементов возрастанию
- ThenByDescending : задает дополнительные критерии для упорядочивания элементов по убыванию
- Join : соединяет две коллекции по определенному признаку
- Aggregate : применяет к элементам последовательности агрегатную функцию, которая сводит их к одному объекту
- GroupBy : группирует элементы по ключу
- ToLookup : группирует элементы по ключу, при этом все элементы добавляются в словарь
- GroupJoin : выполняет одновременно соединение коллекций и группировку элементов по ключу
- Reverse : располагает элементы в обратном порядке
- All : определяет, все ли элементы коллекции удовлятворяют определенному условию
- Any : определяет, удовлетворяет хотя бы один элемент коллекции определенному условию
- Contains : определяет, содержит ли коллекция определенный элемент
- Distinct : удаляет дублирующиеся элементы из коллекции
- Except : возвращает разность двух коллекцию, то есть те элементы, которые создаются только в одной коллекции
- Union : объединяет две однородные коллекции
- Intersect : возвращает пересечение двух коллекций, то есть те элементы, которые встречаются в обоих коллекциях
- Count : подсчитывает количество элементов коллекции, которые удовлетворяют определенному условию
- Sum : подсчитывает сумму числовых значений в коллекции
- Average : подсчитывает cреднее значение числовых значений в коллекции
- Min : находит минимальное значение
- Max : находит максимальное значение
- Take : выбирает определенное количество элементов
- Skip : пропускает определенное количество элементов
- TakeWhile : возвращает цепочку элементов последовательности, до тех пор, пока условие истинно
- SkipWhile : пропускает элементы в последовательности, пока они удовлетворяют заданному условию, и затем возвращает оставшиеся элементы
- Concat : объединяет две коллекции
- Zip : объединяет две коллекции в соответствии с определенным условием
- First : выбирает первый элемент коллекции
- FirstOrDefault : выбирает первый элемент коллекции или возвращает значение по умолчанию
- Single : выбирает единственный элемент коллекции, если коллекция содержит больше или меньше одного элемента, то генерируется исключение
- SingleOrDefault : выбирает единственный элемент коллекции. Если коллекция пуста, возвращает значение по умолчанию. Если в коллекции больше одного элемента, генерирует исключение
- ElementAt : выбирает элемент последовательности по определенному индексу
- ElementAtOrDefault : выбирает элемент коллекции по определенному индексу или возвращает значение по умолчанию, если индекс вне допустимого диапазона
- Last : выбирает последний элемент коллекции
- LastOrDefault : выбирает последний элемент коллекции или возвращает значение по умолчанию
Работа с запросами LINQ
В этом руководстве описаны возможности .NET Core и C#. Вы научитесь выполнять следующие задачи:
- создавать последовательности с помощью LINQ;
- писать методы, которые можно применять в запросах LINQ;
- различать упреждающее и отложенное вычисление.
Вы освоите эти методы на примере приложения, которое демонстрирует один из основных навыков любого иллюзиониста: тасовка по методу фаро. Так называют метод тасовки, при котором колода делится ровно на две части, а затем собирается заново так, что карты из каждой половины следуют строго поочередно.
Этот метод очень удобен для иллюзионистов, поскольку положение каждой карты после каждой тасовки точно известно, и через несколько циклов порядок карт восстанавливается.
Здесь же он используется в качестве не слишком серьезного примера для процессов управления последовательностями данных. Приложение, которое вы создадите, будет моделировать колоду карт и выполнять для них серию тасовок, выводя новый порядок карт после каждой из них. Вы сможете сравнить новый порядок карт с исходным.
Это руководство описывает несколько шагов. После каждого из них вы сможете запустить приложение и оценить результаты. Готовый пример доступен в репозитории dotnet/samples на сайте GitHub. Инструкции по загрузке см. в разделе Просмотр и скачивание примеров.
Необходимые компоненты
Компьютер должен быть настроен для выполнения .NET Core. Инструкции по установке см. на странице Загрузка.NET Core. Это приложение можно запустить в ОС Windows, Ubuntu Linux или OS X, а также в контейнере Docker. Вам потребуется редактор кода, но вы можете выбрать любой привычный для вас. В примерах ниже используется кроссплатформенный редактор Visual Studio Code с открытым исходным кодом. Вы можете заменить его на любое другое средство, с которым вам удобно работать.
Создание приложения
Первым шагом является создание нового приложения. Откройте командную строку и создайте новый каталог для приложения. Перейдите в этот каталог. В командной строке введите команду dotnet new console . Эта команда создает начальный набор файлов для базового приложения Hello World.
Если вы раньше никогда не работали с C#, изучите структуру программы C# по этому руководству. Мы рекомендуем сначала ознакомиться с ним, а затем вернуться сюда и продолжить изучение LINQ.
Создание набора данных
Перед началом работы убедитесь, что в верхней части файла Program.cs , созданного dotnet new console , находятся следующие строки:
// Program.cs using System; using System.Collections.Generic; using System.Linq;
Если эти три строки (инструкции using ) находятся не в верхней части файла, наша программа не будет компилироваться.
Теперь, когда у вас есть все необходимые ссылки, посмотрите, из чего состоит колода карт. Как правило, в колоде игральных карт четыре масти и в каждой масти по тринадцать значений. Вы можете создать класс Card сразу же и заполнить коллекцию объектами Card вручную. С помощью LINQ колоду карт можно создать гораздо быстрее, чем обычным способом. Вместо класса Card вы можете создать две последовательности, представляющие масти и ранги соответственно. Вы создадите два очень простых метода итератора, которые будут создавать ранги и масти как IEnumerable строк:
// Program.cs // The Main() method static IEnumerable Suits() < yield return "clubs"; yield return "diamonds"; yield return "hearts"; yield return "spades"; >static IEnumerable Ranks()
Разместите их под методом Main в вашем файле Program.cs . Оба эти два метода используют синтаксис yield return , создавая последовательность по мере выполнения. Компилятор создаст объект, который реализует интерфейс IEnumerable , и сохранит в него последовательность строк по мере их получения.
Теперь с помощью этих методов итератора создайте колоду карт. Поместите запрос LINQ в метод Main . Это описано ниже:
// Program.cs static void Main(string[] args) < var startingDeck = from s in Suits() from r in Ranks() select new < Suit = s, Rank = r >; // Display each card that we've generated and placed in startingDeck in the console foreach (var card in startingDeck) < Console.WriteLine(card); >>
Несколько выражений from создают запрос SelectMany, который формирует одну последовательность из сочетаний каждого элемента первой последовательности с каждым элементом второй последовательности. Для нашего примера важен порядок последовательности. Первый элемент первой последовательности (масти) поочередно сочетается с каждым элементом второй последовательности (ранги). В итоге мы получаем все тринадцать карт первой масти. Этот процесс повторяется для каждого элемента первой последовательности (масти). Конечным результатом является колода карт, упорядоченная сначала по мастям, а затем по достоинствам.
Важно помнить, что независимо от того, будете ли вы записывать LINQ в синтаксисе запросов, показанном выше, или вместо этого будете использовать синтаксис методов, вы всегда можете перейти от одной формы синтаксиса к другой. Приведенный выше запрос, записанный в синтаксисе запросов, можно записать в синтаксис метода следующим образом:
var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => new < Suit = suit, Rank = rank >));
Компилятор преобразует инструкции LINQ, написанные с помощью синтаксиса запросов, в эквивалентный синтаксис вызова метода. Таким образом, независимо от выбранного синтаксиса, две версии запроса дают одинаковый результат. Выберите, какой синтаксис лучше всего подходит для вашей ситуации. Например, если вы работаете в команде, в которой у некоторых участников есть сложности с синтаксисом метода, попробуйте использовать синтаксис запроса.
Теперь давайте выполним пример, который вы создали к этому моменту. Он отобразит все 52 карты колоды. Возможно, вам будет интересно выполнить этот пример в отладчике и проследить за выполнением методов Suits() и Ranks() . Вы сможете заметить, что каждая строка в каждой последовательности создается только по мере необходимости.

Обработка порядка
Теперь рассмотрим, как вы будете тасовать карты в колоде. Чтобы хорошо потасовать, сначала необходимо разделить колоду на две части. Эту возможность вам предоставят методы Take и Skip, входящие в интерфейсы API LINQ. Поместите их под циклом foreach :
// Program.cs public static void Main(string[] args) < var startingDeck = from s in Suits() from r in Ranks() select new < Suit = s, Rank = r >; foreach (var c in startingDeck) < Console.WriteLine(c); >// 52 cards in a deck, so 52 / 2 = 26 var top = startingDeck.Take(26); var bottom = startingDeck.Skip(26); >
В стандартной библиотеке нет метода для тасовки, которым можно было бы воспользоваться, поэтому вам нужно написать собственный. Метод для тасовки, который вы создадите, иллюстрирует несколько приемов, которые вы будете использовать в программах на основе LINQ, поэтому каждая часть этого процесса будет описана в действиях.
Чтобы добавить некоторые функции для взаимодействия с IEnumerable , который будет возвращен в запросах LINQ, вам потребуется написать особые методы, называемые методами расширения. Короче говоря, метод расширения представляет собой специализированный статический метод, добавляющий новые функциональные возможности в уже имеющийся тип без изменения исходного типа, в который необходимо добавить функциональные возможности.
Переместите методы расширения в другое расположение, добавив новый файл статического класса в программу с именем Extensions.cs , а затем приступите к разработке первого метода расширения:
// Extensions.cs using System; using System.Collections.Generic; using System.Linq; namespace LinqFaroShuffle < public static class Extensions < public static IEnumerableInterleaveSequenceWith(this IEnumerable first, IEnumerable second) < // Your implementation will go here soon enough >> >
Обратите внимание на сигнатуру метода, в частности на параметры:
public static IEnumerable InterleaveSequenceWith (this IEnumerable first, IEnumerable second)
Как вы видите, для первого аргумента этого метода добавлен модификатор this . Это означает, что метод вызывается так, как если бы он был методом-членом и имел тип, указанный для первого аргумента. Такое объявление методов соответствует стандартному принципу, по которому для входа и выхода используется тип IEnumerable . Такая практика позволяет объединять методы LINQ в цепочку, чтобы создавать более сложные запросы.
Очевидно, что так как вы разделили колоду на две части, их необходимо соединить. В коде это означает, что необходимо перечислить обе последовательности, полученные с помощью Take и Skip за один раз, применяя команду interleaving к элементам и создавая одну последовательность: колода карт, которая тасуется сейчас. Чтобы создать метод LINQ, который работает с двумя последовательностями, важно хорошо понимать принципы работы IEnumerable .
Интерфейс IEnumerable содержит один метод: GetEnumerator. Этот метод GetEnumerator возвращает объект, у которого есть метод для перехода к следующему элементу и свойство, которое возвращает текущий элемент в последовательности. С помощью этих двух членов вы выполните перебор всей коллекции и получение элементов. Метод Interleave будет реализован как метод итератора, поэтому вы не будете создавать и возвращать коллекцию, а примените описанный выше синтаксис yield return .
Так выглядит реализация этого метода:
public static IEnumerable InterleaveSequenceWith (this IEnumerable first, IEnumerable second) < var firstIter = first.GetEnumerator(); var secondIter = second.GetEnumerator(); while (firstIter.MoveNext() && secondIter.MoveNext()) < yield return firstIter.Current; yield return secondIter.Current; >>
Теперь, добавив в проект этот метод, вернитесь к методу Main и один раз перетасуйте колоду:
// Program.cs public static void Main(string[] args) < var startingDeck = from s in Suits() from r in Ranks() select new < Suit = s, Rank = r >; foreach (var c in startingDeck) < Console.WriteLine(c); >var top = startingDeck.Take(26); var bottom = startingDeck.Skip(26); var shuffle = top.InterleaveSequenceWith(bottom); foreach (var c in shuffle) < Console.WriteLine(c); >>
Сравнения
Через сколько тасовок колода снова соберется в исходном порядке? Чтобы узнать это, вам нужно написать метод, который проверяет равенство двух последовательностей. Создав такой метод, вы поместите код тасовки колоды в цикл, в котором будете проверять, расположены ли карты в правильном порядке.
Метод, который сравнивает две последовательности, будет очень простым. По структуре он похож на метод, который мы создали для тасовки колоды. Но теперь вместо команды yield return , которая возвращает элементы, вы будете сравнивать элементы каждой последовательности. Если перечисление последовательности завершилось и все элементы попарно совпадают, то последовательности считаются одинаковыми:
public static bool SequenceEquals (this IEnumerable first, IEnumerable second) < var firstIter = first.GetEnumerator(); var secondIter = second.GetEnumerator(); while ((firstIter?.MoveNext() == true) && secondIter.MoveNext()) < if ((firstIter.Current is not null) && !firstIter.Current.Equals(secondIter.Current)) < return false; >> return true; >
Здесь мы видим в действии второй принцип LINQ: терминальные методы. Они принимают последовательность в качестве входных данных (или две последовательности, как в нашем примере) и возвращают скалярное значение. В цепочке методов для запроса LINQ терминальные методы всегда используются последними, отсюда и название «терминальный».
Этот принцип мы применяем при определении того, находится ли колода в исходном порядке. Поместите код тасовки в цикл, который будет останавливаться в том случае, когда порядок последовательности восстановлен. Для проверки примените метод SequenceEquals() . Как вы уже поняли, этот метод всегда будет последним в любом запросе, поскольку он возвращает одиночное значение, а не последовательность:
// Program.cs static void Main(string[] args) < // Query for building the deck // Shuffling using InterleaveSequenceWith(); var times = 0; // We can re-use the shuffle variable from earlier, or you can make a new one shuffle = startingDeck; do < shuffle = shuffle.Take(26).InterleaveSequenceWith(shuffle.Skip(26)); foreach (var card in shuffle) < Console.WriteLine(card); >Console.WriteLine(); times++; > while (!startingDeck.SequenceEquals(shuffle)); Console.WriteLine(times); >
Выполните код и обратите внимание на то, как выполняется переупорядочивание колоды при каждой тасовке. После 8 тасовок (итераций цикла do-while), колода возвращается к исходной конфигурации, в которой она находилась при создании из начального запроса LINQ.
Оптимизации
Пример, который вы создали к этому моменту, выполняет внутреннюю тасовку, то есть первая и последняя карты колоды сохраняют свои позиции после каждой итерации. Давайте внесем одно изменение: вместо этого мы будем использовать внешнюю тасовку, при которой все 52 карты изменяют свои позиции. Для этого колоду нужно собирать так, чтобы первой картой в колоде стала первая карта из нижней половины. Тогда самой нижней картой станет последняя карта из верхней половины колоды. Это простое изменение в одной строке кода. Обновите текущий запрос тасовки, переключив положения Take и Skip. Это поменяет местами нижнюю и верхнюю половины колоды:
shuffle = shuffle.Skip(26).InterleaveSequenceWith(shuffle.Take(26));
Снова запустите программу, и вы увидите, что для восстановления исходного порядка теперь требуется 52 итерации. Также вы могли обратить внимание, что по мере выполнения программы она заметным образом замедляется.
Для этого есть сразу несколько причин. Вы можете решить одну из самых существенных причин спада производительности — неэффективное использование отложенного вычисления.
Короче говоря, отложенное вычисление означает, что вычисление инструкции не выполняется, пока не понадобится ее значение. Запросы LINQ — это инструкции, которые обрабатываются отложенным образом. Последовательности создаются только тогда, когда происходит обращение к их элементам. Обычно это дает LINQ огромное преимущество. Но в некоторых программах, таких как в нашем примере, это приводит к экспоненциальному росту времени выполнения.
Помните, что мы создали исходную колоду с помощью запроса LINQ. Каждая последующая тасовка выполняет три запроса LINQ к колоде, полученной на предыдущем этапе. И все эти запросы выполняются отложенно. В частности, это означает, что запросы выполняются каждый раз при обращении к последовательности. Таким образом, пока вы доберетесь до 52-й итерации, исходная колода будет заново создана очень много раз. Чтобы наглядно это продемонстрировать, давайте создадим журнал выполнения. Затем вы исправите эту проблему.
В вашем файле Extensions.cs введите или скопируйте приведенный ниже метод. Этот метод расширения создает файл с именем debug.log в каталоге проекта и записывает в файл журнала, какой запрос выполняется в данный момент. Этот метод расширения можно добавить к любому запросу, чтобы зафиксировать его выполнение.
public static IEnumerable LogQuery (this IEnumerable sequence, string tag) < // File.AppendText creates a new file if the file doesn't exist. using (var writer = File.AppendText("debug.log")) < writer.WriteLine($"Executing Query "); > return sequence; >
Вы увидите красную волнистую линию под File , означающую, что его не существует. Он не будет компилироваться, поскольку компилятор не знает, что такое File . Чтобы решить эту проблему, добавьте следующую строку кода под самой первой строкой в Extensions.cs :
using System.IO;
Это позволит решить проблему, и красная линия ошибки исчезнет.
Теперь давайте дополним определение каждого запроса сообщением для журнала:
// Program.cs public static void Main(string[] args) < var startingDeck = (from s in Suits().LogQuery("Suit Generation") from r in Ranks().LogQuery("Rank Generation") select new < Suit = s, Rank = r >).LogQuery("Starting Deck"); foreach (var c in startingDeck) < Console.WriteLine(c); >Console.WriteLine(); var times = 0; var shuffle = startingDeck; do < // Out shuffle /* shuffle = shuffle.Take(26) .LogQuery("Top Half") .InterleaveSequenceWith(shuffle.Skip(26) .LogQuery("Bottom Half")) .LogQuery("Shuffle"); */ // In shuffle shuffle = shuffle.Skip(26).LogQuery("Bottom Half") .InterleaveSequenceWith(shuffle.Take(26).LogQuery("Top Half")) .LogQuery("Shuffle"); foreach (var c in shuffle) < Console.WriteLine(c); >times++; Console.WriteLine(times); > while (!startingDeck.SequenceEquals(shuffle)); Console.WriteLine(times); >
Обратите внимание, что запись в журнал не нужно выполнять при обращении к запросу. Она выполняется только при создании исходного запроса. Программа по-прежнему работает очень долго, но теперь вы хорошо видите, почему. Если у вас не хватит терпения выполнять внешнюю тасовку с ведением журнала, переключите программу обратно на внутреннюю тасовку. На ней вы также заметите влияние отложенного вычисления. За один запуск программа выполняет 2592 запроса, если учитывать все создания мастей и достоинств.
Вы можете повысить производительность кода, чтобы уменьшить количество выполнений. Простой способ исправить — кэшировать результаты исходного запроса LINQ, который создает колоду карт. В настоящее время вы выполняете запросы снова и снова каждый раз, когда цикл do-while проходит через итерацию, повторно создавая и перетасовывая колоду карт. Чтобы кэшировать колоду карт, вы можете использовать методы LINQ ToArray и ToList. Когда вы добавляете их в запросы, они будут выполнять те действия, которые вы указали, но теперь они будут хранить результаты в массиве или списке в зависимости от того, какой метод вы вызовете. Добавьте метод LINQ ToArray в оба запроса и снова запустите программу:
public static void Main(string[] args) < IEnumerable? suits = Suits(); IEnumerable? ranks = Ranks(); if ((suits is null) || (ranks is null)) return; var startingDeck = (from s in suits.LogQuery("Suit Generation") from r in ranks.LogQuery("Value Generation") select new < Suit = s, Rank = r >) .LogQuery("Starting Deck") .ToArray(); foreach (var c in startingDeck) < Console.WriteLine(c); >Console.WriteLine(); var times = 0; var shuffle = startingDeck; do < /* shuffle = shuffle.Take(26) .LogQuery("Top Half") .InterleaveSequenceWith(shuffle.Skip(26).LogQuery("Bottom Half")) .LogQuery("Shuffle") .ToArray(); */ shuffle = shuffle.Skip(26) .LogQuery("Bottom Half") .InterleaveSequenceWith(shuffle.Take(26).LogQuery("Top Half")) .LogQuery("Shuffle") .ToArray(); foreach (var c in shuffle) < Console.WriteLine(c); >times++; Console.WriteLine(times); > while (!startingDeck.SequenceEquals(shuffle)); Console.WriteLine(times); >
Теперь при внутренней тасовке выполняется всего 30 запросов. Переключите программу на внешнюю тасовку, и вы заметите аналогичное улучшение: теперь выполняется 162 запроса.
Обратите внимание, что этот пример лишь демонстрирует варианты использования, в которых отложенное вычисление приводит к проблемам с производительностью. Хотя очень важно знать, когда отложенное вычисление может повлиять на производительность кода, не менее важно понимать, что не все запросы должны выполняться упреждающе. Если не использовать ToArray, производительность снизится. Это связано с тем, что каждое новое расположение карт вычисляется на основе предыдущего расположения. Использование отложенного вычисления означает, что каждое расположение колоды строится с самого начала, из исходной колоды, включая вызов кода для создания startingDeck . Это создает огромный объем дополнительной работы.
На практике некоторые алгоритмы хорошо работают с упреждающим вычислением, а другие хорошо выполняются с отложенным вычислением. Для ежедневного использования отложенное вычисление обычно дает более хороший результат, если в качестве источника данных используется отдельный процесс, например база данных. Для баз данных отложенное вычисление позволяет сложным запросам выполнять только один круговой путь к процессу базы данных и обратно к оставшемуся коду. LINQ является гибким, независимо от того, используете ли вы отложенное или упреждающее вычисление, поэтому измерьте процессы и выберите тип вычислений, который обеспечивает наилучшую производительность.
Заключение
В этом проекте вы изучили:
- использование запросов LINQ для агрегирования данных в осмысленную последовательность;
- запись методов расширения для добавления собственных пользовательских функций в запросы LINQ;
- поиск областей в коде, где могут возникнуть проблемы с производительностью наших запросов LINQ, например снижение скорости;
- упреждающее и отложенное вычисление в отношении запросов LINQ и их влияние на производительность запросов.
Помимо LINQ вы узнали об использовании метода, который иллюзионисты используют для карточных фокусов. Они используют тасовку по методу Фаро, потому что она позволяет хорошо контролировать положение каждой карты в колоде. Теперь, когда вы все это знаете, не рассказывайте это остальным!
Дополнительные сведения о LINQ см. в следующих статьях:
- Введение в LINQ
- Основные операции запросов LINQ (C#)
- Преобразования данных с помощью LINQ (C#)
- Синтаксис запросов и синтаксис методов в LINQ (C#)
- Возможности C#, поддерживающие LINQ
Совместная работа с нами на GitHub
Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.