Работа с запросами LINQWork with Language-Integrated Query (LINQ)

ВведениеIntroduction

В этом руководстве описаны возможности .NET Core и C#.This tutorial teaches you features in .NET Core and the C# language. Вы узнаете, как выполнять такие задачи:You’ll learn how to:

  • создавать последовательности с помощью LINQ;Generate sequences with LINQ.
  • писать методы, которые можно применять в запросах LINQ;Write methods that can be easily used in LINQ queries.
  • различать упреждающее и отложенное вычисление.Distinguish between eager and lazy evaluation.

Вы освоите эти методы на примере приложения, которое демонстрирует один из основных навыков любого иллюзиониста: тасовка по методу фаро.You'll learn these techniques by building an application that demonstrates one of the basic skills of any magician: the faro shuffle. Так называют метод тасовки, при котором колода делится ровно на две части, а затем собирается заново так, что карты из каждой половины следуют строго поочередно.Briefly, a faro shuffle is a technique where you split a card deck exactly in half, then the shuffle interleaves each one card from each half to rebuild the original deck.

Этот метод очень удобен для иллюзионистов, поскольку положение каждой карты после каждой тасовки точно известно, и через несколько циклов порядок карт восстанавливается.Magicians use this technique because every card is in a known location after each shuffle, and the order is a repeating pattern.

Здесь же он используется в качестве не слишком серьезного примера для процессов управления последовательностями данных.For your purposes, it is a light hearted look at manipulating sequences of data. Приложение, которое вы создадите, будет моделировать колоду карт и выполнять для них серию тасовок, выводя новый порядок карт после каждой из них.The application you'll build constructs a card deck and then performs a sequence of shuffles, writing the sequence out each time. Вы сможете сравнить новый порядок карт с исходным.You'll also compare the updated order to the original order.

Это руководство описывает несколько шагов.This tutorial has multiple steps. После каждого из них вы сможете запустить приложение и оценить результаты.After each step, you can run the application and see the progress. Готовый пример доступен в репозитории dotnet/samples на сайте GitHub.You can also see the completed sample in the dotnet/samples GitHub repository. Инструкции по загрузке см. в разделе Просмотр и скачивание примеров.For download instructions, see Samples and Tutorials.

PrerequisitesPrerequisites

Компьютер должен быть настроен для выполнения .NET Core.You’ll need to set up your machine to run .NET core. Инструкции по установке см. на странице Загрузка.NET Core.You can find the installation instructions on the .NET Core Download page. Это приложение можно запустить в ОС Windows, Ubuntu Linux или OS X, а также в контейнере Docker.You can run this application on Windows, Ubuntu Linux, or OS X, or in a Docker container. Вам потребуется редактор кода, но вы можете выбрать любой привычный для вас.You’ll need to install your favorite code editor. В примерах ниже используется кроссплатформенный редактор Visual Studio Code с открытым исходным кодом.The descriptions below use Visual Studio Code which is an open source, cross-platform editor. Вы можете заменить его на любое другое средство, с которым вам удобно работать.However, you can use whatever tools you are comfortable with.

Создание приложенияCreate the Application

Первым шагом является создание нового приложения.The first step is to create a new application. Откройте командную строку и создайте новый каталог для приложения.Open a command prompt and create a new directory for your application. Перейдите в этот каталог.Make that the current directory. В командной строке введите команду dotnet new console.Type the command dotnet new console at the command prompt. Эта команда создает начальный набор файлов для базового приложения Hello World.This creates the starter files for a basic "Hello World" application.

Если вы раньше никогда не работали с C#, изучите структуру программы C# по этому руководству.If you've never used C# before, this tutorial explains the structure of a C# program. Мы рекомендуем сначала ознакомиться с ним, а затем вернуться сюда и продолжить изучение LINQ.You can read that and then return here to learn more about LINQ.

Создание набора данныхCreate the Data Set

Перед началом работы убедитесь, что в верхней части файла Program.cs, созданного dotnet new console, находятся следующие строки:Before you begin, make sure that the following lines are at the top of the Program.cs file generated by dotnet new console:

