[C#] 48. IEnumerableとIEnumerator、そしてyieldキーワード


Study / C#    作成日付 : 2021/10/11 19:49:33   修正日付 : 2021/10/11 19:50:10

こんにちは。明月です。


この投稿はIEnumerableとIEnumerator、そしてyieldキーワードに関する説明です。


以前に私がLinq式を説明しながらIEnumerableに関して簡単に説明したことがあります。

link - [C#] 32. ジェネリックタイプ(Generic Type)を使い方


IEnumerableのインタフェースは繰り返しパターンと関係あるパターンですが、我々がよく使う繰り返しキーワードforeachで使っているインターフェースです。

link - 作成中


IEnumerableのインタフェースはGetEnumerator関数が定義されているし、IEnumeratorタイプで返却することになっています。


そしてIEnumeratorの場合はforeachで使っているパターンの動作インターフェースとして、現在値に関するプロパティのCurrent、ポイント移動と値が存在するかを確認する関数のMoveNext、そしてポインタの位置を初期化する関数Resetになっています。


using System;
using System.Collections;

namespace Example
{
  // IEnumerableを継承したListクラス
  class List : IEnumerable
  {
    // IEnumeratorを継承したNodeクラス
    private class Node : IEnumerator
    {
      // データを1から10までの配列
      private int[] data = new int[]
      {
        1,2,3,4,5,6,7,8,9,10
      };
      // 現在の位置ポジション
      private int pos = 0;
      // 現在のポジション値をリターン
      public object Current
      {
        get
        {
          // pos-1を取得する。
          return data[pos - 1];
        }
      }
      // ポジション移動
      public bool MoveNext()
      {
        // ポジションが配列サイズを超えたらfalse
        if (pos >= data.Length)
        {
          return false;
        }
        // ポジション移動
        pos++;
        // true
        return true;
      }
      // ポジション位置初期化
      public void Reset()
      {
        // ポジション移動 0
        pos = 0;
      }
    }
    // インスタンス生成
    private IEnumerator node = new Node();
    // GetEnumeratorをリターン
    public IEnumerator GetEnumerator()
    {
      // ポジション初期化
      node.Reset();
      // Nodeインスタンスリターン
      return node;
    }
  }
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // リストインスタンス生成
      var list = new List();
      // 繰り返しでlistのGetEnumerator関数を通って繰り返しでパターンタイプで取得する。
      foreach (int item in list)
      {
        // MoveNextとCurrentを通って値をコンソールに表示
        Console.WriteLine(item);
      }
      
      // 繰り返しでlistのGetEnumerator関数を通って繰り返しでパターンタイプで取得する。
      foreach (int item in list)
      {
        // MoveNextとCurrentを通って値をコンソールに表示
        Console.WriteLine(item);
      }
      
      // 任意のキーを押してください
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}


foeachの繰り返しキーワードはIEnumerableのインスタンスを継承したクラスの値を使えます。 我々が普通にListやArrayをforeachに入れて使いますが、このクラスがIEnumerableのインスタンスを継承したことと同じ意味です。

IEnumerableのインタフェースはGetEnumerator関数が定義されているし、IEnumeratorのインタフェースを継承したインスタンスをリターンします。

IEnumeratorのインタフェースにはforeachで使うならReset関数を呼び出してポインタを初期化してforeachで次のポインタに移動する時にMoveNextの関数を呼び出して、itemでデータを取得する時にはCurrentプロパティを使います。


ここで少し変なコードが見えますが、MoveNext関数が先に呼び出してCurrentのプロパティからデータを取得します。MoveNextの返却値は現在の値がnullかどうかに関するチェックだし、ポイントを移動しなければならないです。

Currentは現在の値を取得することなのでCurrentではpos - 1で値をリターンするし、MoveNext関数では現在ポインタの現在のポインタに関するnullチェックでpos >= data.Lengtのことで確認してposのポインタを移動します。


ここで我々はyieldキーワードを使ったらIEnumeratorをもっとしやすく使えます。

using System;
using System.Collections;

namespace Example
{
  // IEnumerableを継承したListクラス
  class List : IEnumerable
  {
    // データを1から10までの配列
    private int[] data = new int[]
    {
      1,2,3,4,5,6,7,8,9,10
    };
    // GetEnumeratorをリターン
    public IEnumerator GetEnumerator()
    {
      // yieldキーワードを通って上のdata配列が順番とおりにreturnする。
      yield return data[0];
      yield return data[1];
      yield return data[2];
      yield return data[3];
      yield return data[4];
      yield return data[5];
      yield return data[6];
      yield return data[7];
      yield return data[8];
      yield return data[9];
    }
  }
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // リストインスタンス生成
      var list = new List();
      // 繰り返しでlistのGetEnumerator関数を通って繰り返しでパターンタイプで取得する。
      foreach (int item in list)
      {
        // MoveNextとCurrentを通って値をコンソールに表示
        Console.WriteLine(item);
      }

      // 繰り返しでlistのGetEnumerator関数を通って繰り返しでパターンタイプで取得する。
      foreach (int item in list)
      {
        // MoveNextとCurrentを通って値をコンソールに表示
        Console.WriteLine(item);
      }

      // 任意のキーを押してください
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}


IEnumeratorインタフェースを継承したクラスと同じ結果が表示されます。

yieldの場合は呼び出す時たびにreturnの値を別にすることができます。つまり、GetEnumerator()呼び出しになるとyieldキーワードを把握して一つの関数でなっている連結リストを生成します。

そしてMoveNextが呼び出すたびに次のyieldまで実行することです。


また、IEnumeratorインターフェースはforeachだけで使うことではなく、Linqでも使えます。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace Example
{
  // IEnumerableを継承したListクラス
  class List : IEnumerable<int>
  {
    // データを1から10までの配列
    private int[] data = new int[]
    {
      1,2,3,4,5,6,7,8,9,10
    };
    // GetEnumeratorをリターン(IEnumerable<int>インターフェース)
    public IEnumerator<int> GetEnumerator()
    {
      // データの個数程
      yield return data[0];
      yield return data[1];
      yield return data[2];
      yield return data[3];
      yield return data[4];
      yield return data[5];
      yield return data[6];
      yield return data[7];
      yield return data[8];
      yield return data[9];
    }
    // GetEnumeratorをリターン (IEnumerableインターフェース)
    IEnumerator IEnumerable.GetEnumerator()
    {
      // オバーロードにより関数が分けっていますが、上の関数を呼び出したら良いです。
      return GetEnumerator();
    }
  }
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // リストインスタンス生成
      var list = new List();
      // 上のyieldになっているIEnumeratorをLinq式でフィルターすることができる。
      var ret = list.Where(x => x > 5).OrderByDescending(x => x);
      // 結果をforeach
      foreach (var item in ret)
      {
        // コンソールに出力
        Console.WriteLine(item);
      }

      // 任意のキーを押してください
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}


実はyieldキーワードはよく使いません。私も文法は分かりますが、かなり不慣れな文法です。もちろん、開発する人により違いますが、私の場合は他の言語(?)にはない文法だし、不慣れな文法で可読性を悪くする理由はないと思います。

実は普通のIEnumerableやIEnumeratorを継承して開発することが多くないです。仕様によりキャッシュアルゴリズムを作成するかListアルゴリズムをもっと効率的に改善することができますが、.Net Frameworkで提供するListアルゴリズムが優秀だし、新しいアルゴリズムを作成することでバグに関する安定性を保証することができません。

Arrayもあるし、Listを使ってもシステムが遅くなることではないです。


ここまでIEnumerableとIEnumerator、そしてyieldキーワードに関する説明でした。


ご不明なところや間違いところがあればコメントしてください。

#C#
最新投稿