ほげほげー

C#メインで困ったことや面白く感じたことをメモしていきます。

C#におけるyieldの挙動

初めまして。これからC#について古いことから新しいことまで、
思いついたときにつらつらと書いていきますのでどうぞよろしくお願い致します。
わりとてきとーなので突っ込みどころがあればブコメなりコメなりで突っ込んで頂ければ幸いです。

今日はC#2.0からある古株だけどあまり知られていない*1yieldについて書いてみる。

yield

yieldを使うことで遅延評価の行われる反復処理を比較的容易に実装することができます。
が、単語の意味がよくわからないからか、なかなか現場で使われている所を目にしません。
これが利用できるとパフォーマンス面で有利になったり、可読性が上がったりと、
コーディングの幅が広がるので、使える場面があれば積極的に使っていきましょう。
とはいえ、LINQのおかげでだいぶ使う機会は減りましたけどね。

yieldの定義をMSDNで確認すると以下のようになっています。

yield (C# リファレンス)

ステートメントで yield のキーワードを使用すると、表示 get のメソッド、演算子、またはアクセサーが反復子であることを示します。 コレクションに対するカスタムのイテレーションを実行するために反復子を使用します。 次の例では yield のステートメントの 2 とおりの形式を次に示します。

http://msdn.microsoft.com/ja-jp/library/vstudio/9k7k7cf0.aspx

引用先に具体例もいくらか載っているので、
基本的なところはそちらを参照していただければと。
yieldがコンパイル時にどう置き換わっているのかも書かれているので大変面白い内容になっています。
MSDNつおい。*2

まずは具体例から

ここにyieldを使っていないコードがあるじゃろ?
( ^ω^)
⊃  ⊂

static void Main(string[] args)
{
	foreach (var item in GetList())
	{
		Console.WriteLine(item);
	}
}

static List<string> GetList()
{
	List<string> result = new List<string>();

	for (var i = 0; i < 10; i++)
	{
		/* 数行の処理 */
		result.Add(str);
	}
	
	return result;
}

これをこうして…
( ^ω^)
≡⊃⊂≡

こうじゃ
( ^ω^)
⊃  ⊂

static void Main(string[] args)
{
	foreach (var item in GetList())
	{
		Console.WriteLine(item);
	}
}

static IEnumerable<string> GetList()
{
	for (var i = 0; i < 10; i++)
	{
		/* 数行の処理 */
		yield return str;
	}
}

とまあ反復処理だけをするものを生成する場合にふわっとすっきりするので、
パフォーマンスとか考えなくてもこれだけでもわりと便利だったりします。

ネストが深かったりメソッド内の行数が多かったりするところだったり、
行数が多くてかつ yield return が複数出現したりなんて感じで使うと
そーとー可読性が落ちるのでお勧めしません。

とはいえそのようなコードはそーそー書かないですよね。

も少し具体例を。
たとえばusingをメソッドチェインみたいに使いたい!
なんてことがあった場合、遅延処理なのを利用してこんな実装も考えられます。

/// <summary>サンプル用拡張メソッド定義</summary>
public static class Extensions
{
    /// <summary>Usingメソッドを経由して生成されたIDisposableなインスタンスを利用後に解放する</summary>
    public static IEnumerable<U> Using<T, U>(this IEnumerable<T> self, Func<T, U> func)
        where U : IDisposable
    {
        foreach (var item in self)
        {
            using (var rtn = func(item))
            {
                yield return rtn;
            }
        }
    }

    /// <summary>ForEachな。</summary>
    public static void Do<T>(this IEnumerable<T> self, Action<T> action)
    {
        foreach (var item in self)
            action(item);
    }
}

class Program
{
    static void Main(string[] args)
    {
        var filePaths = new[] { @"C:\file.txt", @"D:\file.txt", @"E:\file.txt" };
        filePaths
            .Using(path => File.OpenRead(path))
            .Using(stream => new StreamReader(stream))
            .Do(reader => { /* StreamReaderを使った処理 */ });
    }
}

ぱっと思い付きで書いたのでアレな感じで実用的ではないですが、
メソッドチェインで書けるとなんかきもちいーですね。

じゃあyieldを使うと何がおこるのか、をまとめていきます。

yield returnを使うとなにがおこるのか

ではサクッと簡単な実装で確認していきませう

static void Main(string[] args)
{
	foreach (var item in GetList())
		Console.WriteLine(item);
}

static List<string> GetList()
{
	return List<string> list = new List<string>
	{
		"hoge",
		"huga",
		"poyo"
	};
}

List作って返してforeachでぐるぐるしてコンソールに吐いてるだけ。
出力結果は書くまでも無いけどこうなる。

hoge
huga
poyo

じゃあ次はyield returnで。
yield returnをするためには下記の条件に沿う必要があります。

get の反復子のメソッドやアクセサーの戻り値の型は IEnumerable、IEnumerable<T>、IEnumerator、または IEnumerator<T>のいずれかになります。

http://msdn.microsoft.com/ja-jp/library/vstudio/dscyy5s0(v=vs.110).aspx#BKMK_Technical

言い換えると

yield returnを使いたい場合は戻りの型を下記のいずれかにする必要があります。
IEnumerable
IEnumerable<T>
IEnumerator
IEnumerator<T>

となります。
ではそれに合わせて実装を変更しましょ。

static void Main(string[] args)
{
	foreach (var item in GetList())
		Console.WriteLine(item);
}

static IEnumerable<string> GetList()
{
	yield return "hoge";
	yield return "huga";
	yield return "poyo";
}

yield return を3回宣言して、それぞれで文字列を渡しています。
何が起こるかというと、foreachによって反復処理を行う際に、
反復が行われるたびに、値をひとつづつ渡す処理が行われています。

今回の場合、出力結果はListの出力結果と同様です。

hoge
huga
poyo

では少し処理を変更します。

static void Main(string[] args)
{
	foreach (var item in GetList())
		Console.WriteLine(item);
}

static IEnumerable<string> GetList()
{
	Console.Write("HOGE");
	yield return "hoge";

	Console.Write("HUGA");
	yield return "huga";

	Console.Write("POYO");
	yield return "poyo";
}

こうなると今度は

HOGEhoge
HUGAhuga
POYOpoyo

と、間に処理が挟まれます。
yield returnで値が返却された後、先に進んでいないことが確認できると思います。

さらにもう少しいじってみます。

static void Main(string[] args)
{
	var list = GetList();
	Console.WriteLine("Before ForEach");
	foreach (var item in list)
		Console.WriteLine(item);
}

static IEnumerable<string> GetList()
{
	Console.Write("HOGE");
	yield return "hoge";
	Console.Write("HUGA");
	yield return "huga";
	Console.Write("POYO");
	yield return "poyo";
}

IEnumerableとして値が返却されているので、
一見Before ForEachと出力される前に"HOGE"が出力されそうですが、
実際の出力結果は

Before ForEach
HOGEhoge
HUGAhuga
POYOpoyo

となります。
これより、yield return が利用されるメソッド内では、
利用されるタイミングまでメソッドの処理は全く実行されないことが確認できます。

さてこれで一通りyield returnの挙動は確認できました。
これを上手いこと使えばEnumeratorとイベント処理とを絡めてほげほげしたり
インスタンスをいちいち全部確保してたらメモリ食って食ってSHOGANAI処理とかのメモリ節約とか
LINQで何が起きてるのかをそこそこ理解できたりとか
C#が楽しくなってくるのではないでしょーか?

いじょう。
yield returnを書いたので、次はひじょーに関連が強い拡張メソッドをLINQを絡めて書いていけたらいーなー。
LINQ再入門てきな。

*1:と、現場で感じている

*2:ただし @neuecc さんにより以前指摘されていた項目(MSDN | http://msdn.microsoft.com/ja-jp/library/cc981895.aspx)(Twitter | https://twitter.com/neuecc/status/352069622922620928)は除く。このエントリで何がクソいのが多少わかるかもしれないしわからないかもしれない。