C#のyield returnの内部挙動を理解する

## はじめに

### yield returnとは

なかなか一言での説明が難しいですね。
なにか複数あるものを1個ずつ取得しようとする処理をreturnを使って書けようにするキーワード、といったところでしょうか。
LINQのWhereやOrderByなどの中間操作の内部処理等で多く使用されています。


サンプルコードをみてみましょう。

```cs
static void Main(string[] args)
{
    Console.WriteLine("Start");
    foreach (var x in YieldSample())
    {
        Console.WriteLine($"loop {x}");
    }
    Console.WriteLine("End");
}

static IEnumerable<int> YieldSample()
 {
     Console.WriteLine("yield return 1");
     yield return 1;
     Console.WriteLine("yield return 2");
     yield return 2;
 }
```

このコードの実行結果は次のようになります。

```
Start
yield return 1
loop 1
yield return 2
loop 2
End
続行するには何かキーを押してください . . .
```
予想通りでしたか？

`YieldSample()` を呼び出すと、一見そのメソッドの処理を最初から最後まで実行するように見えてしまいますが、そうではありません。
このように、「値が必要になったときに値を取るために必要な処理をする」という遅延評価の仕組みを`return`に似たキーワードを使って書けるもの、それが`yield return`である、と言うことができそうです。

## 内部挙動を見てみる

### 逆コンパイル結果を確認する

今ままでの部分の説明はわりとよく見かけるかもしれません。
ただ、実際にどうやって動いているのか、と少しもやもやは残ります。

