program study story

プログラムの勉強 アウトプット

イテレーターとは

・IEnumeratorを簡単に実装するための機能。

イテレーターブロック:

イテレーターブロックを使うことで、「foreach文」で利用可能なコレクションを返す
メソッドやプロパティを簡単に実装することができる。

 

 

 

Visual C# 2017パーフェクトマスター (Perfect Master)

Visual C# 2017パーフェクトマスター (Perfect Master)

  • 作者:金城俊哉
  • 発売日: 2017/09/23
  • メディア: 単行本
 

 

------------------------------------------------------------------------------------

using System.Collections.Generic;

class TestEnumerable
{
// ↓これがイテレーター ブロック。IEnubrable を実装するクラスを自動生成してくれる。
static public IEnumerable<int> FromTo(int from, int to)
{
while(from <= to)
yield return from++;
}

static void Main(string args)
{
// ↓こんな感じで使う。
foreach(int i in FromTo(10, 20))
{
Console.Write("{0}\n", i);
}
}
}

------------------------------------------------------------------------------------

■ 通常のブロックとの違い:

・System.Collections.IEnumerator
・System.Collections.Generic.IEnumerator<T>
・System.Collections.IEnumerable
・System.Collections.Generic.IEnumerable<T>
・returnの代わりにyield returnというキーワードを使う。
・breakの代わりにyield breakというキーワードを使う。

イテレーターブロック中で、yield return文が呼ばれるたびに、
foreach文中で使われる値を1つ得る。
for文や、while文を使わず、ベタにyield returnを並べてもok
------------------------------------------------------------------------------------

static void Main(string args)
{
// ↓こんな感じで使う。
foreach(int i in FromTo(10, 20))
{
Console.Write("{0}\n", i);
}
}

------------------------------------------------------------------------------------

static public IEnumerable GetEnumerable(int from, int to)
{
yield return 1;
yield return 3.14;
yield return "文字列";
yield return new System.Drawing.Point(1, 2);
yield return 1.0f;
}

------------------------------------------------------------------------------------

■ イテレーターの制限:

イテレーターブロックは、戻り値を返せるような関数メンバー(メソッド、演算子、プロパティのget,インデクサーのget)なら、
基本的には何に対しても使える。

しかし、いくつかの制限がある。

・unsafeにはできない。
・引数をref,outにできない。

・以下の場所にyield return,yield break共に書けない場所がある。
finally区内
匿名関数の中(匿名なイテレーターブロック自体作れない)
・catch句のみを持つtry句内(finally句のみを持つtry句内にはyield returnを書けます)
・catch句内

■ GetEnumerator

foreach文で利用できるコレクションクラスを自作するには、
IEnumerableインターフェースを継承し、GetEnumeratorメソッドをオーバーライドする。

※ C#2.0では、このような方法のほかに、GetEnumeratorという名前のイテレータブロックを定義することでもコレクションクラス
を作成できる。
------------------------------------------------------------------------------------
例:GetEnumerator と言う名前のイテレーター ブロックを定義パターン
------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;

namespace ConsoleAppIEnumeratorPractice
{
class Program
{
static void Main(string args)
{
LinearList<int> list = new LinearList<int>();

for (int i = 0; i < 10; i++)
{
list.Add(i * (i + 1) / 2);
}

foreach (int s in list)
{
Console.Write(s + " ");
}
}
}
class LinearList<T>
{
private class Cell
{
public T value;
public Cell next;

public Cell(T value,Cell next)
{
this.value = value;
this.next = next;
}
}
private Cell head;

public LinearList()
{
this.head = null;
}

public void Add(T value)
{
this.head = new Cell(value, head);
}

public IEnumerator<T> GetEnumerator()
{
for (Cell c = this.head; c != null; c = c.next)
{
yield return c.value;
}
}
}
}

------------------------------------------------------------------------------------

■ イテレーターのコンパイル結果

C#コンパイラは、イテレーターブロック内のfor文を、MoveNextメソッド内のようなコードに展開してくれている。