// Program.cs
using System;
using System.Collections.Generic;
using System.Linq;

Если эти три строки (инструкции using) находятся не в верхней части файла, наша программа не будет компилироваться.If these three lines (using statements) aren't at the top of the file, our program will not compile.

Теперь, когда у вас есть все необходимые ссылки, посмотрите, из чего состоит колода карт.Now that you have all of the references that you'll need, consider what constitutes a deck of cards. Как правило, в колоде игральных карт четыре масти и в каждой масти по тринадцать значений.Commonly, a deck of playing cards has four suits, and each suit has thirteen values. Вы можете создать класс Card сразу же и заполнить коллекцию объектами Card вручную.Normally, you might consider creating a Card class right off the bat and populating a collection of Card objects by hand. С помощью LINQ колоду карт можно создать гораздо быстрее, чем обычным способом.With LINQ, you can be more concise than the usual way of dealing with creating a deck of cards. Вместо класса Card вы можете создать две последовательности, представляющие масти и ранги соответственно.Instead of creating a Card class, you can create two sequences to represent suits and ranks, respectively. Вы создадите два очень простых метода итератора, которые будут создавать ранги и масти как IEnumerable<T> строк:You'll create a really simple pair of iterator methods that will generate the ranks and suits as IEnumerable<T>s of strings:

// Program.cs
// The Main() method

static IEnumerable<string> Suits()
{
    yield return "clubs";
    yield return "diamonds";
    yield return "hearts";
    yield return "spades";
}

static IEnumerable<string> Ranks()
{
    yield return "two";
    yield return "three";
    yield return "four";
    yield return "five";
    yield return "six";
    yield return "seven";
    yield return "eight";
    yield return "nine";
    yield return "ten";
    yield return "jack";
    yield return "queen";
    yield return "king";
    yield return "ace";
}

Разместите их под методом Main в вашем файле Program.cs.Place these underneath the Main method in your Program.cs file. Оба эти два метода используют синтаксис yield return, создавая последовательность по мере выполнения.These two methods both utilize the yield return syntax to produce a sequence as they run. Компилятор создаст объект, который реализует интерфейс IEnumerable<T>, и сохранит в него последовательность строк по мере их получения.The compiler builds an object that implements IEnumerable<T> and generates the sequence of strings as they are requested.

Теперь с помощью этих методов итератора создайте колоду карт.Now, use these iterator methods to create the deck of cards. Поместите запрос LINQ в метод Main.You'll place the LINQ query in our Main method. Это описано ниже:Here's a look at it:

// 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, который формирует одну последовательность из сочетаний каждого элемента первой последовательности с каждым элементом второй последовательности.The multiple from clauses produce a SelectMany, which creates a single sequence from combining each element in the first sequence with each element in the second sequence. Для нашего примера важен порядок последовательности.The order is important for our purposes. Первый элемент первой последовательности (масти) поочередно сочетается с каждым элементом второй последовательности (ранги).The first element in the first source sequence (Suits) is combined with every element in the second sequence (Ranks). В итоге мы получаем все тринадцать карт первой масти.This produces all thirteen cards of first suit. Этот процесс повторяется для каждого элемента первой последовательности (масти).That process is repeated with each element in the first sequence (Suits). Конечным результатом является колода карт, упорядоченная сначала по мастям, а затем по достоинствам.The end result is a deck of cards ordered by suits, followed by values.

Важно помнить, что независимо от того, будете ли вы записывать LINQ в синтаксисе запросов, показанном выше, или вместо этого будете использовать синтаксис методов, вы всегда можете перейти от одной формы синтаксиса к другой.It's important to keep in mind that whether you choose to write your LINQ in the query syntax used above or use method syntax instead, it's always possible to go from one form of syntax to the other. Приведенный выше запрос, записанный в синтаксисе запросов, можно записать в синтаксис метода следующим образом:The above query written in query syntax can be written in method syntax as:

