概要
foreachとは、コレクションのすべての要素を1回ずつ読み出すための構文です。
ポイント
-
配列みたいに for (int i = 0; i < array.Length; ++i) { array[i] ... } という形で要素の列挙ができないようなコレクションも、foreach なら列挙可能。
-
foreach (変数 in コレクション) { ... }
コレクション
コレクション(「コンテナ」ともいいます)とは配列やリスト、辞書などの複数の要素をひとつにまとめるクラスのことです。 複数の要素をまとめておく方法にはさまざまな方法があり、 その方法によって呼び名が変わります。 以下にコレクションの例とその簡単な説明を列挙します。
データ格納方式 | 長所 | 欠点 | |
---|---|---|---|
配列 | 要素を単純に横に並べて置いておく。 | 処理の効率もメモリの使用効率もよい。また、任意の場所にある要素にいつでもアクセスできる。 | 末尾以外の場所に要素を挿入することが出来ない(出来ても効率が悪い)。 |
連結リスト | セルと呼ばれる要素を入れておく箱を繋げていく。 | 任意の場所の要素の追加・削除が効率的に行える。 | 配列と比べ効率が落ちる。また、配列と違って前から順に要素をたどっていくことしか出来ない。 |
探査木 | 左右に枝の伸びる木構造にデータを格納。 「左側の枝には小さな値、右側の枝には大きな値を格納する」といった条件をつけておく。 | 要素の検索・挿入・削除が効率的に行える。 | 要素を挿入した順序が意味を成さなくなる。 |
ここでは詳細には触れませんが、 当サイト上にある「C++ STL」や「アルゴリズムとデータ構造」でもコレクションについて簡単な説明がありますので、興味のある方はそちらをご覧ください。 また、コレクションについてより詳しく知りたい方は検索エンジンで「データ構造 アルゴリズム」などをキーワードにして検索してみてください。
ここでは例として連結リストを示します。 あくまで例として示すだけなので、単純な実装方法を取っています。 (本来はもう少しちゃんとした実装の仕方をしないとだめ。)
using System;
using System.IO;
/// <summary>
/// リストのノード
/// </summary>
class Node
{
public int elem;
public Node next;
public Node() : this(0, null){}
public Node(int val, Node next)
{
this.elem = val;
this.next = next;
}
}
/// <summary>
/// 連結リストクラス
/// </summary>
class List
{
public Node head;
public List()
{
head = null;
}
/// <summary>
/// リストに新しい要素を追加する。
/// </summary>
/// <param name="val">追加する値</param>
public void Add(int val)
{
Node node = new Node(val, this.head);
this.head = node;
}
}
IEnumerable インターフェース
ここで1つ問題があります。 データの格納方式が違えば、当然データの読み出し方も変わってくるということです。 例えば、配列の場合、以下のようにすれば全ての要素を読み出せます。
int[] a = new int[]{1, 3, 5, 7};
for(int i=0; i<a.Length; ++i)
Console.Write("{0}\n", a[i]);
しかし、上述の例に挙げたリストクラスに対して同じ操作を行おうとすると以下のようになります。
List list = new List();
list.Add(7);
list.Add(5);
list.Add(3);
list.Add(1);
for(Node n=list.head; n!=null; n=n.next)
{
Console.Write("{0}\n", n.elem);
}
同じ「コレクション内のすべての要素を1回ずつ読み出す」という操作なのに全然違うコードを書く必要があります。 コレクションごとにコードを変更するのは面倒ですし、 仕様の変更に柔軟に対応できないなどの問題があります。
そこで、コレクションクラスは共通のインターフェースを実装するという決まりを作り、
要素へのアクセスはこのインターフェースを通して行うのが一般的です。
そのためのクラスとして .NET Framework には
IEnumerable
というインターフェースが用意されています。
もちろん、C# の配列は IEnumerable
インターフェースを実装しています。
IEnumerable
インターフェースの実装の仕方については後ほど述べることにして、
ここでは IEnumerable
インターフェースを介した要素へのアクセスの仕方のみを説明します。
IEnumerable
インターフェースを介した要素へのアクセスは以下のようにします。
int[] array = new int[]{1, 3, 5, 7};
IEnumerator e = array.GetEnumerator();
while(e.MoveNext())
{
int val = (int)e.Current;
Console.Write("{0}\n", val);
}
ここで、IEnumerator
とは列挙子と呼ばれるクラスを作るためのインターフェースです。
IEnumerator
インターフェースについては後ほど説明します。
foreach文とは
foreach 文を用いるとこで IEnumerable
インターフェースを介した要素へのアクセスを簡単化することが出来ます。
以下のように、foreachを使うことでコレクションのすべての要素を1回ずつ読み出すことができます。
foreach(型名 変数 in コレクション)
文
このコードは以下のように展開されます。
try
{
IEnumerator e = array.GetEnumerator();
while(e.MoveNext())
{
型名 変数 = (型名)e.Current;
文
}
}
finally
{
Dispose処理
}
「Dispose処理」の部分は、コンパイル時点でIDisposable
なことがわかっている型かどうかで実際に生成されるコードが変わります。
コンパイル時点でIDisposable
なことがわかる場合は以下の通り。
((IDisposable)e).Dispose();
逆に、わからない場合は以下のようになります。
IDisposable d = e as IDisposable;
if (d != null) d.Dispose();
例えば、int
型の配列の要素を読み出して画面に表示するには以下のようにします。
int[] array = new int[10]{1, 2, 4, 8, 16, 32, 64, 128, 256, 512};
foreach(int n in array)
{
Console.Write(n + " ");
}
1 2 4 8 16 32 64 128 256 512
foreach文の実態はIEnumerable
インターフェースを介した要素へのアクセスですから、
IEnumerable
インターフェースを実装しているならどんなコレクションクラスの要素でも読み出すことが出来ます。
例えば、.NET Framework標準ライブラリのArrayList
クラスはIEnumrable
インターフェースを実装していますので、以下のようにforeach文を使ってコレクション内の要素を列挙することが出来ます。
ArrayList list = new ArrayList();
for(int i=0; i<10; ++i)
{
list.Add(i * (i + 1) / 2);
}
foreach(int s in list)
{
Console.Write(s + " ");
}
0 1 3 6 10 15 21 28 36 45
余談: パターン ベース
余談になりますが、 foreach で使うコレクションは、実は IEnumerable を実装している必要はなくて、 GetEnumerator という名前のメソッドを持っていればどんな型でもよかったりします。 (要するに、「パターン ベース」。)
拡張メソッドでの GetEnumerator 実装
Ver. 9
C# 8.0 まではパターン ベースと言っても、GetEnumerator
メソッドはインスタンス メソッドである必要がありました。
これが C# 9.0 で緩和されて、拡張メソッドでの実装が認められました。
例えば、C# 8.0 で入った Range
に対して以下のような拡張メソッドを書くことで、foreach (var i in x..y)
みたいな書き方ができるようになります。
using System;
foreach (var i in 5..10)
{
Console.WriteLine(i); // 5, 6, 7, 8, 9
}
static class RangeExtension
{
public static RangeEnumerator GetEnumerator(this Range r) => new(r);
public struct RangeEnumerator
{
private int _i;
private int _end;
public RangeEnumerator(Range r)
{
_i = r.Start.Value - 1;
_end = r.End.Value;
}
public bool MoveNext() => ++_i < _end;
public int Current => _i;
}
}
(これまでは単に C# 1.0 時代からある文法に下手に手を入れるのが怖くて認められていなかっただけです。)
コレクションクラスの自作
IEnumrable
インターフェースを実装することで、foreach文で利用できるコレクションクラスを自作できます。
IEnumrable
インターフェースにはGetEnumerator
メソッドがあり、このメソッドはIEnumerator
インターフェースを返します。
コレクションクラスを自作する場合、このIEnumerator
インターフェースを実装する列挙子も自作する必要があります。
IEnumerator
インターフェースにはCurrent
というプロパティとMoveNext
、Reset
という2つのメソッドがあります。
Current
プロパティはコレクション内の現在の要素を取得するためのもので、
MoveNext
メソッドは列挙子をコレクションの次の要素に進めます。
また、Reset
メソッドは列挙子を初期位置、つまりコレクションの最初の要素の前に戻します。
using System;
using System.Collections;
/// <summary>
/// 片方向連結リストクラス
/// </summary>
class LinearList : IEnumerable
{
/// <summary>
/// 連結リストのセル
/// </summary>
private class Cell
{
public object value;
public Cell next;
public Cell(object value, Cell next)
{
this.value = value;
this.next = next;
}
}
/// <summary>
/// LinearList の列挙子
/// </summary>
private class LinearListEnumerator : IEnumerator
{
private LinearList list;
private Cell current;
public LinearListEnumerator(LinearList list)
{
this.list = list;
this.current = null;
}
/// <summary>
/// コレクション内の現在の要素を取得
/// </summary>
public object Current
{
get{return this.current.value;}
}
/// <summary>
/// 列挙子をコレクションの次の要素に進める
/// </summary>
public bool MoveNext()
{
if(this.current == null)
this.current = this.list.head;
else
this.current = this.current.next;
if(this.current == null)
return false;
return true;
}
/// <summary>
/// 列挙子を初期位置に戻す
/// </summary>
public void Reset()
{
this.current = null;
}
}
private Cell head;
public LinearList()
{
head = null;
}
/// <summary>
/// リストに新しい要素を追加
/// </summary>
public void Add(object value)
{
head = new Cell(value, head);
}
/// <summary>
/// 列挙子を取得
/// </summary>
public IEnumerator GetEnumerator()
{
return new LinearListEnumerator(this);
}
}
class ForeachSample
{
static void Main()
{
LinearList list = new LinearList();
for(int i=0; i<10; ++i)
{
list.Add(i * (i + 1) / 2);
}
foreach(int s in list)
{
Console.Write(s + " ");
}
}
}
45 36 28 21 15 10 6 3 1 0
Ver. 2.0
このようなコレクションクラスを自作する作業は結構面倒なんですが、 C# 2.0 ではこの作業を簡単化するための「イテレーター」という機能が追加されました。 詳しくは、「イテレーター」で説明します。
foreach 文のパフォーマンス
「foreach文とは」で説明したように、 一般には、foreach 文は以下のようなコードに展開されます。 (IDispose を実装しない場合。 IDispose を実装するクラスの場合には、 さらに「using ステートメント」で囲ったのと同じ扱いになります。)
IEnumerator e = array.GetEnumerator();
while(e.MoveNext())
{
型名 変数 = (型名)e.Current;
文
}
このコードだと、
MoveNext() や Current などのメソッド呼び出しのオーバーヘッドが結構大きくて、
for(int i; i < array.Length; ++i) 文;
というようなコードに比べると少し実行効率が悪くなります。
ただ、配列に対して foreach を使った場合、 最適化がかかって for 文相当のコードに変換されるようで、 そこまで大きな差はなくなるようです。
非同期 foreach
Ver. 8.0
C# 8.0で非同期版のforeach
が追加されました。
await foreach
(foreach
の前にawait
を付ける)という構文で、
IAsyncEnumerable<T>
インターフェイス(System.Collections.Generic
名前空間)か、それと同じパターンを満たす型の列挙ができます。
static async Task AsyncForeach(IAsyncEnumerable<int> items)
{
await foreach (var item in items)
{
Console.WriteLine(item);
}
}
詳しくは「非同期foreach」で説明します。