------------------------------------------------------------------------------------
イテレーターブロック
------------------------------------------------------------------------------------
public IEnumerator<T> GetEnumerator()
{
for(Cell c = this.head; c != null; c = c.next)
{
yield return c.value;
}
}

------------------------------------------------------------------------------------
イテレーターブロックコンパイル時
------------------------------------------------------------------------------------
public bool MoveNext()
{
switch (__state)
{
case 1: goto __state1;
case 2: goto __state2;
}
i = __this.top - 1;

__loop:
if (i < 0) goto __state2;
__current = __this.buf[i];
__state = 1;
return true;

__state1:
--i;
goto __loop;

__state2:
__state = 2;
return false;
}
------------------------------------------------------------------------------------

やっていることを簡単に:
イテレーターブロックのfor内がyield return xの際、

state = State1; // 次に復帰するといのための状態の記録
Current = x; // 戻り値をCurrentに保持
return true; // いったん処理終了
case States1: // 次に呼ばれたときに続きから処理するためのラベル

これをswitch文で囲うことで、処理の一時中断と再開を実現する。

■ リソースの破棄

Dispose()メソッドなどを直接呼び出すことでできるが、イテレーターブロック中でDispose()を
呼び出しても正しく呼び出されない場合もある。

------------------------------------------------------------------------------------

tatic IEnumerable<string> Lines(string path)
{
System.IO.StreamReader sr = new System.IO.StreamReader(path);

string line;
while ((line = sr.ReadLine()) != null)
{
yield return line;
}

sr.Dispose(); // この行は呼ばれないことがある
}
------------------------------------------------------------------------------------

利用側のforeachループにbreakなどを書くと、yield returnから後ろが実行されなくなる。
breakを1つ追加するだけで、イテレーターブロック内の最後の1行が実行されなくなる。

正しくsr.Dispose();が呼ばれるようにしたければ、
イテレーターブロック内で「try-catch-finally文」や「usingステートメント」を使う。

------------------------------------------------------------------------------------
usingステートメントパターン
------------------------------------------------------------------------------------

static IEnumerable<string> Lines(string path)
{
using(System.IO.StreamReader sr=new System.IO.StreamReader(path))
{
string line;
while ((line=sr.ReadLine()) !=null)
{
yield return line;
}
}
}
------------------------------------------------------------------------------------

■ 内部イテレータと外部イテレータについて(デザインパターンの違い)

内部イテレータと外部イテレータとは
コレクションの要素の列挙・反復の方法には、2つのデザインパターンがあり、
その方法が内部イテレータと外部イテレータ。

言語によっての違い:

・C#のEnumerator
全要素に対して順方向アクセスしかできない

・C++のiterator
全要素に対して順方向・逆方向・双方向・ランダムアクセスが可能


◇ 内部イテレータ
------------------------------------------------------------------------------------
例:内部イテレータ
------------------------------------------------------------------------------------

public delegate void ForEachAction(int x);

public class List
{
int items;

public List(params int items)
{
this.items = items;
}

/// <summary>
/// 内部イテレータ的に、リストの各要素にたいしてactionを適用する
/// </summary>
/// <param name="action"></param>
public void ForEach(ForEachAction action)
{
for (int i = 0; i <this.items.Length; i++)
{
action(this.items[i]);
}
}
}


static void Main(string args)
{
List l = new List(1, 2, 3, 4, 5);

int sum = 0;
l.ForEach(delegate (int x)
{
sum += x;
});
Console.Write("sum = {0}\n",sum);
}

・反復の仕方はListクラスの中のForEachメソッドの中に書いて、要素ごとに行いたい処理をデリゲートとして渡す。

メリット:
ForEachの実装は簡単

デメリット:
この方法だと、breakとかcontinueが使えない。

◇ 外部イテレータ

.NET Frameworkがとっているアプローチ。

------------------------------------------------------------------------------------
例:外部イテレータ: IEnumerator実装クラス
------------------------------------------------------------------------------------