そこで例によって（前回の記事[【図解】C#のasync/awaitの内部挙動を理解する](https://qiita.com/mrngsht/items/7d280c9f161f6d9855e2)と同じ作戦で）、yieldが定義されていない頃のC#で逆コンパイルした結果を見てみようと思います。yieldはC#2.0で定義されたのでC#1.0で逆コンパイルします。逆コンパイルにはILSpyというツールを使用します。

先ほどのサンプルコードの逆コンパイル結果です。

```cs
private static void Main(string[] args)
{
	Console.WriteLine("Start");
	foreach (int x in YieldSample())
	{
		Console.WriteLine("Loop");
		Console.WriteLine(x);
	}
	Console.WriteLine("End");
}

[IteratorStateMachine(typeof(<YieldSample>d__1))]
private static IEnumerable<int> YieldSample()
{
	return new <YieldSample>d__1(-2);
}
```

まず、`yield return`があった`YieldSample`メソッドの中身は、あるインスタンスを`new`して`return`するのみ、というとてもシンプル内容で置き換わっていることが確認できます。属性のところから`StateMachine`という文字も見えますね。

次に、この見覚えのない`<YieldSample>d__1`というクラスの定義部分について見てみましょう。少し長いですが、内容はわりとシンプルなのでそのまま載せてしまいます。

```cs
[CompilerGenerated]
private sealed class <YieldSample>d__1 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
{
	private int <>1__state;
	private int <>2__current;
	private int <>l__initialThreadId;

	int IEnumerator<int>.Current
	{
		[DebuggerHidden]
		get { return <>2__current; }
	}
	
	object IEnumerator.Current
	{
		[DebuggerHidden]
		get { return <>2__current; }
	}

	[DebuggerHidden]
	public <YieldSample>d__1(int <>1__state)
	{
		this.<>1__state = <>1__state;
		<>l__initialThreadId = Environment.CurrentManagedThreadId;
	}

	[DebuggerHidden]
	void IDisposable.Dispose() { }

	private bool MoveNext()
	{
		switch (<>1__state)
		{
		default:
			return false;
		case 0:
			<>1__state = -1;
			Console.WriteLine("yield return 1");
			<>2__current = 1;
			<>1__state = 1;
			return true;
		case 1:
			<>1__state = -1;
			Console.WriteLine("yield return 2");
			<>2__current = 2;
			<>1__state = 2;
			return true;
		case 2:
			<>1__state = -1;
			return false;
		}
	}
	
	bool IEnumerator.MoveNext()
	{
		//ILSpy generated this explicit interface implementation from .override directive in MoveNext
		return this.MoveNext();
	}

	[DebuggerHidden]
	void IEnumerator.Reset()
	{
		throw new NotSupportedException();
	}

	[DebuggerHidden]
	IEnumerator<int> IEnumerable<int>.GetEnumerator()
	{
		if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
		{
			<>1__state = 0;
			return this;
		}
		return new <YieldSample>d__1(0);
	}

	[DebuggerHidden]
	IEnumerator IEnumerable.GetEnumerator()
	{
		return System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator();
	}
}
```

### ポイント


細かいところは後においておくとして、この記事で主に伝えたかったポイントは下記の2点です。

* `yield return`は状態遷移機械を作って、どこまで処理を進めてどこまで値を返したかの状態管理を行う。
* `yield return`を書いた`YieldSample`メソッドの処理自体は、その状態遷移機械を作って`return`するだけ。元々`YieldSample`メソッドに書いていた処理は何も実行しない。

### 詳細

#### インタフェースと実装について

`yield return`をするメソッドは[公式ドキュメント](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/yield)から

>戻り値の型は、`IEnumerable`、`IEnumerable<T>`、`IEnumerator`、または `IEnumerator<T>` であることが必要です。

とあります。

今回のサンプルコードでは戻り値の型を`IEnumerable<int>`としました。
サンプルコードからコンパイラによって自動生成された`<YieldSample>d__1`クラスは次のインターフェースを実装しています。


```cs
IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
```
逆コンパイル結果を見てみると、`IEnumerable`で定義されるメソッド`GetEnumerator`では、自分自身を`this`として返してしまう処理があります。このように`IEnumerable`としても`IEnumerator`としても使えるように作られていることがわかります。


もし、戻り値の型を`IEnumerator<int>`とした場合は、


```cs
IEnumerator<int>, IEnumerator, IDisposable
```

となります。同時に、`IEnumerable`で定義される`GetEnumerator`メソッドの実装もなくなります。

ちなみに、戻り値の型を非ジェネリックにした場合、コンパイラが生成したクラスが実装するインタフェースのジェネリック部分が`object`になって自動生成されました。

最後まで説明しませんでしたが、密かに`IDisposable`も実装していますね。
これについては次節で説明します。

#### 自動生成されるDisposeメソッド

前節の通り、`yield return`を記述した場合、コンパイルが生成するクラスは全て`IDisposable`を実装していました。そして、上記のサンプルコードの逆コンパイル結果では`Dispose`メソッドが空になっていました。

実は、`yield return`を記述するメソッドで`try-finally`や`using`を使うと、`Dispose`の中にも処理が記述されます。具体的には`finally`句で記述した処理が実行されるようになります[^1]。

[^1]: ここではコンパイラが生成するコードの記載は省略しますが、気になる方はILSpy等を使って是非覗いてみてください。


これにより、次のようなプログラムを実行すると、

```cs
static void Main(string[] args)
{
    Console.WriteLine("Start");
    foreach (var x in YieldSample())
    {
        Console.WriteLine($"loop {x}");
        throw new Exception("error");
    }
    Console.WriteLine("End");
}

static IEnumerable<int> YieldSample()
{
    try
    {
        Console.WriteLine("yield return 1");
        yield return 1;
        Console.WriteLine("yield return 2");
        yield return 2;
    }
    finally
    {
        Console.WriteLine("dispose");
    }
}
```
次のような結果となります。

```
Start
yield return 1
loop 1

Unhandled Exception: System.Exception: error
   at ConsoleApp1.Program.Main(String[] args) in C:\Users\mrngsht\source\repos\ConsoleApp1\ConsoleApp1\Program.cs:line 14
dispose
続行するには何かキーを押してください . . .
```
`finally`句で記述した処理が、最後にに実行されていることがわかります。

なぜこのような動きになるかと言うと、

* `finally`句で記述した処理が、コンパイラが自動生成するクラスの`Dispose`の中で実行されるようになるから（本節冒頭で説明）
* `foreach`文は内部的に`try-finally`構文を作り出し、`finally`句で`Enumerator`のDisposeを行うようになっているから

です。

例えば、サンプルコードでの`foreach`文は、内部的に次のように展開されて、

```cs
var e = YieldSample().GetEnumerator();
try {
    while (e.MoveNext()) {
        var x = e.Current;
        Console.WriteLine($"loop {x}");
        throw new Exception("error");
    }
}
finally 
{
	e.Dispose();
}
```
`e.Dispose()`の中身の処理で`Console.WriteLine("dispose")`が呼ばれるというイメージです。

#### stateの数字の意味

サンプルコードの逆コンパイル結果に次のようなメソッドがありました。

```cs
private bool MoveNext()
{
	switch (<>1__state)
	{
	default:
		return false;
	case 0:
		<>1__state = -1;
		Console.WriteLine("yield return 1");
		<>2__current = 1;
		<>1__state = 1;
		return true;
	case 1:
		<>1__state = -1;
		Console.WriteLine("yield return 2");
		<>2__current = 2;
		<>1__state = 2;
		return true;
	case 2:
		<>1__state = -1;
		return false;
	}
}
```

この中で`<>1__state`というフィールドが使用されていますね。
文字通り、状態遷移機械の状態を保持する役割を担っています。

0, 1, 2の数字の場合は、見ての通り`yield return`を記述したメソッドの処理がどこまで進んだかを管理するために使用されていることがわかります。

では、`-1`はどうでしょう？`switch`文で分岐するときには、`default`に入って`return false`をするのみで何もさせないようになっていますね。
`case 2:`で`-1`を設定している方については状態遷移機会の終了状態を表していると考えられます。
`case 0:`や`case 1:`で 一時的に設定されているのは何でしょう？これはおそらく排他制御の役割をしているものだと思います。例えば、この`Enumerator`を他のスレッドも使って並行的に`MoveNext`メソッドを実行した場合に、複数スレッドで同じ部分の処理（例えば上の例での`Console.WriteLine`）を重複して行わないように制御しているものだと考えられます。

最後に`-2`というものも登場していました。

`Main`メソッドの隣に自分で記述した（がコンパイラに中身を書き換えられた）メソッドと、

```cs
private static IEnumerable<int> YieldSample()
{
	return new <YieldSample>d__1(-2);
}
```

コンパイラが生成したクラス`<YieldSample>d__1`で定義されるコンストラクタと`GetEnumerator`メソッドです。

```cs
public <YieldSample2>d__4(int <>1__state)
{
	this.<>1__state = <>1__state;
}

// 途中略

IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
	if (<>1__state == -2 && <>l__initialThreadId == nvironment.CurrentManagedThreadId)
	{
		<>1__state = 0;
		return this;
	}
	return new <YieldSample>d__1(0);
}
```
この部分は、[インタフェースと実装について](#インタフェースと実装について)の部分でも簡単に触れましたが、コンパイラに生成された1つのクラス`<YieldSample>d__1`が`IEnumerable`と`IEnumerator`の役割の両方を担おうとしていて、さらにこの`GetEnumerator`の最初の1度の呼び出しだけ自分自身のインスタンスをそのまま`IEnumerator`として流用させる目的があるものと思われます[^2]。
一度`GetEnumerator`を実行すると`<>1__state`の値が`0`となり、次回以降の`GetEnumerator`呼び出しでは新しいインスタンスを生成するようになります。

また、今回のサンプルコードとは別に`yield return`を記述するメソッドの戻り値を`IEnumerator`（ジェネリック版も含む）にした場合は、

```cs
private static IEnumerator<int> YieldSample()
{
	return new <YieldSample>d__1(0);
}
```
のようにコンストラクタで初期化する際の`<>1__state`は0となります。
この場合は、[インタフェースと実装について](#インタフェースと実装について)で記載したように、クラス`<YieldSample>d__1`は`IEnumerable`を実装しない（すなわち`GetEnumerator`メソッドを持たない）ため、このクラスから複数の`IEnumerator`が生成されることを考慮しなくてよいからだと考えられます。

[^2]: 同時に`GetEnumerator`を実行したスレッドが、コンストラクタを呼び出したスレッドと同じかどうかの確認も行っているようです。

## 最後に

最後まで読んでいただき、ありがとうございました！
また、長くなってしまった。。。
前回の記事[【図解】C#のasync/awaitの内部挙動を理解する](https://qiita.com/mrngsht/items/7d280c9f161f6d9855e2)と同じく、なんとなく動きはわかるけど、どのように処理されているかがいまいちピンんとこないものをILSpyで覗いてみました。個人的にも新しい発見が多くあり、いろいろ学びながら推測しながら書かせていただきました。

もし誤りやよくない記述、改善コメントがありましたら、是非コメントお願いいたします！