イントロダクション
このチュートリアルでは、.NET と C# 言語の機能について説明します。 次の方法を学びます:
- LINQ を使用してシーケンスを生成します。
- LINQ クエリで簡単に使用できるメソッドを記述します。
- 先行評価と遅延評価を区別する。
あなたは、任意の魔法使いの基本的なスキルの1つを示すアプリケーションを構築することによって、これらの技術を学びます: ファロシャッフル. ファロシャッフルは、カードデッキを半分に正確に分割し、シャッフルが各カードを各半分からインターリーブして元のデッキを再構築するテクニックです。
各カードが各シャッフルの後に既知の場所にあり、順序が繰り返しパターンであるため、魔法使いはこの手法を使用します。
このチュートリアルでは、一連のデータを操作する方法について、軽快に説明します。 アプリケーションはカード デッキを構築し、シャッフルのシーケンスを実行し、毎回シーケンスを書き込みます。 また、更新された注文と元の注文が比較されます。
このチュートリアルには複数の手順があります。 各手順の後、アプリケーションを実行して進行状況を確認できます。 完成したサンプルは、dotnet/samples GitHub リポジトリでも確認できます。 ダウンロード手順については、サンプルとチュートリアルを参照してください。
[前提条件]
- 最新の .NET SDK
- Visual Studio Code エディター
- C# DevKit
アプリケーションを作成する
新しいアプリケーションを作成します。 コマンド プロンプトを開き、アプリケーションの新しいディレクトリを作成します。 現在のディレクトリにします。 コマンド プロンプトでコマンド dotnet new console -o LinqFaroShuffle を入力します。 このコマンドは、基本的な "Hello World" アプリケーションのスターター ファイルを作成します。
C# を使用したことがない場合は、 このチュートリアル で C# プログラムの構造について説明します。 その資料を読んでから、LINQ についてもっと学ぶためにここに戻ってください。
データ セットを作成する
ヒント
このチュートリアルでは、サンプル コードと一致するように LinqFaroShuffle という名前空間にコードを整理するか、既定のグローバル名前空間を使用できます。 名前空間を使用する場合は、すべてのクラスとメソッドが同じ名前空間内に一貫していることを確認するか、必要に応じて適切な using ステートメントを追加します。
カードのデッキを構成するものを考えてみましょう。 カードのデッキには4つのスーツがあり、各スーツには13の値があります。 通常は、 Card クラスをすぐに作成し、 Card オブジェクトのコレクションを手動で設定することを検討できます。 LINQ を使用すると、カードのデッキを作成する通常の方法よりも簡潔にすることができます。
Card クラスを作成する代わりに、スーツとランクを表す 2 つのシーケンスを作成します。 文字列形式のIEnumerable<T>としてランクとスートを生成するイテレーターメソッドのペアを作成します。
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";
}
これらのメソッドは、Console.WriteLine ファイルの Program.cs ステートメントの下に配置します。 これら 2 つのメソッドはどちらも、 yield return 構文を使用して、実行時にシーケンスを生成します。 コンパイラは、 IEnumerable<T> を実装し、要求に応じて文字列のシーケンスを生成するオブジェクトをビルドします。
次に、これらの反復子メソッドを使用してカードのデッキを作成します。 LINQ クエリを Program.cs ファイルの先頭に配置します。 次のように表示されます。
var startingDeck = from s in Suits()
from r in Ranks()
select (Suit: s, Rank: r);
// Display each card that's generated and placed in startingDeck
foreach (var card in startingDeck)
{
Console.WriteLine(card);
}
複数の from 句によって SelectManyが生成され、最初のシーケンス内の各要素と 2 番目のシーケンス内の各要素を組み合わせた 1 つのシーケンスが作成されます。 この例では順序が重要です。 最初のソース シーケンス (Suits) の最初の要素は、2 番目のシーケンス (Ranks) のすべての要素と結合されます。 このプロセスは、最初のスーツのすべての13枚のカードを生成します。 最初のシーケンス (Suits) の各要素でこのプロセスは繰り返されます。 その結果、はじめにスート順に並んで、次に値の順に並んだカード デッキができます。
前のサンプルで使用したクエリ構文で LINQ を記述する場合でも、代わりにメソッド構文を使用する場合でも、常に構文の形式から他の形式に移動できます。 クエリ構文で記述された上記のクエリは、メソッド構文で次のように記述できます。
var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => (Suit: suit, Rank: rank )));
コンパイラは、クエリ構文で記述された LINQ ステートメントを同等のメソッド呼び出し構文に変換します。 したがって、構文の選択に関係なく、クエリの 2 つのバージョンで同じ結果が生成されます。 状況に最適な構文を選択します。 たとえば、一部のメンバーがメソッド構文に問題があるチームで作業している場合は、クエリ構文の使用を好みます。
この時点でビルドしたサンプルを実行します。 デッキに全52枚のカードが表示されます。 デバッガーでこのサンプルを実行して、 Suits() メソッドと Ranks() メソッドの実行方法を確認すると便利な場合があります。 各シーケンス内の各文字列が必要に応じてのみ生成されることを明確に確認できます。
順序を操作する
次に、デッキのカードをシャッフルする方法に焦点を当てます。 良いシャッフルの最初のステップは、デッキを2つに分割することです。 LINQ API の一部である Take メソッドと Skip メソッドは、その機能を提供します。
foreach ループの後に配置します。
var top = startingDeck.Take(26);
var bottom = startingDeck.Skip(26);
ただし、標準ライブラリではシャッフルメソッドを利用できないため、独自の方法を記述する必要があります。 作成するシャッフル メソッドは、LINQ ベースのプログラムで使用するいくつかの手法を示しているため、このプロセスの各部分について手順で説明します。
LINQ クエリの IEnumerable<T> 結果を操作する方法に機能を追加するには、 拡張メソッドと呼ばれる特別な種類のメソッドを記述します。 拡張メソッドは、機能を追加する元の型を変更することなく、既存の型に新しい機能を追加する特殊な目的の 静的メソッド です。
という名前の新しいExtensions.csクラス ファイルをプログラムに追加して拡張メソッドに新しいホームを与え、最初の拡張メソッドのビルドを開始します。
public static class CardExtensions
{
extension<T>(IEnumerable<T> sequence)
{
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
{
// Your implementation goes here
return default;
}
}
}
注
Visual Studio 以外のエディター (Visual Studio Code など) を使用している場合は、拡張メソッドにアクセスできるようにusing LinqFaroShuffle; ファイルの先頭にを追加することが必要になる場合があります。 この using ステートメントは Visual Studio によって自動的に追加されますが、他のエディターでは追加されない場合があります。
extension コンテナーは、拡張する型を指定します。
extension ノードは、 コンテナー内のすべてのメンバーのextensionの型と名前を宣言します。 この例では、 IEnumerable<T>を拡張しており、パラメーターの名前は sequence です。
拡張メンバー宣言は、受信側の型のメンバーであるかのように表示されます。
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
拡張型のメンバーメソッドであるかのように、メソッドを呼び出します。 このメソッド宣言は、入力と出力の型が IEnumerable<T>される標準的なイディオムにも従います。 これにより、LINQ メソッドを連結して、より複雑なクエリを実行できます。
デッキを半分に分割するので、それらの半分を一緒に結合する必要があります。 コードでは、つまり、 Take と Skip で取得したシーケンスの両方を一度に列挙し、要素 をインターリーブ し、1 つのシーケンス (現在シャッフルされたカードのデッキ) を作成します。 2 つのシーケンスで動作する LINQ メソッドを記述するには、 IEnumerable<T> のしくみを理解する必要があります。
IEnumerable<T> インターフェイスには、GetEnumeratorという 1 つの方法があります。
GetEnumeratorによって返されるオブジェクトには、次の要素に移動するメソッドと、シーケンス内の現在の要素を取得するプロパティがあります。 これら 2 つのメンバーを使用してコレクションを列挙し、要素を返します。 このインターリーブ メソッドは反復子メソッドであるため、コレクションを構築してコレクションを返す代わりに、前のコードに示した yield return 構文を使用します。
そのメソッドの実装を次に示します。
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
{
var firstIter = sequence.GetEnumerator();
var secondIter = second.GetEnumerator();
while (firstIter.MoveNext() && secondIter.MoveNext())
{
yield return firstIter.Current;
yield return secondIter.Current;
}
}
このメソッドを記述したら、 Main メソッドに戻り、デッキを 1 回シャッフルします。
var shuffledDeck = top.InterleaveSequenceWith(bottom);
foreach (var c in shuffledDeck)
{
Console.WriteLine(c);
}
比較
デッキを元の順序に戻すために必要なシャッフルの数を決定します。 調べるには、2 つのシーケンスが等しいかどうかを判断するメソッドを記述します。 そのメソッドを作成したら、デッキをシャッフルするコードをループに配置し、デッキが順序に戻るタイミングを確認します。
2 つのシーケンスが等しいかどうかを判断するメソッドを記述するのは簡単です。 これは、デッキをシャッフルするために記述したメソッドと同様の構造です。 ただし、今回は、各要素に yield return を使用する代わりに、各シーケンスの一致する要素を比較します。 シーケンス全体が列挙されるとき、すべての要素が一致する場合、シーケンスは同じです。
public bool SequenceEquals(IEnumerable<T> second)
{
var firstIter = sequence.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;
}
このメソッドは、2 つ目の LINQ イディオムであるターミナル メソッドを示しています。 入力としてシーケンス (この場合は 2 つのシーケンス) を受け取り、1 つのスカラー値を返します。 ターミナル メソッドを使用する場合、それらは常に LINQ クエリのメソッドチェーンの最後のメソッドになります。
これを使用して、デッキが元の順序に戻るタイミングを判断するときに、これを実際に確認できます。 シャッフル コードをループ内に配置し、 SequenceEquals() メソッドを適用してシーケンスが元の順序に戻ったときに停止します。 シーケンスの代わりに単一の値が返されるため、クエリでは常に最後のメソッドになることがわかります。
var startingDeck = from s in Suits()
from r in Ranks()
select (Suit: s, Rank: r);
// Display each card generated and placed in startingDeck in the console
foreach (var card in startingDeck)
{
Console.WriteLine(card);
}
var top = startingDeck.Take(26);
var bottom = startingDeck.Skip(26);
var shuffledDeck = top.InterleaveSequenceWith(bottom);
var times = 0;
// Re-use the shuffle variable from earlier, or you can make a new one
shuffledDeck = startingDeck;
do
{
shuffledDeck = shuffledDeck.Take(26).InterleaveSequenceWith(shuffledDeck.Skip(26));
foreach (var card in shuffledDeck)
{
Console.WriteLine(card);
}
Console.WriteLine();
times++;
} while (!startingDeck.SequenceEquals(shuffledDeck));
Console.WriteLine(times);
これまでにビルドしたコードを実行し、各シャッフルでデッキがどのように再配置されるかを確認します。 8 回シャッフルした後 (do-while ループの反復)、開始 LINQ クエリから最初に作成した元の構成にデッキが戻ります。
最適化
これまでに作成したサンプルではアウト シャッフルが実行され、各実行で上と下のカードは同じままです。 1 つの変更を加えてみましょう。代わりに イン シャッフル を使用します。52 枚のカードすべてが位置を変更します。 インシャッフルでは、デッキをインターリーブして、下半分の最初のカードがデッキの最初のカードになるようにします。 つまり、上半分の最後のカードが下のカードになります。 この変更には、1 行のコードが必要です。 TakeとSkipの位置を切り替えて、現在のシャッフル クエリを更新します。 この変更により、デッキの上半分と下半分の順序が切り替わります。
shuffledDeck = shuffledDeck.Skip(26).InterleaveSequenceWith(shuffledDeck.Take(26));
プログラムをもう一度実行すると、デッキの順序が変更されるまでに 52 回の反復が必要になります。 また、プログラムの実行が続くと、パフォーマンスが大幅に低下します。
このパフォーマンス低下には、いくつかの理由があります。 レイジー評価の非効率的な使用という主な原因の 1 つに取り組むことができます。
遅延評価では、ステートメントの評価は、その値が必要になるまで実行されないと示されます。 遅延して評価されているステートメントは LINQ クエリです。 シーケンスは、要素が要求されたときにのみ生成されます。 通常、これは LINQ の主な利点です。 ただし、このようなプログラムでは、遅延評価によって実行時間が指数関数的に増加します。
LINQ クエリを使用して元のデッキを生成したことを思い出してください。 各シャッフルは、前のデッキで 3 つの LINQ クエリを実行することによって生成されます。 これらのクエリはすべて遅延実行されます。 これは、シーケンスが要求されるたびに再度実行されることを意味します。 52 回目のイテレーションに到達する頃には、元のデッキを何度も再生成しています。 この動作を示すログを書き込みます。 データを収集したら、パフォーマンスを向上させることができます。
Extensions.cs ファイルで、次のコード サンプルのメソッドを入力するか、コピーします。 この拡張メソッドは、プロジェクト ディレクトリ内に debug.log という名前の新しいファイルを作成し、現在実行されているクエリをログ ファイルに記録します。 この拡張メソッドを任意のクエリに追加して、クエリが実行されたことをマークします。
public IEnumerable<T> LogQuery(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;
}
次に、ログ メッセージを使用して各クエリの定義をインストルメント化します。
var startingDeck = (from s in Suits().LogQuery("Suit Generation")
from r in Ranks().LogQuery("Rank Generation")
select (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);
クエリにアクセスするたびにログを記録しないことに注意してください。 ログに記録されるのは、元のクエリを作成した場合のみです。 プログラムの実行にはまだ時間がかかりますが、その理由がわかります。 ログを記録しながらインシャッフルを実行するのに我慢できなくなった場合は、アウトシャッフルに戻してください。 遅延評価の効果は引き続き見られます。 1 回の実行で、値とスーツの生成を含む 2,592 個のクエリが実行されます。
コードのパフォーマンスを向上させ、実行回数を減らすことができます。 簡単な修正は、カードのデッキを構築する元の LINQ クエリの結果を キャッシュ することです。 現在、do-while ループが繰り返し実行されるたびにクエリを繰り返し実行し、カードのデッキを再構築し、毎回再シャッフしています。 カードのデッキをキャッシュするには、LINQ メソッドを ToArray および ToList適用します。 クエリに追加すると、ユーザーが指定したのと同じアクションが実行されますが、呼び出すメソッドに応じて、結果が配列またはリストに格納されます。 LINQ メソッド ToArray を両方のクエリに追加し、プログラムをもう一度実行します。
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 個のクエリにダウンしました。 in シャッフルでもう一度実行すると、同様の改善が見られます。162 個のクエリが実行されるようになりました。
この例は、遅延評価によってパフォーマンスの問題が発生する可能性があるユース ケースを強調 するように設計されています 。 遅延評価がコードのパフォーマンスに影響する可能性がある場所を確認することは重要ですが、すべてのクエリを熱心に実行する必要はないことを理解することも同様に重要です。
ToArrayを使用せずに発生するパフォーマンスのヒットは、カードのデッキの各新しい配置が前の配置から構築されているためです。 レイジー評価を使用すると、新しいデッキ構成が元のデッキからビルドされ、 startingDeckをビルドしたコードも実行されます。 これにより、大量の余分な作業が発生します。
実際には、先行評価を使用すると効率的に動作するアルゴリズムもあれば、遅延評価を使用したほうがよいアルゴリズムもあります。 日常の使用では、データ ソースがデータベース エンジンのように個別のプロセスである場合は、遅延評価のほうが通常は適しています。 データベースの場合、遅延評価では、より複雑なクエリでデータベース プロセスへのラウンド トリップを 1 回だけ実行し、残りのコードに戻すことができます。 LINQ は、遅延評価と一括評価のどちらを使用するかを柔軟に選択できるため、プロセスを測定し、どの評価を選択しても最適なパフォーマンスが得られます。
結論
このプロジェクトでは、次の内容について説明しました。
- LINQ クエリを使用してデータを意味のあるシーケンスに集計する。
- LINQ クエリにカスタム機能を追加するための拡張メソッドの記述。
- LINQ クエリで速度低下などのパフォーマンスの問題が発生する可能性があるコード内の領域を見つけます。
- LINQ クエリの遅延評価および即時評価がクエリのパフォーマンスに与える可能性のある影響。
LINQ とは別に、魔法使いがカードのトリックに使用する手法について学習しました。 魔法使いは、すべてのカードがデッキ内で移動する場所を制御できるため、ファロシャッフルを使用します。 知ったのだから、他の人のためにそれを台無しにしないでください!
LINQ の詳細については、次を参照してください。
.NET