【.Net C#】Enumerator の使い方

この記事では Microsoft .Net Framework の Enumerator および IEnumerable<T> の使い方について説明しています。

動作確認した環境
Microsoft Visual C# Compiler 3.5.0 beta4
.Net Framework 4.8.03752

2021/01/07
サンプルソースコード内 GetEnumerator() の戻り値の型を IEnumerable から IEnumerator に修正。
 

Enumerator って何?

foreach するときに必要になります。

例)

配列で foreach する

int[] array = new int[10]{10,9,8,7,6,5,4,3,2,1};
foreach (int e in array) System.Console.WriteLine(e);

List<T> で foreach する

List<int> list = new List<int>();
list.Add(1);
list.Add(2);
list.Add(3);
foreach (int e in list) System.Console.WriteLine(e);

配列や既存のコレクションを foreach で使う場合、Enumerator を気にする必要はありません。
Enumerator を使う必要が出てくるのは、自作したコレクションやクラスの非公開な配列やコレクションに対して foreach を使う場合です。

クラスの非公開なコレクションに対しては foreach できない

class test {
	List<int> data = new List<int>();	//非公開なコレクション
}
test t = new test();
foreach (int e in t) System.Console.WriteLine(e); //コンパイルエラー

上記のようなケースは、C# を使っていれば普通に出てきます。
この場合、foreach を使えるようにする方法は2つあります。

1. クラスが持っているコレクションを公開する

2. IEnumerable<T> を継承する

1. クラスが持っているコレクションを公開する

とりあえず、非公開になっているコレクションを公開すれば、foreach に渡すことはできます。

例)

class test {
	public List<int> data = new List<int>();	//公開コレクション
}
test t = new test();
foreach (int e in t.data) System.Console.WriteLine(e); //OK

問題は、クラスのメンバ変数を公開すべきか?という点です。
この問題については、以下の記事が分かりやすく説明してくれています。

通常は、クラスのメンバ変数(フィールド)は公開しません。
フィールドは非公開にして、プロパティやメソッドを使って、フィールドにアクセスする方法を提供します。
ただ、いついかなる状況でもそうする必要がある…という話ではありません。
オブジェクト指向やクラス設計の話になるので、これ以上掘り下げると、とても長くなってしまいます。
1つの記事にまとめられるような内容ではないので割愛します。

2. IEnumerable<T> を継承する

クラスが持っているコレクションを公開すべきでない場合、IEnumerable<T> を使います。

IEnumerable<T> をどのように使うか?は、状況によって変わってきます。
大きく分けて、ざっと以下のパターンが考えられます。

  1. 1つの非公開コレクションに対して foreach する。
  2. 複数の非公開コレクションに対して foreach する。
  3. 配列に対して foreach する。

それぞれ、実装方法が変わりますので、以下でひとつひとつ説明していきます。

1つの非公開コレクションに対して foreach する。

foreach したいコレクションが1つだけなら、メソッドを2つ追加するだけで対応可能です。

class test : IEnumerable<int> {
	List<int> data = new List<int>();	//foreachしたい非公開コレクション
	public IEnumerator<int> GetEnumerator() { return this.data.GetEnumerator(); }
	IEnumerator IEnumerable.GetEnumerator() { return null; }
}
test t = new test();
foreach (int e in t) System.Console.WriteLine(e); //OK

IEnumerable<T> を継承して、以下の2つのメソッドを追加しただけです。

公開メソッド  public IEnumerator<T> GetEnumerator()
非公開メソッド IEnumerator IEnumerable.GetEnumerator()

foreach の対象にしたいコレクションが List<int> (要素が int 型)なので、IEnumerable も、この型に合わせて <int> にしています。

もし、test クラスがジェネリックなら、以下のように書くことができます。

class test<T> : IEnumerable<T> {
	List<T> data = new List<T>();
	public IEnumerator<T> GetEnumerator() { return this.data.GetEnumerator(); }
	IEnumerator IEnumerable.GetEnumerator() { return null; }
}
test t<int> = new test<int>();
foreach (int e in t) System.Console.WriteLine(e); //OK
public IEnumerator<T> GetEnumerator()

IEnumerable<T> が持っているメソッドで、foreach はこのメソッドを呼び出してループ処理をします。
内部フィールドの List<int> data も、IEnumerable<T> を継承しているので、GetEnumerator() メソッドを持ってます。
なので、data が持ってる GetEnumerator() を引き渡せば、非公開フィールドの data を foreach できるようになります。

IEnumerator IEnumerable.GetEnumerator()

ジェネリック版の IEnumerable<T> は、ジェネリックじゃない IEnumerable を継承しています。
ジェネリックじゃない IEnumerable の GetEnumerator() は使われないのですが、実装処理を書かないとコンパイルエラーになります。
使われないことを示すために、メソッドを非公開にして、意味のない値(null)を返しています。
公開することはできますが、使わないものを公開しても、そのクラスを使う自分以外の人を混乱させるだけなので、良くないです。

