聖夜なので技術記事を捧げます。

大好評間近な「テメーらは説明が下手だ低能ども」シリーズ。今回はyieldについて。

C#とは書いてるが、C#知らんでも読めるように説明してみる。ヤドカリくらいの知能があればギリ読めるかもしれないな。いや無理かも。割と難解だ。メジロくらいのIQが必要かも。

◆return

ここに、どこの家庭にもあるような普通の関数があります。

※Main関数から開始される。
※Console.WriteLineは、標準出力に値を書きだす関数。

    private static void Main()
    {
        int hoge = 0;

        hoge = Hoge();

        Console.WriteLine(hoge);
    }

    private static int Hoge()
    {
        return 1;
    }

なんのことはない。Hoge()を呼ぶと、1を返してくれる。

それを変数 hoge に格納し、標準出力にWriteしている。つまりコンソールに「1」が表示されるだけのプログラムだ。

◆進化

じゃあ、関数 Hoge を

呼ぶたびに、前回より1大きい値を返す

関数にしてみる。

    private static void Main()
    {
        int hoge = 0;

        hoge = Hoge();
        Console.WriteLine(hoge);

        hoge = Hoge();
        Console.WriteLine(hoge);
    }

    private static int aaa = 0;

    private static int Hoge()
    {
        aaa = aaa + 1;
        return aaa;
    }
  • 関数の外に変数 aaa を定義
  • 関数を呼ぶごとに aaa の値に1を加算
  • aaa を return

これでよし。

◆問題発生

    private static void Main()
    {
        int hoge = 0;

        hoge = Hoge();
        Console.WriteLine(hoge);

        Fuga();

        hoge = Hoge();
        Console.WriteLine(hoge);
    }

    private static int aaa = 0;

    private static int Hoge()
    {
        aaa = aaa + 1;
        return aaa;
    }

    private static void Fuga()
    {
        aaa = 0;
    }

オイオイオイオーイ!フィールド変数ってどこからでも触れるじゃねぇか!ふざけんな!

Fuga 呼んだら aaa の中身 0 なったわ!裏切られた!びっくりしすぎて鼻血出たわ!金返せ!

◆どうしよう

こうなればよくね?

    private static void Main()
    {
        int hoge = 0;

        hoge = Hoge();
        Console.WriteLine(hoge);
        
        hoge = Hoge();
        Console.WriteLine(hoge);
    }

    private static int Hoge()
    {
        int aaa = 0;
        aaa = aaa + 1;
        return aaa;
    }

すげぇ。aaa が Fuga から触れなくなったじゃん。すげぇ。素直にすげぇ。

…いや、これ動いてなくね?呼ぶたびに aaa は 0 に初期化されてるし、何回呼んでも1しか返ってこなくね?

あれ…これ…

…あれ?

◆ ~114514年後~

こうなった。

    private static void Main()
    {
        var hoge = Hoge().GetEnumerator();

        hoge.MoveNext();
        Console.WriteLine(hoge.Current);
        hoge.MoveNext();
        Console.WriteLine(hoge.Current);
        hoge.MoveNext();
        Console.WriteLine(hoge.Current);
        hoge.MoveNext();
        Console.WriteLine(hoge.Current);
    }

    private static IEnumerable<int> Hoge()
    {
        int aaa = 0;
        while (true)
        {
            aaa = aaa + 1;
            yield return aaa;
        }
    }

※while(true)はつまり無限ループ

◆いや意味がわからねぇ

つまり、必要な道具は、以下のような性質を持つ関数だったんです。

returnした続きから処理を再開できる関数

良く分からんかもしれんから、Hoge() を砕いてみる。以下のようになる。

    private static IEnumerable<int> Hoge()
    {
        int aaa = 0;
        aaa = aaa + 1;
        yield return aaa;
        aaa = aaa + 1;
        yield return aaa;
        aaa = aaa + 1;
        yield return aaa;
        aaa = aaa + 1;
        yield return aaa;
        // 以下省略
    }

▼動き

  • Main が Hoge を呼ぶ。
  • Hoge 起床。
    • 変数 aaa を 0で初期化。
    • aaa に 1 を加算。
    • aaa を返却。
    • Hoge、休憩。
  • Main は Hoge から1を受け取る。
  • Main が Hoge を呼ぶ(にかいめ)。
  • Hoge、起きる。
    • さっきの続きから、つまり、yield return の次の行から処理を再開する。
    • 次の yield return をし、休憩。
  • Main は Hoge から2を受け取る。

