IEnumerator<T>、MoveNext()がfalseを返す状態でCurrentにアクセスした際の挙動は未定義

　この投稿では、`IEnumerator<T>`を実装したオブジェクトが、`MoveNext()`メソッドで`false`を返す状態で`Current`プロパティにアクセスするとどうなるかを紹介します。またそれにまつわる問題を紹介します。

# 問題です！

　[`IEnumerator<int>`](https://msdn.microsoft.com/ja-jp/library/78dfe2yb(v=vs.110).aspx)インターフェースを実装したクラスのオブジェクト`enumerator`があります。

```csharp:
IEnumerator<int> enumerator = GetExampleEnumerable().GetEnumerator();
```

　今このオブジェクトが、[`MoveNext()`](https://msdn.microsoft.com/ja-jp/library/system.collections.ienumerator.movenext(v=vs.110).aspx)メソッドを呼び出すと`false`を返す状態だとします。

```csharp:
while(enumerator.MoveNext()){
	;
}
// whileを抜けたので、`enumerator.MoveNext()`はfalseを返した
```

　この状態で[`Current`](https://msdn.microsoft.com/ja-jp/library/58e146b7(v=vs.110).aspx)プロパティに次のようにアクセスするとどうなるでしょうか？

```csharp:
var current = enumerator.Current;
```

　答えの候補としては、

* 例外を投げる
* 型の規定値`default(T)`が返る
* 最後の要素か型の規定値`default(T)`が返る
* 未定義

　などが考えられますね。


# 正解は？


　正解は`未定義`です。
　いいですか`未定義`です。
　もう一度言いますよ、`未定義`です。

　以下、[MSDN(日本語)](https://www.google.co.jp/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=IEnumerator%3CT%3E)より抜粋です。

> MoveNext がコレクションの末尾を過ぎると、列挙子はコレクションの最後の要素の後ろに配置され、MoveNext は false を返します。 列挙子がこの位置にある場合、以降、MoveNext を呼び出しても false が返されます。 MoveNext への最後の呼び出しで false が返された場合は、Current が未定義です。 Current を、再度、コレクションの最初の要素に設定することはできません。列挙子の新しいインスタンスを作成する必要があります。

　`未定義`ですね。

　`未定義`、つまり`IEnumerator<T>`の実装によってどのような挙動になるかが変わります。(クラスによって違うし、もしかしたらMonoと.NET Frameworkで違うかもしれません。)

　ちなみに実際に下記のそれぞれの挙動をするものが存在します。

* 例外を投げる
* 型の規定値default(T)が返る
* 最後の要素か型の規定値default(T)が返る


```csharp:コード例(Mono4.2.3)

// 例外を投げる例
[Test ()]
public void TestArray ()
{
	IEnumerable<int> enumerable = new int[]{ 0, 1, 2 };
	IEnumerator<int> enumerator = enumerable.GetEnumerator ();
	MoveEnumeratorToNoNext (enumerator); // このメソッドで

	Assert.Throws<InvalidOperationException> (() => {
		var current = enumerator.Current;
	});
}

// 型の規定値を返す例
[Test ()]
public void TestList ()
{
	IEnumerable<int> enumerable = new List<int> { 0, 1, 2 };
	IEnumerator<int> enumerator = enumerable.GetEnumerator ();
	MoveEnumeratorToNoNext (enumerator);

	Assert.AreEqual (default(int), enumerator.Current);
}

// 最後の要素か型の規定値default(T)が返る例
[Test ()]
public void TestYield ()
{
	IEnumerable<int> enumerable = YieldParams<int> (0, 1, 2);
	IEnumerator<int> enumerator = enumerable.GetEnumerator ();
	MoveEnumeratorToNoNext (enumerator);

	Assert.AreEqual (enumerable.LastOrDefault (), enumerator.Current);
}

static void MoveEnumeratorToNoNext<T> (IEnumerator<T> enumerator)
{
	while (enumerator.MoveNext ()) {
		;
	}
}

static IEnumerable<T> YieldParams<T> (params T[] args)
{
	foreach (T it in args) {
		yield return it;
	}
}
```


　参考コード全文は[こちら](https://gist.github.com/RyotaMurohoshi/eb5164768916ddd7fa89095260ba22f9)

# で、どういう問題があるの？

　「`IEnumerator<T>`が`MoveNext()`で`false`を返す状態で`Current`にアクセスした結果が`未定義`」ですが、実際のコーディングではどう気を付けなければいけないでしょうか？

　`IEnumerator<T>`は、`foreach`文の内部やLINQの内部など多くの場面で使われています。しかしそれらの利用者が`IEnumerator<T>`を意識しなくても使えるようになっていますね。

　気を付けなければいけないのは、コレクションライブラリやLINQライクの拡張関数を作成する時です。この仕様を見落としてしまうと思わぬバグを作ってしまうかもしれません。

　次のよくないコードを見て下さい。どこがよくないかわかりますか？

```csharp:問題があるコード
IEnumerable<T> enumerable = GetTargetIEnumerable();
IEnumerator<T> enumerator = enumerable.GetEnumerator();
bool hasNext = enumereator.MoveNext();
T current = e.Current;

if(!hasNext) {
	// MoveNext()がfalseの場合の処理
}
// 以下略
```

　よくない点は、「`MoveNext()`の結果が`false`かもしれないのに`Current`プロパティにアクセスしている」という点です。`MoveNext()`が`false`を返す場合に`Current`プロパティにアクセスした場合は、例外を投げるものもあります。そのように例外を投げる場合、上記のコードのように`MoveNext()`メソッドの結果が`false`の場合に処理を終えるなどの処理をしていた場合でも、意味がありません。次のようにする必要がありますね。

```csharp:問題を修正したコード
IEnumerable<T> enumerable = GetTargetIEnumerable();
IEnumerator<T> enumerator = enumerable.GetEnumerator();
bool hasNext = enumereator.MoveNext();

if(!hasNext) {
	// MoveNext()がfalseの場合の処理
	// 略
}

T current = e.Current;
// 以下略
```

　コレクションライブラリやLINQライクな`IEnumerable<T>`の拡張関数を作る際はこの点に注意する必要があります。

　この問題で厄介なのは、`未定義`という点です。例外を投げるものもあれば、規定値を返すものもあれば、最終要素を返すものもあります。同じコードでも`List<T>`や`Set<T>`そしてLINQの結果を処理対象とする時は問題は発生しないけれど、配列ならば問題が発生するということもあります。

　「メソッドに対してテストを書いていてもテストに使ったのが`List<T>`で問題に気がつかず、配列で問題が発生するのに気がつかなかった。」ということもありえますね。

# まとめ

　`IEnumerator<T>`インターフェースを実装したオブジェクトが`MoveNext()`メソッドで`false`を返す状態で`Current`にアクセスした場合の挙動は未定義です。

　未定義ということは、例外を投げるものもあるかもしれないし、型の規定値を返すものもあるかもしれない、ということです。

　コレクションライブラリやLINQライクのライブラリを実装する際は、この点に気をつけて実装しないと思わぬバグを作りこんでしまう場合があるかもしれません。

　最後に、実際にこれが原因で不具合を作っていたIxの不具合を紹介します。

# 具体例

　[Ix(Interactive Extensions)](https://github.com/Reactive-Extensions/Rx.NET#interactive-extensions)というライブラリがあります。LINQのような`IEnumerable<T>`の拡張関数や、`IEnumerable<T>`を生成するメソッドが定義されています。

　問題があるのは、このIxのCatchメソッドです。(git commit revision : [`2252cb4`](https://github.com/Reactive-Extensions/Rx.NET/tree/2252cb4edbb25aca12005b9a912311edd2f095f3))

　ここで使っているCatchメソッドは、`IEnumerable<TSource>`を列挙している途中で例外が発生した場合、それ以後の列挙をやめて引数で渡した`IEnumerable<TSource>`を列挙するというものです。

　下記のテストは通ります。想定している挙動です。

```csharp:
[Test ()]
public void TestCatchFirstSecondWithoutException ()
{
	var sequence = new List<int>{ 0, 1, 2 }.Catch (second: new List<int>{ 3, 4, 5 });
	var result = sequence.SequenceEqual (new []{ 0, 1, 2 });
	Assert.True (result);
}
```

　しかし、次のテストは失敗します。

```csharp:
[Test ()]
public void TestCatchFirstSecondWithoutException ()
{
	var sequence = new int[]{ 0, 1, 2 }.Catch (second: new int[]{ 3, 4, 5 });
	var result = sequence.SequenceEqual (new []{ 0, 1, 2 });
	Assert.True (result);
}
```

　これは`IEnumerable<TSource>`の列挙の途中で想定外の例外が発生してしまい、引数で渡した`IEnumerable<TSource>`の列挙をするからです。ではなぜ`IEnumerable<TSource>`の列挙の途中で想定外の例外が発生するのでしょうか。

　問題のあるコードはIxの実装の[ここ](https://github.com/Reactive-Extensions/Rx.NET/blob/2252cb4edbb25aca12005b9a912311edd2f095f3/Ix.NET/Source/System.Interactive/EnumerableEx.Exceptions.cs)の部分です。`MoveNext()メソッド`の結果にかかわらず、`Current`プロパティにアクセスしていますね。これが原因でIxの現バージョン([`2252cb4`](https://github.com/Reactive-Extensions/Rx.NET/tree/2252cb4edbb25aca12005b9a912311edd2f095f3))のCatchメソッドを配列で使った場合、思わぬ挙動をしてしまいます。

　Catchメソッドに対して[テスト](https://github.com/Reactive-Extensions/Rx.NET/blob/2252cb4edbb25aca12005b9a912311edd2f095f3/Ix.NET/Source/Tests/Tests.Exceptions.cs#L77)は書いてありましたが、配列でテストをしていなくてこのバグを見落としていたようです。

　これに関しての修正の[プルリクエスト](https://github.com/Reactive-Extensions/Rx.NET/pull/171)を送りました。