【初心者向け】foreachで回せる便利な独自コレクションを作ろう
はじめに
.NET では、簡単なルールに則るだけで独自のコレクションが作成できます。この記事では、初心者でも IEnumerable<T>
の実装を行うだけで foreach
可能で便利な独自コレクションを作成することができることを説明します。
IEnumerable を実装すると何がうれしいの?
-
foreach
で順番に中身を取り出せるようになる - 標準ライブラリのメソッド(
Count()
など)をそのまま使える
ここでは難しい用語や複雑なコードは使わず、「最小構成で動かす」ことに集中します。
最小の独自コレクションを作ってみる
次のクラスを丸ごとコピペすれば、foreach
で巡回できる int
型を要素に持つ独自のコレクションが完成します。
using System.Collections;
using System.Collections.Generic;
public class NumberCollection : IEnumerable<int>
{
private readonly List<int> _items = new();
public void Add(int value)
{
_items.Add(value);
}
public IEnumerator<int> GetEnumerator()
{
return _items.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
foreach で使ってみる
独自のクラスを foreach に記述する事ができます
var numbers = new NumberCollection();
numbers.Add(3);
numbers.Add(5);
numbers.Add(8);
foreach (var number in numbers)
{
Console.WriteLine(number); //3 5 8
}
2 つある GetEnumerator() は何?
GetEnumerator()
は「要素を 1 つずつ順番に取り出すための係」を作るメソッドです。foreach
はこのメソッドを内部で呼び出して、コレクションの中身を順番に処理しています。
public IEnumerator<int> GetEnumerator() // ← 1つ目
{
return _items.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() // ← 2つ目
{
return GetEnumerator();
}
見ての通り、どちらも内部の List<int>
に処理を丸投げしているだけです。つまり「うちのコレクションの要素を順番に取り出したいなら、内部の List に聞いてください」と言っているような感じになっています。
なぜ 2 つあるかというと、.NET の歴史的な理由で新旧 2 つのバージョンに対応する必要があるためです:
1 つ目:型が明確な int 専用バージョン
2 つ目:どんな型でも使える汎用バージョン(1 つ目を呼び出すだけ)
まずは「IEnumerable<T>
を実装するときは、この 2 つをセットで書く必要がある」とだけ覚えておけば十分です。
自分好みの機能を足してみる
ここまでならList<int>
で同じことが出来るので、わざわざ独自コレクションを作成する意味がありません。
しかし独自コレクションを作成することにより、用途に合わせた便利なメソッドやプロパティを追加する事ができます。例えば偶数の数を数えるメソッドを追加してみます。
public int CountEven()
{
var count = 0;
foreach (var value in _items)
{
if (value % 2 == 0)
{
count++;
}
}
return count;
}
使い方は次の通りです。
var numbers = new NumberCollection();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
numbers.Add(4);
Console.WriteLine(numbers.CountEven()); // 2
どんなときに独自コレクションが活きる?
CountEven()
のように偶数を数えたいだけなら、List<int>
に対してその場で foreach
を回して数えるほうが手っ取り早いかもしれません。
var count = 0;
foreach (var number in numbers)
{
if (number % 2 == 0)
{
count++;
}
}
Console.WriteLine(count);
問題は、こうした処理を画面やサービスごとに何度も書くことになったときです。同じ判定ロジックを毎回コピペすると、修正漏れが起きたり、似たようなコードが散らばって読みにくくなったりします。
独自のコレクションクラスを作成し、CountEven()
のようにメソッドへまとめておけば、別ファイルや別プロジェクトでも同じ書き方で呼び出せますし、あとから条件を調整したくなっても 1 箇所直すだけで済みます。
つまり、特定の集合に対する定番の操作をまとめたいとき、独自コレクションは有効な選択肢になります。
まとめ
-
IEnumerable<T>
を実装すれば、独自クラスでもforeach
やCount()
などの標準的な操作をそのまま利用可能 -
CountEven()
のように処理を一箇所へ集約することで、重複コードを減らし保守性を高めることが可能
Discussion
内部に
List<T>
を使って List<T>.GetEnumerator() をして その戻り値の型 をIEnumerator<T>
ではなく直接List<T>.Enumerator
にすれば 省メモリも実現できますね。わかります。