ちょーびっくりだよね。

◆…そうかもしれないけど、Hogeの戻り値ってintでいいじゃねぇか。IEnumerable<int>ってなんだよ。怖いわ。お前言ってることが怖いわ。

落ち着けよ。声がでけぇよ。

ひとつずつ考えていってみよう。

◆型

returnした続きから処理を再開できる関数

があったとして、return可能なものに制限が必要となると思わんかね。どういうことかっていうと

一回目呼んだときの戻り値が int で、二回目呼んだときの戻り値が string。

だと、ちょっと辛いよな。returnの度に型のわからん値が返ってくる。処理に困る。だから、returnする値は常に同じ型でなければならないな。値じゃなくて、任意のクラスのインスタンスでもいいだろう。

そして、ピンク色でプルプルしてたかわいいぼくらの int だったが、ゴツくて黒ずんでる IEnumerable<int> に変貌してしまった。

何故だ。ぼくらはただ、呼ぶたびに int を返してくれる関数が欲しかっただけなのに。

◆繰り返し

IEnumerable は「Enumerableなインターフェース」を表現している。

※インターフェースっていうのは「このプロパティとかメソッドとか実装してくれろ」という縛り、ルールだ。つまり、インターフェースに指定されたプロパティやらメソッドを実装すれば「おっ!IEnumerable の実装を満たしているんですね!じゃあこれは IEnumerable として扱うことが出来ますね!お前だけは許さん!表に出ろ!」とC#くんに認めてもらえる。

Enumerableっていうのは「Enum」からもわかるように、列挙を意味している。列挙は列挙されているもんだから、一つ一つ順繰りに取ってくることが出来るだろ?

分かるか。大丈夫か。泣くな。難しく考えるな。

そいで、IEnumerable<int>は「intの列挙」を表現している。戻り値が int じゃなくて IEnumerable<int> になってしまった理由は、手近なところだと foreach に食わせたいからだな。foreach くんは IEnumerable でループをぶんまわすことができます。

そう。一個ずつ値を返す yield return は、結果を foreach に食わせることが出来る。むしろ何個値が取れるかなんて分からないから、ただの int じゃまずい。値とれなくて Exception とかなったらオメー会社クビになるでしょ?

◆ジェネリクス

IEnumerable<int>を認知しただろう。

じゃあ、IEnumerable<string> が存在していることがわかるな。

そして、自作クラスである「Unko」があったとして、IEnumerable<Unko> が存在していることが分かるだろう。そしてそんなの一つ一つ定義していられないから、もうひっくるめて IEnumerable<T> ってすることにします(説明放棄)。「T」は「任意の型」。

◆IEnumerator<T>

IEnumerable、ひいてはインターフェースというものは、オブジェクトの性質を表すものだ。つまり、yield return を使うと、戻り値が T じゃなくて「なんか知らんが繰り返し T がとれる不思議オブジェクト」になる。

IEnumerable<T>の定義を確認すると、どうやら GetEnumerator というメソッドしか持っていないことが分かる。

GetEnumerator の戻り値は IEnumerator<T> です。

なんだよそれ!Enumeratorってなんだよ!次から次へともうやめてくれ!お前にはうんざりなんだよ!いっそ殺してくれ!

と泣きわめいても無駄だ。ここは辺境のクソブログ。いくら叫ぼうが助けは来ない。

IEnumerator<T>の定義を確認すると…

っていうか、実コードがある人は IEnumerable<T> にカーソル乗ってる状態で F12 とか押せば定義に飛べるよ。

で、定義を見ると「Current」というT型のプロパティを持っているだけだ。雑魚かよ。ビビらせやがってクソが。

▼IEnumerator

と思いきや、実は IEnumerator<T> は更に IEnumerator を実装しているんですね。定義のページの「実装」って欄に「IEnumerator」がおわしますね。

もう一段潜ろう。

IEnumeratorの定義

どうやら MoveNextReset というメソッドの実装を要求するインターフェースなんだらしい。ふーん。

◆CurrentとMoveNextとReset

IEnumerator は親インターフェースとか持ってないし、MoveNext と Reset はどっちもオブジェクトを返却しない。そんなわけで僕らのジャーニーはどうやらここが終着点らしい。

▼Current

列挙があって、その列挙のうち今どこの値なのか。それは Current さ。

