LINQ の使用Working with LINQ

はじめにIntroduction

このチュートリアルでは、.NET Core と C# 言語の機能を説明します。This tutorial teaches you features in .NET Core and the C# language. 内容は以下のとおりです。You’ll learn:

  • LINQ を使用してシーケンスを生成する方法。How to generate sequences with LINQ.
  • LINQ クエリで簡単に使用できるメソッドを記述する方法How to write methods that can be easily used in LINQ queries.
  • 先行評価と遅延評価を区別する方法How to distinguish between eager and lazy evaluation.

これらの方法を、マジシャンの基本的スキルの 1 つであるファロ― シャッフルを再現するアプリケーションを作成しながら学習していきます。You'll learn these techniques by building an application that demonstrates one of the basic skills of any magician: the faro shuffle. ファロ― シャッフルとは、簡単に言うと、カード デッキを正確に 2 等分し、双方のデッキから 1 枚ずつ交互に並ぶようにシャッフルして、元のデッキを並べ替えることです。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 will construct a card deck, and then perform 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. GitHub の dotnet/samples リポジトリでは、完全版のサンプルを確認することもできます。You can also see the completed sample in the dotnet/samples GitHub repository. ダウンロード方法については、「サンプルおよびチュートリアル」を参照してください。For download instructions, see Samples and Tutorials.

必須コンポーネントPrerequisites

お使いのコンピューターを、.NET Core が実行されるように設定する必要があります。You’ll need to setup 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, 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.

データ セットの作成Creating the Data Set

開始する前に、dotnet new console によって生成された Program.cs ファイルの上部に次の行があることを確認してください。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;

これら 3 つの行 (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. 一般的に、1 組のトランプ カードには 4 種類の絵札 (スート) があり、スートごとに 13 個の値があります。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 クラスを作成する代わりに、それぞれがスートとランクを表す 2 つのシーケンスを作成できます。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";
}

Program.cs ファイル内の Main メソッドの下にこれらを配置します。Place these underneath the Main method in your Program.cs file. これら 2 つのメソッドは両方とも、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. Main メソッドの中に LINQ クエリを配置します。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 句は 1 つの SelectMany を生成し、これが、最初のシーケンス内の各要素と 2 番目のシーケンス内の各要素とを組み合わせた 1 つのシーケンスを作成します。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. 最初のソース シーケンス (Suits) 内の最初の要素が、2 番目のシーケンス (Ranks) のすべての要素と組み合わされます。The first element in the first source sequence (Suits) is combined with every element in the second sequence (Ranks). これで、最初のスートのカード 13 枚すべてが生成されます。This produces all thirteen cards of first suit. このプロセスを、最初のシーケンス (Suits) 内の各要素について繰り返します。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. そのため、選択した構文に関係なく、クエリの 2 つのバージョンでは同じ結果が生成されます。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 枚のカードを書き出しているコンソール ウィンドウ

順序の操作Manipulating the Order

次に、カード デッキをどのようにシャッフルするかに注目します。Next, focus on how you're going to shuffle the cards in the deck. 適切なシャッフルの最初の手順は、カード デッキを 2 つの山に分けることです。The first step in any good shuffle is to split the deck in two. LINQ API の一部である Take メソッドと Skip メソッドの機能を利用できます。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.

LINQ クエリから返される IEnumerable<T> を操作する方法に対していくつかの機能を追加するには、拡張メソッドと呼ばれる特別な種類のメソッドをいくつか記述する必要があります。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 という名前の新しい static クラス ファイルをプログラムに追加した後、最初の拡張メソッドをビルドすることで、自分の拡張メソッドに新しいホームを与えます。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. コードでは、これは、TakeSkip を同時に実行し、要素を interleaving して取得したシーケンスの両方を列挙し、1 つのシーケンス (今シャッフルが行なわれたカード デッキ) を作成することを意味します。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. 2 つのシーケンスで動作する LINQ メソッドを作成するには、IEnumerable<T> がどのように動作するかを理解する必要があります。Writing a LINQ method that works with two sequences requires that you understand how IEnumerable<T> works.

IEnumerable<T> インターフェイスには GetEnumerator のメソッドが 1 つあります。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. これら 2 つのメンバーを使用して、コレクションを列挙し、要素を返します。You will use those two members to enumerate the collection and return the elements. このインターリーブ メソッドは反復子メソッドになるので、コレクションを作成してそのコレクションを返す代わりに、上記の 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 メソッドに戻り、デッキを 1 回シャッフルします。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? これを見つけるには、2 つのシーケンスが等しいかどうかを判断するメソッドを記述する必要があります。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.

2 つのシーケンスが等しいかどうかを判断するメソッドの記述は簡単です。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;
}

これは 2 つ目の LINQ の表現形式であるターミナル メソッドを示しています。This shows a second LINQ idiom: terminal methods. これらは、シーケンスを入力として受け取り (この場合 2 つのシーケンス)、単一のスカラー値を返します。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 loop の繰り返し) 後に、デッキは、開始時の 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. ここで 1 つ変更を加えましょう。"イン シャッフル " を代わりに使用します。ここでは、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. これは、1 行のコードを変更する単純な変更です。This is a simple change to a singular line of code. TakeSkip の位置を入れ替えることで、現在のシャッフル クエリを更新します。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. このパフォーマンスの低下の主な原因の 1 つを説明できます。それは、"遅延評価" の非効率的な使用です。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 クエリを 3 回実行することによって生成されます。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. 1 回の実行で、すべての値とスートの生成を含めてクエリを 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 loop が繰り返されるたびに、何度もクエリが実行され、カード デッキが再構築され、シャッフルが毎回実行されています。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 メソッドの ToArrayToList を活用できます。それらをクエリに追加すると、実行するように指示したのと同じアクションが実行されますが、今回は、呼び出すように選択したメソッドに応じて、結果が配列または一覧内に格納されます。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. データベースでは、遅延評価を使用すると、より複雑なクエリがデータベース プロセスに対して 1 往復だけ実行された後、残りのコードに戻ることができます。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: