ネタ元→カブるべからず(2)
要素を列挙可能なコレクションに対し、要素の1個目と2個目以降で異なる処理を行いたいとする。
foreach では問答無用で全要素を列挙してしまうので適さない。
かといって、1個目か2個目以降かを判別するためだけにフラグを持たせたり、添字アクセスするのも美しくない。
そうだ、「2個目以降だけ列挙する列挙子」を開発したらどうだろう、というのが発端。
こんな感じのものを考えていたわけだ。
class RangeEnumerator<T> : IEnumerable<T>, IEnumerator<T>
{
private readonly IEnumerator<T> _baseEnumerator;
private readonly int _first;
private readonly int _count;
private int _currentIndex;
// baseEnumerator の first 番目(ゼロベース)から、最大 count 個の要素を列挙する
public RangeEnumerator( IEnumerable<T> baseCollection, int first, int count )
{
this._baseEnumerator = baseCollection.GetEnumerator();
this._first = first;
this._count = count;
Reset();
}
public RangeEnumerator( IEnumerable<T> baseCollection, int first ) : this( baseCollection, first, -1 )
{
}
public RangeEnumerator( IEnumerable<T> baseCollection ) : this( baseCollection, 0 )
{
}
public T Current
{
get
{
return this._baseEnumerator.Current;
}
}
public void Dispose()
{
this._baseEnumerator.Dispose();
}
object IEnumerator.Current
{
get
{
return ( this._baseEnumerator as IEnumerator ).Current;
}
}
public bool MoveNext()
{
++this._currentIndex;
if( this._count != -1 && ( this._first + this._count ) <= this._currentIndex )
{
return false;
}
return this._baseEnumerator.MoveNext();
}
public void Reset()
{
this._baseEnumerator.Reset();
this._currentIndex = -1;
for( int i = 0; i < this._first; ++i )
{
if( ! MoveNext() )
{
throw new ArgumentOutOfRangeException();
}
}
}
public IEnumerator<T> GetEnumerator()
{
return this;
}
IEnumerator IEnumerable.GetEnumerator()
{
return ( this as IEnumerator );
}
}
IEnumerable について調べているうちに、いくつかの興味深い事実を発見した。
まずは「Enumerable パターン」の存在である。
C# プログラミング ガイド:foreach を使用してコレクション クラスにアクセスするによれば、C# では、foreach と互換性を保つ目的で、コレクション クラスを IEnumerable と IEnumerator から継承することは厳密には必要ありません。要求された GetEnumerator、MoveNext、Reset、Current の各メンバがクラスに含まれている限り、コレクション クラスで foreach を使用できます。
だそうだ。おそらく、内部ではリフレクションを使っているのであろう。
(IEnumrable / IEnumerator を実装する場合を含み、)これらのメソッドを実装することを「Enumerable パターン」と呼ぶ(ちなみに、GetEnumerator はインスタンスメソッドでなければならない。静的メソッドだとエラーになる)。
また、yield return の戻り値(というのも変だが)としては、IEnumerable<T> と IEnumerator<T> のどちらも書けるというのも初めて知った。
class Test
{
public IEnumerator<int> Hoge()
{
yield return 1;
yield return 2;
yield return 3;
}
public IEnumerable<int> Moge()
{
yield return 1;
yield return 2;
yield return 3;
}
}
これはどちらも合法的なコードである。
しかし、大きな違いが一つある。IEnumerator<T> を返す方は、foreach できないのだ。
Test t = new Test();
// これはOK
foreach( int i in t.Moge() )
{
Console.WriteLine( i );
}
/* これはダメ
foreach( int i in t.Hoge() )
{
Console.WriteLine( i );
}
*/
// これならOK
using( IEnumerator<int> e = t.Hoge() )
{
while( e.MoveNext() )
{
Console.WriteLine( e.Current );
}
}
最後に、yield return で得た IEnumerator を Reset するべからずという問題を取り上げよう。
上のコードの3つ目のケースを、こんな風に書き換えると、
// これはダメ
using( IEnumerator<int> e = t.Hoge() )
{
e.Reset();
while( e.MoveNext() )
{
Console.WriteLine( e.Current );
}
}
e.Reset で NotSupportedException が発生してしまう。これはドキュメントのバグである。
IEnumerator<T> は Reset の定義を持たない。これは非ジェネリックな IEnumerator から継承されるメソッドである。
そして、IEnumerator のドキュメントにも、IEnumerator.Reset のドキュメントにも、このメソッドが NotSupportedException を発生し得るとは書かれていない。
IEnumerator.Reset が投げ得る例外は、唯一 InvalidOperationException であるはずだ。
しかし、最新のオフライン版のドキュメントには、しれっとこんなことが書かれている。
Reset メソッドは COM 相互運用性のために提供されています。これは必ずしも実装する必要はありません。代わりに、実装側で単に NotSupportedException をスローできます。
こういうことをされるとまったく困る。
実際、あるメソッドないしはプロパティが投げ得る例外について、信頼に足るドキュメントは存在しないのが現状である。
余談だが、IEnumerable.Reset を呼ぶ際、列挙子のもとになるコレクションが変更されていると InvalidOperationException が投げられることは明記されているが、COM 相互運用の IEnumXXX インターフェイスの中には、Reset メソッドを呼ぶことで列挙子を作り直し、母体となるコレクションの変更を反映して列挙子を有効化するものもある。
ついでに、ドキュメントのミスをもう一つ指摘しておこう。
IEnumerator<T> のドキュメントには、MoveNext がコレクションの末尾を過ぎると、列挙子はコレクションの最後の要素の後ろに配置され、MoveNext は false を返します。列挙子がこの位置にある場合、以降、MoveNext を呼び出しても false が返されます。MoveNext への最後の呼び出しで false が返された場合は、Current が未定義です。Current を、再度、コレクションの最初の要素に設定することはできません。列挙子の新しいインスタンスを作成する必要があります。
とある。…あれ? Reset は?
IEnumerator のドキュメントには、しっかり、Current をコレクションの最初の要素に再び設定するには、Reset を呼び出してから、MoveNext を呼び出します。
と書かれているが…?
確かに、IEnumerator<T> 自体は Reset を持っていないが、IEnumerator を継承しているのだがら、ここでは Reset に言及して然るべきである。というか、IEnumerable<T> 自体に Reset が無いのが言及しない理由なら、MoveNext については書かれているのはおかしいだろう。
なお、この記事を書くにあたっては、窓際プログラマーの独り言を参考にした。