yield キーワード


列挙の簡易実装

foreach 文などで利用できるコレクション型は、System.Collection 名前空間の IEnumerable インタフェースを実装しているオブジェクトのことを表します。 例えば配列も、この IEnumerable インタフェースの実装の一つです。

IEnumerable はインタフェースなので、これを実装すれば独自のコレクション型を定義することができます。 コレクション型の基本的な概念は、先頭から末尾までのデータの列のことで、その多くは動的、または静的なインデックスで管理された配列となるでしょう。

しかし、foeeach で処理できるような IEnumerable インタフェースの実装を作成するのは多少煩雑でした。 IEnumerable インタフェースは、同じく System.Collection 名前空間の IEnumerator インタフェースを実装するオブジェクトを返す GetEnumerator() メソッドのみを宣言しています。

IEnumerator GetEnumerator();

そのため、このインタフェースを実装するクラスは、内部で IEnumerator を実装するオブジェクトを作成し、それを GetEnumerator() メソッドで返さなければなりませんでした。 IEnumerator インタフェースは、現在の要素を返す Current プロパティと、次の要素に移動する Move() メソッド、現在の位置を初期化する Reset() メソッドを宣言しています。 これを実装するクラスは、コレクションデータを内部でインデックス管理しなければなりません。

object Current {get;}
bool MoveNext();
void Reset();

もちろん、経験豊富な実践力のあるプログラマであれば、その程度の実装は設計書を起こすことなく作成することができるでしょう。 しかし、どうやら Microsoft の設計者たちは、単純なデータの列を表すオブジェクトを作成するために 2 つのインタフェースを実装させるのは、やや冗長と考えたようです。 これを簡素化し、IEnumerable または IEnumerator をより直感的に作成できる仕組みが C# 2.0 で追加された yield キーワードです。

yield キーワードを使う場所は限られていて、IEnumerator または IEnumerable インタフェースを戻り値するイテレータブロックと呼ばれる中でしか使えません。 ただし、匿名メソッドや finally ブロックなどで利用することはできません。

yield キーワードは単体で利用するものではなく、yield return、または yield break とすることで意味を成します。 これらは return や break を修飾しているというよりも、yield return 文、及び yield break 文として、イディオム的に捕らえるというのが正しいと思われます。

yield return expression;
yield break;

yield break については後で説明するので、まずは yield return の使い方から見てみます。 expression は、通常の return 文同様にブロックが返す値を指定します。

yield return の登場によって、IEnumerator または IEnumerable インタフェースを戻り値するメソッドは、このインタフェースを実装するオブジェクトを return 文で返すか、または yield return 文を用いて要素単位で返すことができます。 yield return 文の場合、expression に指定する値は、イテレータの要素となる値です。 ブロックでは yield return が処理された順番に、返された値を要素として扱います。

using System;

class Test {
	public static System.Collections.IEnumerable GetIterator() {
		yield return 1;
		yield return 10;
		yield return 100;
		yield return "ネコミミモード";
	}
	static void Main() {
		foreach(object n in GetIterator()) {
			Console.WriteLine(n);
		}
	}
}
>test
1
10
100
ネコミミモード

このプログラムの GetIterator() メソッドをみて分かるように、メソッドの戻り値は IEnumerable 型なのにもかかわらず、yeild return は数値や文字列を返してます。 これらの値は、単体として返されるのではなく IEnumerator が返す個々の要素であると認識されます。 プログラムの実行結果を見れば、foreach 文で正しく処理されていることを確認できます。

ここで問題なのは、yield return 文はどのタイミングで評価されているかという部分です。 一般的な手続き型言語の解釈をするとすれば、GetIterator() が逐次実行されたときに内部の隠蔽された動的な配列に対して返された値が保存され、そこから IEnumerable が生成されたと考えるかもしれません。 手続き型言語の性質を考えれば、これはセンスある回答かもしれません。

ところが、そうではないのです。 上記の手法では、結局逐次処理で配列を生成しているため、状況に応じて配列を動的に制御するという強力な IEnumrator オブジェクトを作ることはできません。

驚くべきことに、Microsoft の設計者はここで、これまで C の派生言語ではまったく見られることのなかった暴挙ともいえる手法を採用します。 yield return は、メソッドを Lua 言語などでみられるコルーチンとして振舞わせてしまうのです。

コルーチンの最大の特徴は、通常の関数やメソッドと呼ばれるものとは異なり、レジューム可能であるところにあります。 スレッドのように、コルーチンはブロックの途中で制御を中断することができ、任意のタイミングで中断部分からブロックを再開することができるのです。 yield return が呼び出されると、その時点でメソッドは処理を中断して制御を返すのです。

手順としては、yield return でオブジェクトを返すメソッドが呼び出されると、メソッドは何も実行せずにオブジェクトだけを返します。 このとき、オブジェクトの MoveNext() メソッドに対して yield return の内容が割り当てられます。 そして、MoveNext() が実行されるたびに、前回の yield return 文からブロックが再開されるという仕組みになっています。

using System;
using System.Collections;

class Test {
	public static IEnumerator GetIterator() {
		for(int i = 0 ; true ; i++) {
			Console.WriteLine("yield return " + i);
			yield return i;
		}
	}
	static void Main() {
		IEnumerator iterator = GetIterator();

		Console.WriteLine("最初の MoveNext() メソッドの手前です");
		iterator.MoveNext();

		Console.WriteLine("2回目の MoveNext() メソッドの手前です");
		iterator.MoveNext();
	}
}
>test
最初の MoveNext() メソッドの手前です
yield return 0
2回目の MoveNext() メソッドの手前です
yield return 1

このプログラムの実行結果を見れば、プログラムがどのような順序で評価されているかを確認することができます。 最初に yield return で IEnumerator を返す GetIterator() メソッドを呼び出していますが、メソッドの for ブロックはこの時点で評価されません。 GetIterator() はオブジェクトだけ返して終了しています。 これは、最初に出力された文が MoveNext() 呼び出しの手前の WriteLine() メソッドであることが証明しています。

GetIterator() メソッドの本体は、メソッドが返した IEnumerator オブジェクトの MoveNext() が呼び出されると実行され、yield return 文で制御を返します。 GetIterator() メソッドの for 文は無限ループになっているにもかかわらず、実行結果を見れば制御が返されていることが分かります。 そして、再び MoveNext() を呼び出すと、前回の yield return 文からメソッドが再開されるのです。

もちろん、yield return で IEnumerator を返すメソッドのオブジェクト独立しています。 メソッドの引数やローカル変数は、共有されることはありません。

using System;
using System.Collections;

class Test {
	public static IEnumerator GetIterator(string name , int start) {
		for(int i = start ; true ; i++) {
			Console.WriteLine(name + " = " + i);
			yield return i;
		}
	}
	static void Main() {
		IEnumerator iterator1 = GetIterator("iterator1" , 10);
		IEnumerator iterator2 = GetIterator("iterator2" , 256);

		iterator1.MoveNext();
		iterator2.MoveNext();

		iterator1.MoveNext();
		iterator2.MoveNext();
	}
}
>test
iterator1 = 10
iterator2 = 256
iterator1 = 11
iterator2 = 257

このプログラムの結果から、GetIterator() メソッドが返したオブジェクトは、GetIterator() メソッドの引数やローカル変数を独自のメモリ空間にインスタンス化されて保有していることが分かります。 MoveNext() によってメソッドを途中再開しても、他のオブジェクトと競合することはありません。


yield break

yield break 文は、yield return 文同様に IEnumerator または IEnumerable インタフェースを戻り値するメソッドの内部で使えます。 yield return 文は、MoveNext() メソッドが実行されるたびに、前回 yield return した部分から再開されるというものでした。 これに対し yield break 文は、この文が実行された時点で、要素の終了を強制します。 つまり、yield break 文は MoveNext() メソッドの戻り値を false にするのです。

MoveNext() が false を返した場合、オブジェクトは要素の末尾に到達したことを意味します。 これによって、例えば foreach 文はループを終了させるでしょう。

using System;
using System.Collections;

class Test {
	public static IEnumerable GetIterator() {
		for(int i = 0 ; true ; i++) {
			if (i == 5) yield break;
			yield return i;
		}
	}
	static void Main() {
		foreach(object n in GetIterator()) {
			Console.WriteLine(n);
		}
	}
}
>test
0
1
2
3
4

このプログラムの GetIterator() では、for 文はローカル変数 i をインクリメントし続け、i を yield return で返し続けます。 しかし、プログラムが何らかの事情で要素の末尾を迎える必要がある場合、または全ての yield return を実行せずに列挙を終了させたい場合、yield break 文で終了させられるのです。



前のページへ戻る次のページへ