var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => new { Suit = suit, Rank = rank }));

Компилятор преобразует инструкции LINQ, написанные с помощью синтаксиса запросов, в эквивалентный синтаксис вызова метода.The compiler translates LINQ statements written with query syntax into the equivalent method call syntax. Таким образом, независимо от выбранного синтаксиса, две версии запроса дают одинаковый результат.Therefore, regardless of your syntax choice, the two versions of the query produce the same result. Выберите, какой синтаксис лучше всего подходит для вашей ситуации. Например, если вы работаете в команде, в которой у некоторых участников есть сложности с синтаксисом метода, попробуйте использовать синтаксис запроса.Choose which syntax works best for your situation: for instance, if you're working in a team where some of the members have difficulty with method syntax, try to prefer using query syntax.

Теперь давайте выполним пример, который вы создали к этому моменту.Go ahead and run the sample you've built at this point. Он отобразит все 52 карты колоды.It will display all 52 cards in the deck. Возможно, вам будет интересно выполнить этот пример в отладчике и проследить за выполнением методов Suits() и Ranks().You may find it very helpful to run this sample under a debugger to observe how the Suits() and Ranks() methods execute. Вы сможете заметить, что каждая строка в каждой последовательности создается только по мере необходимости.You can clearly see that each string in each sequence is generated only as it is needed.

Окно консоли с приложением, которое выводит 52 карты

Обработка порядкаManipulate the Order

Теперь рассмотрим, как вы будете тасовать карты в колоде.Next, focus on how you're going to shuffle the cards in the deck. Чтобы хорошо потасовать, сначала необходимо разделить колоду на две части.The first step in any good shuffle is to split the deck in two. Эту возможность вам предоставят методы Take и Skip, входящие в интерфейсы API LINQ.The Take and Skip methods that are part of the LINQ APIs provide that feature for you. Поместите их под циклом foreach:Place them underneath the foreach loop:

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

В стандартной библиотеке нет метода для тасовки, которым можно было бы воспользоваться, поэтому вам нужно написать собственный.However, there's no shuffle method to take advantage of in the standard library, so you'll have to write your own. Метод для тасовки, который вы создадите, иллюстрирует несколько приемов, которые вы будете использовать в программах на основе LINQ, поэтому каждая часть этого процесса будет описана в действиях.The shuffle method you'll be creating illustrates several techniques that you'll use with LINQ-based programs, so each part of this process will be explained in steps.

Чтобы добавить некоторые функции для взаимодействия с IEnumerable<T>, который будет возвращен в запросах LINQ, вам потребуется написать особые методы, называемые методами расширения.In order to add some functionality to how you interact with the IEnumerable<T> you'll get back from LINQ queries, you'll need to write some special kinds of methods called extension methods. Короче говоря, метод расширения представляет собой специализированный статический метод, добавляющий новые функциональные возможности в уже имеющийся тип без изменения исходного типа, в который необходимо добавить функциональные возможности.Briefly, an extension method is a special purpose static method that adds new functionality to an already-existing type without having to modify the original type you want to add functionality to.

Переместите методы расширения в другое расположение, добавив новый файл статического класса в программу с именем Extensions.cs, а затем приступите к разработке первого метода расширения:Give your extension methods a new home by adding a new static class file to your program called Extensions.cs, and then start building out the first extension method:

// Extensions.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace LinqFaroShuffle
{
    public static class Extensions
    {
        public static IEnumerable<T> InterleaveSequenceWith<T>(this IEnumerable<T> first, IEnumerable<T> second)
        {
            // Your implementation will go here soon enough
        }
    }
}

Обратите внимание на сигнатуру метода, в частности на параметры:Look at the method signature for a moment, specifically the parameters:

public static IEnumerable<T> InterleaveSequenceWith<T> (this IEnumerable<T> first, IEnumerable<T> second)

