【C#】yield returnを使ってIEnumerableを返すイテレータメソッドを実装する

C#において、コレクションやシーケンス(データの並び)を返すメソッドを実装する場合、List<T>などのコレクションを生成して返す代わりに、yield return構文を使用することが推奨されるケースが多くあります。

yieldを使用すると、コンパイラが自動的にステートマシンを生成し、呼び出し側が必要とするタイミングで値を一つずつ生成する「遅延評価(Lazy Evaluation)」が実現できます。これにより、無限に続く数列を表現したり、メモリ消費を最小限に抑えたりすることが可能になります。

ここでは、2の累乗(1, 2, 4, 8…)を計算するシーケンスを題材に、yield returnによるイテレータの実装方法と、yield breakによる終了条件の記述について解説します。


目次

yield returnの仕組み

yield returnを含むメソッドは、戻り値をIEnumerable<T>(またはIEnumerator<T>)として定義します。このメソッドが呼び出された時点では実際の処理は実行されず、foreachループなどで要素が要求されたタイミングで初めて実行が開始されます。

  1. yield return 値;: 値を呼び出し元に返し、現在の処理位置を保存して一時停止します。
  2. yield break;: シーケンスの生成を終了します(反復処理の完了)。

実践的なコード例:2の累乗シーケンスの生成

以下のコードは、1から始まり、前回の値を2倍にした数値を次々と返すイテレータメソッドの実装例です。long型の範囲を超える(オーバーフローする)直前で生成を停止する安全装置も組み込んでいます。

using System;
using System.Collections.Generic;
using System.Linq;

namespace IteratorSample
{
    class Program
    {
        static void Main()
        {
            // シナリオ:
            // 2の累乗(Powers of 2)のシーケンスを生成し、条件に合わせて利用する。
            // GeneratePowersOfTwoメソッド自体は無限ループを含んでいるが、
            // 呼び出し側(LINQ)で制御するため、必要な分しか計算されない。

            var powers = GeneratePowersOfTwo()
                .Select((Value, Index) => new { Index, Value }) // インデックスを付与
                .TakeWhile(x => x.Index <= 10); // 最初の11個(2^0 ~ 2^10)だけ取得

            Console.WriteLine("--- 2の累乗シーケンス (2^0 ~ 2^10) ---");
            foreach (var item in powers)
            {
                Console.WriteLine($"2^{item.Index} = {item.Value}");
            }

            Console.WriteLine("\n--- オーバーフロー直前まで列挙 ---");
            // long型の限界まで列挙する場合
            // GeneratePowersOfTwo側で yield break しているので無限ループにはなりません。
            int count = 0;
            foreach (var value in GeneratePowersOfTwo())
            {
                count++;
            }
            Console.WriteLine($"生成された総数: {count} 個");
        }

        /// <summary>
        /// 2の累乗を順次返すイテレータメソッド
        /// </summary>
        /// <returns>long型のシーケンス</returns>
        static IEnumerable<long> GeneratePowersOfTwo()
        {
            long current = 1;

            // 2^0 (= 1) を返す
            yield return current;

            // 無限ループの形をとっていますが、yield return で都度停止します。
            while (true)
            {
                // 次の値を計算する前にオーバーフローをチェック
                // current * 2 が long.MaxValue を超える場合は終了
                if (current > long.MaxValue / 2)
                {
                    // イテレーションを終了する
                    yield break;
                }

                current *= 2;

                // 現在の値を返し、処理を一時停止する
                yield return current;
            }
        }
    }
}

実行結果

--- 2の累乗シーケンス (2^0 ~ 2^10) ---
2^0 = 1
2^1 = 2
2^2 = 4
2^3 = 8
2^4 = 16
2^5 = 32
2^6 = 64
2^7 = 128
2^8 = 256
2^9 = 512
2^10 = 1024

--- オーバーフロー直前まで列挙 ---
生成された総数: 63 個

技術的なポイント

1. メモリ効率の向上

通常のメソッドでList<long>を作成して返す場合、すべての計算結果をメモリ上に保持する必要があります。しかし、yieldを使用した場合、現在の値と状態のみがメモリに保持されるため、大量のデータ(あるいは無限のデータ)を扱う際に非常にメモリ効率が良くなります。

2. LINQとの親和性

yield returnで実装されたメソッドはIEnumerable<T>を返すため、LINQのメソッド(Take, Where, Selectなど)と直接組み合わせることができます。 上記の例では、GeneratePowersOfTwoメソッド内で「どこまで計算するか」を指定せず、呼び出し側の.TakeWhile(...)で必要なデータ量を決定しています。このように、データの「生成」と「利用」のロジックを分離できるのがイテレータの大きな利点です。

3. オーバーフロー対策とyield break

無限シーケンスを扱う場合でも、安全のために終了条件を設けることが重要です。上記のコードでは、long型の最大値を超える計算を行う前にyield breakを実行し、シーケンスを正常に終了させています。これにより、呼び出し側がTakeなどで制限し忘れた場合でも、プログラムがクラッシュしたり無限ループに陥ったりするのを防ぎます。

まとめ

yield returnを使用することで、複雑なステートマシンを記述することなく、効率的で読みやすいイテレータを作成できます。巨大なファイルの一行ずつの読み込みや、計算コストの高いシーケンス生成など、データを「ストリーム」として扱いたい場合に積極的に活用してください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次