連載:C# 2.0入門

第3回 新しい繰り返しのスタイル − yield return文とForEachメソッド

株式会社ピーデー 川俣 晶
2007/07/31
Page1 Page2 Page3 Page4

C# 2.0によるRangeクラスの実装

 この2つのうっとうしさは、C# 2.0の反復子の機能を使って書き直すと、きれいさっぱり消えてなくなる。

 ともかく、圧倒的にスリムになったコードを見ていただきたい。

using System;
using System.Collections.Generic;

class Range
{
  private int from, to;

  public IEnumerator<int> GetEnumerator()
  {
    for (int i = from; i <= to; i++) yield return i;
  }

  public Range(int from, int to)
  {
    this.from = from;
    this.to = to;
  }
}

class Program
{
  static void Main(string[] args)
  {
    foreach (int i in new Range(0, 9))
    {
      Console.Write("{0} ", i);
    }
    // 出力:0 1 2 3 4 5 6 7 8 9
  }
}
リスト2 C# 2.0によるRangeクラスの実装

 以上のように、2つ必要だったクラスは1つになり、MoveNextメソッド、Currentプロパティは消滅し、状態を記憶しておくフィールドは単なるローカルなループ変数へと立場を大幅に後退させてしまった。

 この程度のコード量であれば、気軽に列挙可能なオブジェクトを作ろう……という気にもなるだろう。

 さて、どのような構文がこの劇的な変化をもたらしたのだろうか。

 C# 2.0版で目新しい構文は、「IEnumerator<int>」と「yield return i;」しかない。このうち、前者はC# 1.xからあるIEnumerableインターフェイス(System.Collections名前空間)のジェネリック版にすぎないので、型が明示されたことを除けば何ら目新しいことはない。

 つまり、注目ポイントは「yield return i;」だけである。ところが、このたった1つの文がすべてを劇的に変えてしまうのである。

 実は、「yield return文」と、後で紹介する「yield break文」を含むブロックは、通常のブロックではなく「反復子ブロック」というものになる。

 つまりリスト2内の以下のブロックは、反復子ブロックになる。

{
  for (int i = from; i <= to; i++) yield return i;
}

 反復子ブロックは通常のブロックとは異なり、すぐには実行されず、その代わりに列挙子オブジェクトというものが生成される。

 ソース・コード上で書かなくてよくなったクラス(リスト1のRangeEnumクラスのようなクラス)に相当するクラスが自動的に用意され、そのインスタンスが作成されるのである。その作業はC#プログラマーからは見えない水面下の部分で自動的に行われる。

 しかし、そのような詳細を知る必要はない。foreach文で使うとすれば、C#プログラマーが知る必要があるのは、以下のことだけである。

  • 反復子ブロックはすぐには実行されない
  • 実行されるのはforeach文で列挙が開始された後
  • 反復子ブロック内でyield return文が実行されると、その引数の値がforeach文に渡り、文が1回実行される
  • foreach文に指定された実行文(リスト2ではConsole.Writeメソッド)の実行が終わると、反復子ブロック内の続きが実行される

 ここで注意が必要なのは、yield return文は、return文と同じように記述し、同じように値を呼び出し側に返すが、呼び出し側の処理が一段落するとその続きが再度実行されるという点である。

 例えば、次のリスト3には2つのyield return文が存在するが、これらは双方とも実行される。最初のyield return文で終わってしまうわけではない。一時的に呼び出し元に実行権を譲るだけである。

using System;
using System.Collections.Generic;

class Sample
{
  public IEnumerator<char> GetEnumerator()
  {
    Console.Write("[Cを返す]");
    yield return 'C';

    Console.Write("[#を返す]");
    yield return '#';
  }
}

class Program
{
  static void Main(string[] args)
  {
    foreach (char c in new Sample())
    {
      Console.Write("{0}", c);
    }
    // 出力:[Cを返す]C[#を返す]#
  }
}
リスト3 実行継続されるyield return文の確認

 実行結果を見ると分かるとおり、foreach文で指定された実行文と、反復子ブロック内の文は交互に実行される。このような実行形態はあまり例がなく、一見して分かりにくいかもしれない。しかし、それによってソースが短くなり、コーディング負担も減り、楽ができるわけである。楽ができる以上は、正しく理解するために努力する価値がある。

yeild break文による中断

 さて、ここで気になるのはyield return文が列挙を中断しないとしたら、中断させるにはどうすればよいのか……ということである。

 処理の途中で何かの例外的条件に引っ掛かって、そこで処理を打ち切るのは定番処理の1つである。

 ここで、「簡単簡単、return文でメソッド抜ければ終わるのだろう?」と考えた読者も多いと思うが話はそれほど甘くはない。なぜなら、反復子ブロックはブロックであってブロックではないからだ。反復子ブロックは、実際に実行される場合には、バラバラに分解され、自動的に生成される列挙子オブジェクトに組み込まれることになる。その結果として、単純にreturn文で抜け出せるような簡単な構造ではなくなっているのだ。

 しかし、それでは不便なので、途中で反復子ブロックを打ち切って終了する「yield break文」が用意されている。これを使えば、途中で打ち切る処理は容易に記述できる。

 リスト4にそれを使った例を示す。リスト4では「yield break;」が実行されるとその後のコードは実行されないので、「[#を返す]」や「#」が出力されることはない。

using System;
using System.Collections.Generic;

class Sample
{
  public IEnumerator<char> GetEnumerator()
  {
    Console.Write("[Cを返す]");
    yield return 'C';

    Console.Write("[yield break実行]");
    yield break;

    Console.Write("[#を返す]");
    yield return '#';
  }
}

class Program
{
  static void Main(string[] args)
  {
    foreach (char c in new Sample())
    {
      Console.Write("{0}", c);
    }
    // 出力:[Cを返す]C[yield break実行]
  }
}
リスト4 yield break文で処理を打ち切る


 INDEX
  C# 2.0入門
  第3回 新しい繰り返しのスタイル − yield return文とForEachメソッド
    1.繰り返しという古くて新しい問題/数を数えるというサンプル/C# 1.xによるRangeクラスの実装
  2.C# 2.0によるRangeクラスの実装/yeild break文による中断
    3.yieldは予約語ではない/1つのクラスに複数の列挙機能を付ける/自動的に作られるオブジェクトと2重利用/catchできない制約
    4.制約の真相:見た目と違う真実の姿/ForEachメソッドを使う別解/性能比較
 
インデックス・ページヘ  「C# 2.0入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間