はじめに
foreach
は、C#で配列やコレクション(List<T>
、Collection<T>
など)を列挙する場合に使用する基本的な構文ですが、いざ自前で実装したクラスや構造体で行う場合、いくつかの条件を満たす必要があります。
今回は条件を実装例をもとに触れていきたいと思います。
foreach ステートメント(仕様)
反復ステートメント - foreach
ステートメントでは以下のように記載されています。
foreach ステートメントは、次の例のように、System.Collections.IEnumerable または System.Collections.Generic.IEnumerable インターフェイスを実装している型のインスタンス内の要素ごとに、ステートメントまたはステートメントのブロックを実行します。
foreach ステートメントは、これらの型に制限されません。 これは次の条件を満たす任意の型のインスタンスと共に使用できます。
- 型には、パラメーターなしのパブリック GetEnumerator メソッドがあります。 GetEnumerator メソッドは、型の拡張メソッドでもかまいません。
- GetEnumerator メソッドの戻り値の型に、パブリック プロパティ Current と、戻り値の型が bool であるパラメーターなしのパブリック メソッド MoveNext がある。
確認環境
サンプルコードは、以下の環境で実行確認しています。
- Visual Studio 2022(Version 17.11.4)
- .NET 8(C# 12)
実装方法 その一(IEnumerable<T>
インターフェイス)
上記の内容から一番わかりやすい実装方法としては IEnumerable<T>
インターフェイスをする方法になります。(非ジェネリック版のIEnumerable
もありますが、今回はジェネリック版のほうで進めます。)
以下のようなSampleクラスでは、IEnumerable<T>
インターフェイスを実装し、IEnumerator<T>.GetEnumerator
メソッドの返却値には、IEnumerator<T>
を実装したSampleEnumeratorクラスを返すことで1から引数に指定した数だけ数字を増加しながら列挙することができます。
public class Sample(int maxCount) : IEnumerable<int>
{
private int maxCount = maxCount;
public IEnumerator<int> GetEnumerator()
=> new SampleEnumerator(maxCount);
IEnumerator IEnumerable.GetEnumerator()
=> new SampleEnumerator(maxCount);
private sealed class SampleEnumerator(int maxCount) : IEnumerator<int>
{
private readonly int maxCount = maxCount;
private int index;
public int Current => index;
object IEnumerator.Current => index;
public void Dispose(){}
public bool MoveNext()
=> index++ < maxCount;
public void Reset()
=> index = 0;
}
}
var sample = new Sample(3);
foreach(var i in sample)
{
Console.WriteLine($"{i}");
}
// Output;
// 1
// 2
// 3
いつもお世話になっているList<T>
も同様に実装されていますが、こちらではメモリアロケーションを考慮してIEnumerator<T>
の実装の型を struct にしているなど少しカスタマイズされています。
実際のアプリ作成時では、IEnumerator<T>
を実装した自前のクラスや構造体を作成するよりも、例えばList<T>
をフィールドに保持して、List<T>.GetEnumerator
メソッドを返すほうが簡単だと思います。
// 例えばこんな感じでList<T>のGetEnumeratorを利用する。
public class Sample(int maxCount) : IEnumerable<int>
{
private List<int> list = new(Enumerable.Range(1, maxCount));
public IEnumerator<int> GetEnumerator()
=> list.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> list.GetEnumerator();
}
実装方法 その二(ダックタイピング的なスタイル)
IEnumerable<T>
インターフェイスを実装していないとforeach
が使用できないというわけではなく、例えばSpan<T>
はIEnumerable<T>
インターフェイスを実装していませんが、foreach
を使用することができます。
Span<int> span = Enumerable.Range(1, 3).ToArray().AsSpan();
// Span<T>はIEnumerable<T>インターフェイスを実装していませんが、foreachを使用可能
foreach(var i in span)
{
Console.WriteLine($"{i}");
}
// Output:
// 1
// 2
// 3
foreach
は、IEnumerable<T>
インターフェイスが必須というわけではなく、「パラメーターなしのパブリック GetEnumerator
メソッドがある」と「GetEnumerator
メソッドの戻り値の型に、パブリック プロパティ Current
と、戻り値の型が bool であるパラメーターなしのパブリック メソッド MoveNext
がある。」という指定した名前のメソッドおよびプロパティを実装していれば使用することができます。
先ほどのSampleクラスでは以下のような形になります。
ただし、この方法で実装した場合、LINQ
を使用することができないため、その点は注意する必要があります。
// ref structでも適用可能
public ref struct Sample(int maxCount)
{
private int maxCount = maxCount;
public SampleEnumerator GetEnumerator() // GetEnumeratorメソッドは必須
=> new SampleEnumerator(maxCount);
public ref struct SampleEnumerator(int maxCount)
{
private readonly int maxCount = maxCount;
private int index;
public readonly int Current // Currentプロパティは必須
=> index;
public bool MoveNext() // MoveNextメソッドは必須
=> index++ < maxCount;
}
}
// 同じようにforeachに使用可能です
var sample = new Sample(3);
foreach(var i in sample)
{
Console.WriteLine($"{i}");
}
// Output:
// 1
// 2
// 3
この方法であれば、Span<T>
のようにref 構造体にもforeach
を使用することができますので、例えば ReadOnlySpan<byte>
をソースに何かしらの値を列挙していくような処理を実装することができます。
拙作ライブラリ Utf8StringSplitter でも同様の方法を用いて実装しています。
C# 13から制限事項はありますが、ref 構造体にインターフェイス実装を行うことが可能になるようです。
using System.Text;
using Utf8StringSplitter;
foreach(var s in Utf8Splitter.Split("1,2,3,4,5"u8, ","u8))
{
Console.WriteLine($"{Encoding.UTF8.GetString(s)}");
}
// Output:
// 1
// 2
// 3
// 4
// 5
あとがき
C#でプログラムを実装するときに普段何気なく書いているforeach
ですが、実際に仕様を確認してみるとダックタイピング的なスタイルで書けるなどまだまだ勉強する必要があると感じました。
あと上記でも少し出しましたが、その二(ダックタイピング的なスタイル)同様の方法で実装した拙作ライブラリUtf8StringSplitterについても、興味があればぜひ使用してもらえればと思います。