連載:C# 2.0入門

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

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

yieldは予約語ではない

 互換性を維持するため、「yield」は予約語ではなく、returnやbreakキーワードの手前に書かれた場合のみ、特別な意味を持つようになっている。

 そのため、yieldという名前を変数などに使うことは安全である。それどころか、returnキーワードの手前に書かない限り特別な意味を持たないため、yield returnの後に変数名yieldを書くこともできる(とはいえ、紛らわしいので変数などの名前にyieldは避けた方がよいだろう)。

 実際に書いた例をリスト5に示す。

using System;
using System.Collections.Generic;

class Sample
{
  public IEnumerator<int> GetEnumerator()
  {
    int yield = 12345;
    yield return yield;
  }
}

class Program
{
  static void Main(string[] args)
  {
    foreach (int i in new Sample())
    {
      Console.Write(i);
    }
    // 出力:12345
  }
}
リスト5 「yield」を変数名として使った例

1つのクラスに複数の列挙機能を付ける

 クラス・ライブラリのDictionaryクラス(System.Collections.Generic名前空間)はそれ自身が列挙可能であると同時に、列挙可能なKeysプロパティやValuesプロパティを持っている。こういったクラスを反復子で実装するのも簡単である。

 リスト6は、配列をそのままの順番で列挙するGetEnumeratorメソッドと、逆順で列挙するGetReverseOrderメソッドを提供するクラスの例である。

using System;
using System.Collections.Generic;

class Sample
{
  private int[] array = { 1, 2, 3 };

  public IEnumerator<int> GetEnumerator()
  {
    for (int i = 0; i < array.Length; i++)
    {
      yield return i;
    }
  }

  public IEnumerable<int> GetReverseOrder()
  {
    for (int i = array.Length - 1; i >= 0 ; i--)
    {
      yield return i;
    }
  }
}

class Program
{
  static void Main(string[] args)
  {
    foreach (int i in new Sample())
    {
      Console.WriteLine(i);
    }
    // 出力:
    // 0
    // 1
    // 2

    foreach (int i in new Sample().GetReverseOrder())
    {
      Console.WriteLine(i);
    }
    // 出力:
    // 2
    // 1
    // 0
  }
}
リスト6 反復子で複数の列挙機能を持つクラスを作る

 ここでは、GetEnumeratorメソッドはforeach文内で明示的に書く必要はないが、GetReverseOrderメソッドは明示的に名前を書く必要があることに留意しよう。

 また、このリスト6には、1つだけ注意点がある。それは、GetEnumeratorメソッドの戻り値の型はIEnumerator<int>型であるのに対して、GetReverseOrderメソッドはIEnumerable<int>型になっているという点である。

 GetEnumeratorメソッドとGetReverseOrderメソッドは、ソース・コードだけ見ているとよく似た兄弟に見えるが、foreach文から見ると扱いがまったく別個なのである。foreach文は通常IEnumerable<T>型のオブジェクトを受け取って処理するが、特定の型を返すGetEnumeratorという名前のpublicなメソッドは、それだけで列挙可能と見なされるのである。

自動的に作られるオブジェクトと2重利用

 反復子ブロックはブロックではないので、直感に反する動作を行うように見えることがある。

 例えば次のリスト7のように、たった1つの反復子ブロックを2重の繰り返しで使うと、1つのメソッドが内側のループと外側のループで共有されるため、正常にカウントされないように思えるかもしれない。ところが、このコードは、内側と外側の繰り返しが別個に、かつ正常にカウントされるのである。

using System;
using System.Collections.Generic;

class Program
{
  private static IEnumerable<int> getCounter(int from, int to)
  {
    for (int i = from; i <= to; i++) yield return i;
  }

  static void Main(string[] args)
  {
    IEnumerable<int> range = getCounter(0, 2);

    foreach (int j in range)
    {
      foreach (int i in range)
      {
        Console.Write("({0},{1}) ", i, j);
      }
      Console.WriteLine();
    }
    // 出力:
    // (0,0) (1,0) (2,0)
    // (0,1) (1,1) (2,1)
    // (0,2) (1,2) (2,2)
  }
}
リスト7 2重に反復子ブロックを使った例

 この場合、ソース・コードを見ると列挙を行うオブジェクトは、getCounterメソッド呼び出しによって1つだけ生成され、そのオブジェクトはIEnumerable<int>インターフェイスを実装しているので変数rangeに代入された……というふうに見えるかもしれない。それ故に、列挙を行うオブジェクトは暗黙的に1つだけ作られて共有された……と見えるかもしれない。

 しかし、実際はそうではない。

 列挙可能インターフェイス(IEnumerable<T>)は「列挙子ファクトリ」という機能を持つ。つまり、このインターフェイスは列挙を行うオブジェクト(列挙子)を作り出すファクトリ(作成工場)として機能する。

 そのため、「foreach (int i in range)」といったコードは、変数rangeを通じて列挙オブジェクトを取得しようとする作業の過程で新しい列挙オブジェクトを作り出すのである。このソース・コードにはこういったコードが2つあるため、実行時には列挙オブジェクトは2つ生成される。

 つまり、

反復子ブロック → 1つの列挙子ファクトリ → 2つの列挙オブジェクト

という流れになる。その結果として、内側と外側のforeach文は別の列挙オブジェクトを使い、別々にカウントが行われる。

catchできない制約

 yield return文に慣れると、リスト8のようなコードを書いてみるプログラマーも出てくると思う。だが、このコードはコンパイルできない。

catch 句を含む try ブロックの本体で値を生成することはできません。

というエラーが出てしまうからだ。

using System;
using System.Collections.Generic;
using System.IO;

class Program
{
  private static IEnumerable<string> readTextFile(string filename)
  {
    StreamReader reader = File.OpenText(filename);
    try
    {
      for (; ; )
      {
        string s = reader.ReadLine();
        if (s == null) break;
        yield return s;
      }
    }
    catch (IOException e)
    {
      Console.WriteLine(e.ToString());
    }
    finally
    {
      reader.Close();
    }
  }

  static void Main(string[] args)
  {
    foreach (string s in readTextFile(@"c:\test.txt"))
    {
      Console.WriteLine(s);
    }
  }
}
リスト8 コンパイルできない例

 実は、yield return文は、例外処理のためのtry〜catch構文の中で使用できないという制約を持つ。制約があるのはこの組み合わせだけである。例えば、yield break文をtry〜catch構文の中で使用することはできるし、yield return文は、try〜finally構文の中で使用することができる。

 そこで、yield return文のみ、try〜catch構文から外すように書き直したのがリスト9である。

using System;
using System.Collections.Generic;
using System.IO;

class Program
{
  private static IEnumerable<string> readTextFile(string filename)
  {
    StreamReader reader = File.OpenText(filename);
    try
    {
      for (; ; )
      {
        string s;
        try
        {
          s = reader.ReadLine();
        }
        catch (IOException e)
        {
          Console.WriteLine(e.ToString());
          yield break;
        }
        if (s == null) break;
        yield return s;
      }
    }
    finally
    {
      reader.Close();
    }
  }

  static void Main(string[] args)
  {
    // 実行時にはテキスト・ファイル「c:\test.txt」が必要
    foreach (string s in readTextFile(@"c:\test.txt"))
    {
      Console.WriteLine(s);
    }
  }
}
リスト9 リスト8をコンパイル可能に修正した例

 見てのとおり、かなり回りくどくなったが、同じ意図を記述することはできる。


 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 記事ランキング

本日 月間