わんこ蕎麦で説明すると、IEnumerator<蕎麦>の Currentプロパティが今お椀に入っている蕎麦。「Current蕎麦」です。蕎麦の玉が列挙されていて、そのうちの「食える蕎麦」、現在蕎麦が Current です。なんだこの説明わかりやすすぎる。

▼MoveNext

列挙をひとつ進めてくれるメソッド。このメソッドを叩くたびに、つぎの yield return まで進んでくれるイメージ。

※いろいろ端折っている。言ってもしょうがねぇし。

戻り値は bool、すなわち真偽値だ。MoveNext を叩いて戻ってきた値が true であれば、次の要素がとれている。false なら、その時の Current が最後の要素だったという事だ。

MoveNextメソッドを呼ばない限り、Currentを何回参照しても同じ値が取れてくる。

▼Reset

列挙の最初の位置に戻る。それだけ。

◆揃いました。

説明終わり。じゃあもっかいさっきのコードを眺めてみろ。そして全ては初めから手中にあったのだと知って震えろ。

    private static void Main()
    {
        var hoge = Hoge().GetEnumerator();

        hoge.MoveNext();
        Console.WriteLine(hoge.Current);
        hoge.MoveNext();
        Console.WriteLine(hoge.Current);
        hoge.MoveNext();
        Console.WriteLine(hoge.Current);
        hoge.MoveNext();
        Console.WriteLine(hoge.Current);
    }

    private static IEnumerable<int> Hoge()
    {
        int aaa = 0;
        while (true)
        {
            aaa = aaa + 1;
            yield return aaa;
        }
    }

うん。なんかわかった?

あと、foreach あんじゃん。foreachって MoveNext とか一切触らなくていいし、魔法みたいじゃん。でもその実、泥臭く MoveNext をピチピチ叩いて泣きながら Current をペロペロしてるだけなんですよね。夢を壊すようなこと言って悪いけどなぁ。

いいか嬢ちゃん。サンタなんざいねぇんだよ。ちなみにトナカイもいない。

◆yield returnってなんだったの?

yieldはただのcontextual keywordっすよ(意味不明)

ひとたびコンパイラを通してしまえばテメーの大事な yield なんて跡形もなく消える。

yield returnを含むコードをコンパイルすると、IEnumerable<T>を実装したクラスを自動で生やしてくれるんですね。列挙を実装するときにクラスをいちいち作らなくていいし可読性も高まるという、うまあじ構文なんですよ。

うまあじ構文に過ぎないわけだから、yieldを使わずに等価なコードを書くことが出来る。さっきまで話していた IEnumerable<T> も IEnumerator<T> も、yieldで自動生成してもらうまでもなく自分の手で実装できる。だけどそれってダルすぎてもう来世がダルメシアンになりそうじゃん。intを繰り返し返却したかっただけなのに、いちいちクラス作らされるとか泣くじゃん。

だから、一定のルールのもとで IEnumerable<T> を簡単に生成できればいいねっつって yield が生まれたわけだ。

yield breakっつーのもあって、MoveNext の戻り値を強制的に false にしてしまったりする。こっから先は通さねぇぞという男気がすごいね(超適当)。

こんなんで説明OKすか。OKだろ。

あとこれは秘密なんだけども、yield return使ってるメソッドの戻り値はいきなり IEnumerator<T> でもいい。IEnumerable<T>を経る必要はない。でも foreachくんは IEnumerator<T> 食えないんで、さほど使い道ないかもね。

◆余談

IEnumerable<int> とか IEnumerable<string> の話をしたんだけど、例えば「一定の性質を持ったものを yield return したい」と思う事もあるかと思うんですよ。別のクラスなんだけど、同じように扱うことが出来るものたち。

IEnumerable でスパゲッティーがとれてきて、それを頭からかぶりたい。だからパスタを列挙したいんだけど、ナポリタンとかイカスミパスタとか、クラスが違う。そういう時は ISpaghetti というインターフェースを自作すればいいよね。そして IEnumerable<ISpaghetti> って返せばいいよね。

インターフェースとabstractはいずれ説明記事書くわ。いや、クラスとかインスタンスとかのほうが先か。

◆結論

そんな感じだよ。理解できたかね。

非同期版の IAsyncEnumerable<T> ってやつも最近出てきましてな。当然IAsyncEnumerator<T>もいるわけで、それらは各自どうにかしろ(丸投げ)。じゃあな!メリークリスマス!