Как вы видите, для первого аргумента этого метода добавлен модификатор this.You can see the addition of the this modifier on the first argument to the method. Это означает, что метод вызывается так, как если бы он был методом-членом и имел тип, указанный для первого аргумента.That means you call the method as though it were a member method of the type of the first argument. Такое объявление методов соответствует стандартному принципу, по которому для входа и выхода используется тип IEnumerable<T>.This method declaration also follows a standard idiom where the input and output types are IEnumerable<T>. Такая практика позволяет объединять методы LINQ в цепочку, чтобы создавать более сложные запросы.That practice enables LINQ methods to be chained together to perform more complex queries.

Очевидно, что так как вы разделили колоду на две части, их необходимо соединить.Naturally, since you split the deck into halves, you'll need to join those halves together. В коде это означает, что необходимо перечислить обе последовательности, полученные с помощью Take и Skip за один раз, применяя команду interleaving к элементам и создавая одну последовательность: колода карт, которая тасуется сейчас.In code, this means you'll be enumerating both of the sequences you acquired through Take and Skip at once, interleaving the elements, and creating one sequence: your now-shuffled deck of cards. Чтобы создать метод LINQ, который работает с двумя последовательностями, важно хорошо понимать принципы работы IEnumerable<T>.Writing a LINQ method that works with two sequences requires that you understand how IEnumerable<T> works.

Интерфейс IEnumerable<T> содержит один метод: GetEnumerator.The IEnumerable<T> interface has one method: GetEnumerator. Этот метод GetEnumerator возвращает объект, у которого есть метод для перехода к следующему элементу и свойство, которое возвращает текущий элемент в последовательности.The object returned by GetEnumerator has a method to move to the next element, and a property that retrieves the current element in the sequence. С помощью этих двух членов вы выполните перебор всей коллекции и получение элементов.You will use those two members to enumerate the collection and return the elements. Метод Interleave будет реализован как метод итератора, поэтому вы не будете создавать и возвращать коллекцию, а примените описанный выше синтаксис yield return.This Interleave method will be an iterator method, so instead of building a collection and returning the collection, you'll use the yield return syntax shown above.

Так выглядит реализация этого метода:Here's the implementation of that method:

public static IEnumerable<T> InterleaveSequenceWith<T>
    (this IEnumerable<T> first, IEnumerable<T> second)
{
    var firstIter = first.GetEnumerator();
    var secondIter = second.GetEnumerator();

    while (firstIter.MoveNext() && secondIter.MoveNext())
    {
        yield return firstIter.Current;
        yield return secondIter.Current;
    }
}

Теперь, добавив в проект этот метод, вернитесь к методу Main и один раз перетасуйте колоду:Now that you've written this method, go back to the Main method and shuffle the deck once:

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

СравненияComparisons

Через сколько тасовок колода снова соберется в исходном порядке?How many shuffles it takes to set the deck back to its original order? Чтобы узнать это, вам нужно написать метод, который проверяет равенство двух последовательностей.To find out, you'll need to write a method that determines if two sequences are equal. Создав такой метод, вы поместите код тасовки колоды в цикл, в котором будете проверять, расположены ли карты в правильном порядке.After you have that method, you'll need to place the code that shuffles the deck in a loop, and check to see when the deck is back in order.

Метод, который сравнивает две последовательности, будет очень простым.Writing a method to determine if the two sequences are equal should be straightforward. По структуре он похож на метод, который мы создали для тасовки колоды.It's a similar structure to the method you wrote to shuffle the deck. Но теперь вместо команды yield return, которая возвращает элементы, вы будете сравнивать элементы каждой последовательности.Only this time, instead of yield returning each element, you'll compare the matching elements of each sequence. Если перечисление последовательности завершилось и все элементы попарно совпадают, то последовательности считаются одинаковыми:When the entire sequence has been enumerated, if every element matches, the sequences are the same:

public static bool SequenceEquals<T>
    (this IEnumerable<T> first, IEnumerable<T> second)
{
    var firstIter = first.GetEnumerator();
    var secondIter = second.GetEnumerator();

    while (firstIter.MoveNext() && secondIter.MoveNext())
    {
        if (!firstIter.Current.Equals(secondIter.Current))
        {
            return false;
        }
    }

    return true;
}