複数の非公開コレクションに対して foreach する。

厄介なのは、foreach したい非公開コレクションが複数ある場合です。

class test {
	List<int> data1 = new List<int>();	//非公開コレクション1
	List<float> data2 = new List<float>();	//非公開コレクション2
}

test に IEnumerable<T> を継承させても、data1 か data2 の片方にしか foreach できません。

IEnumerable<int> で継承した場合、List<int> しか返せない。

class test : IEnumerable<int> {	//intにした場合、data1 しか foreach できない
	List<int> data1 = new List<int>();	//非公開コレクション1
	List<float> data2 = new List<float>();	//非公開コレクション2
	public IEnumerator<int> GetEnumerator() { return this.data1.GetEnumerator(); }
	IEnumerator IEnumerable.GetEnumerator() { return null; }
}

IEnumerable<float> で継承した場合、List<float> しか返せない。

class test : IEnumerable<float> {	//floatにした場合、data2 しか foreach できない
	List<int> data1 = new List<int>();	//非公開コレクション1
	List<float> data2 = new List<float>();	//非公開コレクション2
	public IEnumerator<float> GetEnumerator() { return this.data2.GetEnumerator(); }
	IEnumerator IEnumerable.GetEnumerator() { return null; }
}

test クラスが IEnumerable<T> を継承してしまうと、GetEnumerator() を1つしか持てないので、1つのコレクションしか返せません。
Dictionary の Keys と Values のような、複数の GetEnumerator() を外部に出す仕組みが必要です。

class test {
	public class IntList : IEnumerable<int> { // List<int> の GetEnumerator() を外部に出すためのクラス
		List<int> list;
		public IntList(List<int> l) { this.list = l; }
		public IEnumerator<int> GetEnumerator() { return this.list.GetEnumerator(); }
		IEnumerator IEnumerable.GetEnumerator() { return null; }
	}
	public class FloatList : IEnumerable<float> { // List<float> の GetEnumerator() を外部に出すためのクラス
		List<float> list;
		public FloatList(List<float> l) { this.list = l; }
		public IEnumerator<float> GetEnumerator() { return this.list.GetEnumerator(); }
		IEnumerator IEnumerable.GetEnumerator() { return null; }
	}
	List<int> data1 = new List<int>();	//非公開コレクション1
	List<float> data2 = new List<float>();	//非公開コレクション2
	public IntList Data1 { get { return new IntList(this.data1); } }
	public FloatList Data2 { get { return new FloatList(this.data2); } }
}

test t = new test();
foreach (int e in t.Data1) System.Console.WriteLine(e);	//非公開コレクション1をforeach
foreach (float e in t.Data2) System.Console.WriteLine(e);	//非公開コレクション2をforeach

少々手間ですね…。

配列に対して foreach する。

配列に対して foreach するのは、もっと面倒です。

public class ArrayEnumeratorBase<T> : IEnumerable<T> {
	public class Enumerator<U> : IEnumerator<U> {
		//	この Enumerator が動作するのに必要なデータとコンストラクタ
		U[] array = null;
		int current = -1;
		int size;
		public Enumerator(U[] source) { this.array = source; this.size = this.array.Length; }

		//	この Enumerator が処理を行う際、外部から呼び出されるメソッドとプロパティ
		public bool MoveNext() { return ++this.current < this.size; }
		public void Reset() { this.current = -1; }
		public U Current { get { return this.array[this.current]; } }
		object IEnumerator.Current { get { return this.Current; } }
		void IDisposable.Dispose() { this.array = null; this.current = -1; this.size = 0; }
	}

	// IEnumerable<T> のメソッド
	public IEnumerator<T> GetEnumerator() { return this.enumerator; }

	// コンストラクタ
	protected ArrayEnumeratorBase(T[] source) { this.enumerator = new Enumerator<T>(source); }

	// IEnumerable<T> が IEnumerable を継承しているため実装しなければコンパイルが通らないが
	// このメソッドが呼ばれることはないので、外部公開する必要はなく、有効な値を返す必要もない。
	IEnumerator IEnumerable.GetEnumerator() { return null; }

	Enumerator<T> enumerator;
}

まずこんな感じのベースクラスが必要です。
ただ、これ1つあれば使いまわせます。

class test<T> : ArrayEnumeratorBase<T> {
	T[] array = null;
	public test(T[] source) : base(source) { this.array = source; }
}

test<int> t = new test<int>(new int[]{5,4,3,2,1,0});
foreach (int e in t) System.Console.WriteLine(e);

こんな風に使います。

多次元配列やジャグ配列バージョンのベースクラスも作れば、もっと便利になると思います。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です