// <summary>
/// 外部イテレータ用のIEnumerator実装クラス
/// </summary>
class Enumerator : IEnumerator<int>
{
List l;
int n;

internal Enumerator(List l)
{
this.l = l;
this.n = -1;
}

public int Current
{
get { return l.items[n]; }
}

void IDisposable.Dispose() { }

object System.Collections.IEnumerator.Current
{
get { return this.Current; }
}
bool System.Collections.IEnumerator.MoveNext()
{
++n;
return n != l.items.Length;
}

void System.Collections.IEnumerator.Reset()
{
n = -1;
}
}

/// <summary>
/// 外部イテレータを返す処理
/// </summary>
/// <returns>イテレータ</returns>
public IEnumerator<int> GetEnumerator()
{
return new Enumerator(this);
}
}

------------------------------------------------------------------------------------
Pramgramクラス(IEnumeratorクラス呼び出し側)
------------------------------------------------------------------------------------
class Program
{
static void Main(string[] args)
{
List l = new List(1, 2, 3, 4, 5);

int sum = 0;

IEnumerator<int> e = l.GetEnumerator();

while (e.MoveNext())
{
sum += e.Current;
}

Console.Write("sum = {0}\n",sum);
}
}
------------------------------------------------------------------------------------

Enumeratorという別のクラスを通してitems中の要素を1つずつ取り出す。

メリット:
・コードの見た目的に、whileを使っていることから反復処理らしく見える。
・breakやcontinueも使える


デメリット:
・IEnumeratorを実装する作業が必要
・外部イテレータの場合、ループ一回に付きMoveNextとCurrent(のgetter)という2回のメソッド呼び出しがあるため、
 アプローチの速度が少しだけ遅い。

■ C#のforeach

外部イテレータ的アプローチの欠点を克服

1.MoveNextとかCurrentとかいちいち書くのがめんどくさい
2.IEnumeratorの実装がものすごい面倒

◎「1.MoveNextとかCurrentとかいちいち書くのがめんどくさい」を解決するのが、C#のforeach
Listクラスにforeach文を使うために必要なコードの大半を書いているので、
IEnumerableインターフェースを実装するだけ。

◎2.IEnumeratorの実装がものすごい面倒を解決するのが、イテレーター構文

------------------------------------------------------------------------------------
◇ 「外部イテレータ」:書き方
------------------------------------------------------------------------------------
/// <summary>
/// 外部イテレータを返す処理
/// </summary>
/// <returns>イテレータ</returns>
public IEnumerator<int> GetEnumerator()
{
return new Enumerator(this);
}
------------------------------------------------------------------------------------

上記構文を、下記のように変更するだけ。

------------------------------------------------------------------------------------
◇ 「イテレータ構文」:書き方
------------------------------------------------------------------------------------
/// <summary>
/// イテレータ構文を使って外部イテレータを自動生成。
/// </summary>
/// <returns>イテレータ</returns>
public IEnumerator<int> GetEnumerator()
{
for (int i = 0; i < this.items.Length; ++i)
{
yield return this.items[i];
}
}
------------------------------------------------------------------------------------

内部イテレータで書いたForEachメソッドとほどんど一緒で、
actionデリゲート呼び出しの部分がyield returnに変えただけ。
------------------------------------------------------------------------------------
◇ 「内部イテレータ」:書き方
------------------------------------------------------------------------------------
/// <summary>
/// 内部イテレータ的に、リストの各要素にたいしてactionを適用する
/// </summary>
/// <param name="action"></param>
public void ForEach(ForEachAction action)
{
for (int i = 0; i <this.items.Length; i++)
{
action(this.items[i]);
}
}
------------------------------------------------------------------------------------

● まとめ

イテレータ構文とは:
内部イテレータ的な書き方で外部イテレータを自動生成するもの。
※ ほかの言語によっては、このような構文についてを,「ジェネレータ」と呼んだりする。