++C++; // 未確認飛行 C 避けて通れない「非同期処理」を克服しよう

Top総合 目次C# によるプログラミング入門

foreach

このエントリーをはてなブックマークに追加

目次

キーワード

概要

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 コレクション)
  

このコードは以下のように展開されます。

IEnumerator e = array.GetEnumerator();
while(e.MoveNext())
{
  型名 変数 = (型名)e.Current;
  
}

ただし、 GetEnumerator で取得した列挙子が IDisposable を継承する場合、 ループ終了後に Dispose が呼ばれます。 要するに、上記のコードの代わりに、以下のようなコードに展開されます。

try
{
  IEnumerator e = array.GetEnumerator();
  while(e.MoveNext())
  {
    型名 変数 = (型名)e.Current;
    
  }
} 
finally
{
  e.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 

余談: duck typing

余談になりますが、 foreach で使うコレクションは、実は IEnumerable を実装している必要はなくて、 GetEnumerator という名前のメソッドを持っていればどんな型でもよかったりします。 (要するに、ダックタイピング。)

コレクションクラスの自作

IEnumrableインターフェースを実装することで、foreach文で利用できるコレクションクラスを自作できます。

IEnumrableインターフェースにはGetEnumeratorメソッドがあり、このメソッドはIEnumeratorインターフェースを返します。 コレクションクラスを自作する場合、このIEnumeratorインターフェースを実装する列挙子も自作する必要があります。

IEnumeratorインターフェースにはCurrentというプロパティとMoveNextResetという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 文相当のコードに変換されるようで、 そこまで大きな差はなくなるようです。

[お問い合わせ](q)