Здесь мы видим в действии второй принцип LINQ: терминальные методы.This shows a second LINQ idiom: terminal methods. Они принимают последовательность в качестве входных данных (или две последовательности, как в нашем примере) и возвращают скалярное значение.They take a sequence as input (or in this case, two sequences), and return a single scalar value. В цепочке методов для запроса LINQ терминальные методы всегда используются последними, отсюда и название "терминальный".When using terminal methods, they are always the final method in a chain of methods for a LINQ query, hence the name "terminal".

Этот принцип мы применяем при определении того, находится ли колода в исходном порядке.You can see this in action when you use it to determine when the deck is back in its original order. Поместите код тасовки в цикл, который будет останавливаться в том случае, когда порядок последовательности восстановлен. Для проверки примените метод SequenceEquals().Put the shuffle code inside a loop, and stop when the sequence is back in its original order by applying the SequenceEquals() method. Как вы уже поняли, этот метод всегда будет последним в любом запросе, поскольку он возвращает одиночное значение, а не последовательность:You can see it would always be the final method in any query, because it returns a single value instead of a sequence:

// Program.cs
static void Main(string[] args)
{
    // Query for building the deck

    // Shuffling using InterleaveSequenceWith<T>();

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

Выполните код и обратите внимание на то, как выполняется переупорядочивание колоды при каждой тасовке.Run the code you've got so far and take note of how the deck rearranges on each shuffle. После 8 тасовок (итераций цикла do-while), колода возвращается к исходной конфигурации, в которой она находилась при создании из начального запроса LINQ.After 8 shuffles (iterations of the do-while loop), the deck returns to the original configuration it was in when you first created it from the starting LINQ query.

ОптимизацияOptimizations

Пример, который вы создали к этому моменту, выполняет внутреннюю тасовку, то есть первая и последняя карты колоды сохраняют свои позиции после каждой итерации.The sample you've built so far executes an out shuffle, where the top and bottom cards stay the same on each run. Давайте внесем одно изменение: вместо этого мы будем использовать внешнюю тасовку, при которой все 52 карты изменяют свои позиции.Let's make one change: we'll use an in shuffle instead, where all 52 cards change position. Для этого колоду нужно собирать так, чтобы первой картой в колоде стала первая карта из нижней половины.For an in shuffle, you interleave the deck so that the first card in the bottom half becomes the first card in the deck. Тогда самой нижней картой станет последняя карта из верхней половины колоды.That means the last card in the top half becomes the bottom card. Это простое изменение в одной строке кода.This is a simple change to a singular line of code. Обновите текущий запрос тасовки, переключив положения Take и Skip.Update the current shuffle query by switching the positions of Take and Skip. Это поменяет местами нижнюю и верхнюю половины колоды:This will change the order of the top and bottom halves of the deck:

shuffle = shuffle.Skip(26).InterleaveSequenceWith(shuffle.Take(26));

Снова запустите программу, и вы увидите, что для восстановления исходного порядка теперь требуется 52 итерации.Run the program again, and you'll see that it takes 52 iterations for the deck to reorder itself. Также вы могли обратить внимание, что по мере выполнения программы она заметным образом замедляется.You'll also start to notice some serious performance degradations as the program continues to run.

Для этого есть сразу несколько причин.There are a number of reasons for this. Вы можете решить одну из самых существенных причин спада производительности — неэффективное использование отложенного вычисления.You can tackle one of the major causes of this performance drop: inefficient use of lazy evaluation.

Короче говоря, отложенное вычисление означает, что вычисление инструкции не выполняется, пока не понадобится ее значение.Briefly, lazy evaluation states that the evaluation of a statement is not performed until its value is needed. Запросы LINQ — это инструкции, которые обрабатываются отложенным образом.LINQ queries are statements that are evaluated lazily. Последовательности создаются только тогда, когда происходит обращение к их элементам.The sequences are generated only as the elements are requested. Обычно это дает LINQ огромное преимущество.Usually, that's a major benefit of LINQ. Но в некоторых программах, таких как в нашем примере, это приводит к экспоненциальному росту времени выполнения.However, in a use such as this program, this causes exponential growth in execution time.

Помните, что мы создали исходную колоду с помощью запроса LINQ.Remember that we generated the original deck using a LINQ query. Каждая последующая тасовка выполняет три запроса LINQ к колоде, полученной на предыдущем этапе.Each shuffle is generated by performing three LINQ queries on the previous deck. И все эти запросы выполняются отложенно.All these are performed lazily. В частности, это означает, что запросы выполняются каждый раз при обращении к последовательности.That also means they are performed again each time the sequence is requested. Таким образом, пока вы доберетесь до 52-й итерации, исходная колода будет заново создана очень много раз.By the time you get to the 52nd iteration, you're regenerating the original deck many, many times. Чтобы наглядно это продемонстрировать, давайте создадим журнал выполнения.Let's write a log to demonstrate this behavior. Затем вы исправите эту проблему.Then, you'll fix it.

В вашем файле Extensions.cs введите или скопируйте приведенный ниже метод.In your Extensions.cs file, type in or copy the method below. Этот метод расширения создает файл с именем debug.log в каталоге проекта и записывает в файл журнала, какой запрос выполняется в данный момент.This extension method creates a new file called debug.log within your project directory and records what query is currently being executed to the log file. Этот метод расширения можно добавить к любому запросу, чтобы зафиксировать его выполнение.This extension method can be appended to any query to mark that the query executed.

public static IEnumerable<T> LogQuery<T>
    (this IEnumerable<T> 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 {tag}");
    }

    return sequence;
}

