IEnumerator<T>、MoveNext()がfalseを返す状態でCurrentにアクセスした際の挙動は未定義
この投稿では、IEnumerator<T>
を実装したオブジェクトが、MoveNext()
メソッドでfalse
を返す状態でCurrent
プロパティにアクセスするとどうなるかを紹介します。またそれにまつわる問題を紹介します。
問題です!
IEnumerator<int>
インターフェースを実装したクラスのオブジェクトenumerator
があります。
IEnumerator<int> enumerator = GetExampleEnumerable().GetEnumerator();
今このオブジェクトが、MoveNext()
メソッドを呼び出すとfalse
を返す状態だとします。
while(enumerator.MoveNext()){
;
}
// whileを抜けたので、`enumerator.MoveNext()`はfalseを返した
この状態でCurrent
プロパティに次のようにアクセスするとどうなるでしょうか?
var current = enumerator.Current;
答えの候補としては、
- 例外を投げる
- 型の規定値
default(T)
が返る - 最後の要素か型の規定値
default(T)
が返る - 未定義
などが考えられますね。
正解は?
正解は未定義
です。
いいですか未定義
です。
もう一度言いますよ、未定義
です。
以下、MSDN(日本語)より抜粋です。
MoveNext がコレクションの末尾を過ぎると、列挙子はコレクションの最後の要素の後ろに配置され、MoveNext は false を返します。 列挙子がこの位置にある場合、以降、MoveNext を呼び出しても false が返されます。 MoveNext への最後の呼び出しで false が返された場合は、Current が未定義です。 Current を、再度、コレクションの最初の要素に設定することはできません。列挙子の新しいインスタンスを作成する必要があります。
未定義
ですね。
未定義
、つまりIEnumerator<T>
の実装によってどのような挙動になるかが変わります。(クラスによって違うし、もしかしたらMonoと.NET Frameworkで違うかもしれません。)
ちなみに実際に下記のそれぞれの挙動をするものが存在します。
- 例外を投げる
- 型の規定値default(T)が返る
- 最後の要素か型の規定値default(T)が返る
// 例外を投げる例
[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;
}
}
参考コード全文はこちら
で、どういう問題があるの?
「IEnumerator<T>
がMoveNext()
でfalse
を返す状態でCurrent
にアクセスした結果が未定義
」ですが、実際のコーディングではどう気を付けなければいけないでしょうか?
IEnumerator<T>
は、foreach
文の内部やLINQの内部など多くの場面で使われています。しかしそれらの利用者がIEnumerator<T>
を意識しなくても使えるようになっていますね。
気を付けなければいけないのは、コレクションライブラリやLINQライクの拡張関数を作成する時です。この仕様を見落としてしまうと思わぬバグを作ってしまうかもしれません。
次のよくないコードを見て下さい。どこがよくないかわかりますか?
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
の場合に処理を終えるなどの処理をしていた場合でも、意味がありません。次のようにする必要がありますね。
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)というライブラリがあります。LINQのようなIEnumerable<T>
の拡張関数や、IEnumerable<T>
を生成するメソッドが定義されています。
問題があるのは、このIxのCatchメソッドです。(git commit revision : 2252cb4
)
ここで使っているCatchメソッドは、IEnumerable<TSource>
を列挙している途中で例外が発生した場合、それ以後の列挙をやめて引数で渡したIEnumerable<TSource>
を列挙するというものです。
下記のテストは通ります。想定している挙動です。
[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);
}
しかし、次のテストは失敗します。
[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の実装のここの部分です。MoveNext()メソッド
の結果にかかわらず、Current
プロパティにアクセスしていますね。これが原因でIxの現バージョン(2252cb4
)のCatchメソッドを配列で使った場合、思わぬ挙動をしてしまいます。
Catchメソッドに対してテストは書いてありましたが、配列でテストをしていなくてこのバグを見落としていたようです。
これに関しての修正のプルリクエストを送りました。
Author And Source
この問題について(IEnumerator<T>、MoveNext()がfalseを返す状態でCurrentにアクセスした際の挙動は未定義), 我々は、より多くの情報をここで見つけました https://qiita.com/RyotaMurohoshi/items/12ac263b4e44c65bc763著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .