遅延評価のセマンティクス
イテレーターの遅延実行について理解します。
「遅延評価のセマンティクス」はCoddyKit上の無料C# Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはC# Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 C# Academyコースには全4レッスンが含まれています。
遅延実行
イテレーターと LINQ クエリは遅延実行を使用します。クエリを定義しただけでは、ほとんど何も行われません。実際に結果を列挙するときに、項目ごとに処理が実行されます。
列挙するまで何も実行されない
イテレーターメソッドを呼び出しても、その本体は実行されません。本体の実行が始まるのは、foreach(または別のコンシューマー)が最初の値を取得するときです。
using System;
using System.Collections.Generic;
public class Program
{
static IEnumerable<int> Numbers()
{
Console.WriteLine("-- iterator body started --");
yield return 1;
yield return 2;
}
public static void Main()
{
var seq = Numbers();
Console.WriteLine("query created, body not run yet");
foreach (var n in seq) Console.WriteLine(n);
}
}値は一度に 1 つずつ生成される
反復するたびに、イテレーターは次の値を生成するのに必要な分だけ実行されます。このプルベースのモデルにより、非常に大きなシーケンスでもメモリ使用量を低く抑えられます。
using System;
using System.Collections.Generic;
public class Program
{
static IEnumerable<int> Trace()
{
for (int i = 1; i <= 3; i++)
{
Console.WriteLine("producing " + i);
yield return i;
}
}
public static void Main()
{
foreach (var n in Trace())
Console.WriteLine("consumed " + n);
}
}再列挙すると再実行される
遅延シーケンスには、キャッシュされた結果がありません。2 回列挙するとイテレーターが 2 回実行されるため、ソースが変更される可能性がある場合や処理コストが高い場合には注意が必要です。
using System;
using System.Collections.Generic;
public class Program
{
static int _calls = 0;
static IEnumerable<int> Counter()
{
_calls++;
yield return _calls;
}
public static void Main()
{
var seq = Counter();
foreach (var n in seq) Console.WriteLine(n);
foreach (var n in seq) Console.WriteLine(n);
Console.WriteLine("total runs: " + _calls);
}
}変化する状態を取得する
実行が遅延されるため、イテレーターはクエリの定義時ではなく列挙時にソースを読み取ります。クエリを定義した後に行われた変更も結果に反映されます。
using System;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
var list = new List<int> { 1, 2 };
IEnumerable<int> doubled = Double(list);
list.Add(3); // added before enumeration
foreach (var n in doubled) Console.WriteLine(n);
}
static IEnumerable<int> Double(IEnumerable<int> nums)
{
foreach (var n in nums) yield return n * 2;
}
}即時実行を強制する
結果のスナップショットを作成するには、ToList() または ToArray() を使ってシーケンスをマテリアライズします。これによりイテレーターが 1 回実行され、値が保存されるため、その後のソースの変更から切り離せます。
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main()
{
var list = new List<int> { 1, 2 };
List<int> snapshot = Double(list).ToList(); // executed now
list.Add(99);
Console.WriteLine(string.Join(",", snapshot));
}
static IEnumerable<int> Double(IEnumerable<int> nums)
{
foreach (var n in nums) yield return n * 2;
}
}短絡評価で処理を節約する
コンシューマーは値を遅延取得するため、最初に一致した項目を探す操作などでは早期に停止できます。生成側は、要求されない値を計算しません。
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
static IEnumerable<int> Numbers()
{
for (int i = 1; ; i++)
{
Console.WriteLine("checking " + i);
yield return i;
}
}
public static void Main()
{
int firstBig = Numbers().First(n => n > 3);
Console.WriteLine("found " + firstBig);
}
}遅延される例外
イテレーター内の例外は、メソッドの呼び出し時ではなく、列挙が問題の行に到達したときにスローされます。最初に検証されると考えていると、意外に感じることがあります。
using System;
using System.Collections.Generic;
public class Program
{
static IEnumerable<int> Risky(int divisor)
{
yield return 10 / divisor; // throws only during enumeration
}
public static void Main()
{
var seq = Risky(0);
Console.WriteLine("no error yet");
try { foreach (var n in seq) Console.WriteLine(n); }
catch (DivideByZeroException) { Console.WriteLine("caught during enumeration"); }
}
}遅延パイプラインは低コストで構成できる
遅延演算子を連鎖させても、各演算子が繰り返し実行されるわけではありません。各値は要求に応じてパイプライン全体を 1 回だけ通過するため、効率がよく、メモリにも優しい方法です。
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main()
{
var result = Enumerable.Range(1, 1000000)
.Where(n => n % 2 == 0)
.Select(n => n * n)
.Take(3);
foreach (var n in result) Console.WriteLine(n);
}
}いつマテリアライズするか
複数回列挙する場合、ソースが変更される可能性がある場合、またはリソースを破棄する前に結果を取得しておく必要がある場合は、マテリアライズします。それ以外の場合は、メモリと処理を節約するために遅延実行のままにします。
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main()
{
var evens = Enumerable.Range(1, 10).Where(n => n % 2 == 0).ToList();
// Safe to enumerate many times now
Console.WriteLine("count: " + evens.Count);
Console.WriteLine("sum: " + evens.Sum());
}
}自分で試す
遅延実行を直接確認してみましょう。クエリを定義し、ソースを変更すると、列挙した時点で初めて変更が結果に現れます。
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main()
{
var source = new List<int> { 1, 2, 3 };
var query = source.Where(n => n > 1); // not executed yet
source.Add(10);
source.Remove(2);
// Executes now, reflecting all changes
Console.WriteLine(string.Join(", ", query));
}
}クイックチェック
遅延実行について考えてみてください。
まとめ
遅延評価では、列挙されたときにだけシーケンスが実行されます。
- クエリを定義しただけでは処理されず、消費したときに処理されます。
- 再列挙すると、イテレーターが再実行されます。
- ソースは列挙時に読み取られるため、その後の変更が反映されます。
- 例外は列挙中に発生します。
- 必要に応じて
ToList()/ToArray()を使い、結果をマテリアライズします。
よくある質問
「遅延評価のセマンティクス」レッスンは無料ですか?
はい。「遅延評価のセマンティクス」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、C# Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 C# Academyコースには全4レッスンが含まれています。
「遅延評価のセマンティクス」で何を学びますか?
イテレーターの遅延実行について理解します。 ブラウザで直接実行するハンズオンコードでC# Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
C# Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのC# Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。
「遅延評価のセマンティクス」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このC# Academyレッスンでコードを書いて実行できますか?
はい。すべてのC# Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。