Вы увидите красную волнистую линию под File, означающую, что его не существует.You will see a red squiggle under File, meaning it doesn't exist. Он не будет компилироваться, поскольку компилятор не знает, что такое File.It won't compile, since the compiler doesn't know what File is. Чтобы решить эту проблему, добавьте следующую строку кода под самой первой строкой в Extensions.cs:To solve this problem, make sure to add the following line of code under the very first line in Extensions.cs:

using System.IO;

Это позволит решить проблему, и красная линия ошибки исчезнет.This should solve the issue and the red error disappears.

Теперь давайте дополним определение каждого запроса сообщением для журнала:Next, instrument the definition of each query with a log message:

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

Обратите внимание, что запись в журнал не нужно выполнять при обращении к запросу.Notice that you don't log every time you access a query. Она выполняется только при создании исходного запроса.You log only when you create the original query. Программа по-прежнему работает очень долго, но теперь вы хорошо видите, почему.The program still takes a long time to run, but now you can see why. Если у вас не хватит терпения выполнять внешнюю тасовку с ведением журнала, переключите программу обратно на внутреннюю тасовку.If you run out of patience running the in shuffle with logging turned on, switch back to the out shuffle. На ней вы также заметите влияние отложенного вычисления.You'll still see the lazy evaluation effects. За один запуск программа выполняет 2592 запроса, если учитывать все создания мастей и достоинств.In one run, it executes 2592 queries, including all the value and suit generation.

Вы можете повысить производительность кода, чтобы уменьшить количество выполнений.You can improve the performance of the code here to reduce the number of executions you make. Простой способ исправить — кэшировать результаты исходного запроса LINQ, который создает колоду карт.A simple fix you can make is to cache the results of the original LINQ query that constructs the deck of cards. В настоящее время вы выполняете запросы снова и снова каждый раз, когда цикл do-while проходит через итерацию, повторно создавая и перетасовывая колоду карт.Currently, you're executing the queries again and again every time the do-while loop goes through an iteration, re-constructing the deck of cards and reshuffling it every time. Чтобы кэшировать колоду карт, вы можете использовать методы LINQ ToArray и ToList. Когда вы добавляете их в запросы, они будут выполнять те действия, которые вы указали, но теперь они будут хранить результаты в массиве или списке в зависимости от того, какой метод вы вызовете.To cache the deck of cards, you can leverage the LINQ methods ToArray and ToList; when you append them to the queries, they'll perform the same actions you've told them to, but now they'll store the results in an array or a list, depending on which method you choose to call. Добавьте метод LINQ ToArray в оба запроса и снова запустите программу:Append the LINQ method ToArray to both queries and run the program again:

public static void Main(string[] args)
{
    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 запросов.Now the out shuffle is down to 30 queries. Переключите программу на внешнюю тасовку, и вы заметите аналогичное улучшение: теперь выполняется 162 запроса.Run again with the in shuffle and you'll see similar improvements: it now executes 162 queries.

Обратите внимание, что этот пример лишь демонстрирует варианты использования, в которых отложенное вычисление приводит к проблемам с производительностью.Please note that this example is designed to highlight the use cases where lazy evaluation can cause performance difficulties. Хотя очень важно знать, когда отложенное вычисление может повлиять на производительность кода, не менее важно понимать, что не все запросы должны выполняться упреждающе.While it's important to see where lazy evaluation can impact code performance, it's equally important to understand that not all queries should run eagerly. Если не использовать ToArray, производительность снизится. Это связано с тем, что каждое новое расположение карт вычисляется на основе предыдущего расположения.The performance hit you incur without using ToArray is because each new arrangement of the deck of cards is built from the previous arrangement. Использование отложенного вычисления означает, что каждое расположение колоды строится с самого начала, из исходной колоды, включая вызов кода для создания startingDeck.Using lazy evaluation means each new deck configuration is built from the original deck, even executing the code that built the startingDeck. Это создает огромный объем дополнительной работы.That causes a large amount of extra work.

На практике некоторые алгоритмы хорошо работают с упреждающим вычислением, а другие хорошо выполняются с отложенным вычислением.In practice, some algorithms run well using eager evaluation, and others run well using lazy evaluation. Для ежедневного использования отложенное вычисление обычно дает более хороший результат, если в качестве источника данных используется отдельный процесс, например база данных.For daily usage, lazy evaluation is usually a better choice when the data source is a separate process, like a database engine. Для баз данных отложенное вычисление позволяет сложным запросам выполнять только один круговой путь к процессу базы данных и обратно к оставшемуся коду.For databases, lazy evaluation allows more complex queries to execute only one round trip to the database process and back to the rest of your code. LINQ является гибким, независимо от того, используете ли вы отложенное или упреждающее вычисление, поэтому измерьте процессы и выберите тип вычислений, который обеспечивает наилучшую производительность.LINQ is flexible whether you choose to utilize lazy or eager evaluation, so measure your processes and pick whichever kind of evaluation gives you the best performance.

ЗаключениеConclusion

В этом проекте вы изучили:In this project, you covered:

  • использование запросов LINQ для агрегирования данных в осмысленную последовательность;using LINQ queries to aggregate data into a meaningful sequence
  • запись методов расширения для добавления собственных пользовательских функций в запросы LINQ;writing Extension methods to add our own custom functionality to LINQ queries
  • поиск областей в коде, где могут возникнуть проблемы с производительностью наших запросов LINQ, например снижение скорости;locating areas in our code where our LINQ queries might run into performance issues like degraded speed
  • упреждающее и отложенное вычисление в отношении запросов LINQ и их влияние на производительность запросов.lazy and eager evaluation in regards to LINQ queries and the implications they might have on query performance

Помимо LINQ вы узнали об использовании метода, который иллюзионисты используют для карточных фокусов.Aside from LINQ, you learned a bit about a technique magicians use for card tricks. Они используют тасовку по методу Фаро, потому что она позволяет хорошо контролировать положение каждой карты в колоде.Magicians use the Faro shuffle because they can control where every card moves in the deck. Теперь, когда вы все это знаете, не рассказывайте это остальным!Now that you know, don't spoil it for everyone else!

Дополнительные сведения о LINQ см. в следующих статьях:For more information on LINQ, see: