C#のラムダ式でyieldっぽい何かをawaitで代用する方法

C#がインラインでyield書けないならawait使えばいいじゃない。と、偉い人は言いました。というわけで、こそこそっと開発がされているIxに、面白い機能が入りました(開発リポジトリ上だけなのでNuGetからダウンロードしても、まだ入ってません)。こんなのです。

var hoge = "あいうえお";

var seq = EnumerableEx.Create<int>(async Yield =>
{
    await Yield.Return(10);
    await Yield.Return(100);

    hoge = "ふがふが"; // インラインで書けるのでお外への副作用が可能

    await Yield.Return(1000);
});


foreach (var item in seq)
{
    Console.WriteLine(item); // 10, 100, 1000
}
Console.WriteLine(hoge); // ふがふが

そう、yield return(っぽい何か)がラムダ式で、メソッド外部に出すことなく書けてしまうのです!これは素敵ですね?い、いや、なんか何やってるのか分からなすぎて黒魔術怖いって雰囲気も漂ってますね!しかし面白いものは面白いので、実装見ましょう。

add types iyielder, iawaitable, and iawait; add support for creating ienumerable from action.ということで、まあ、ローカルで動かしたいんでコピペってきましょう。

public static class EnumerableEx
{
    public static IEnumerable<T> Create<T>(Action<IYielder<T>> create)
    {
        if (create == null) throw new ArgumentNullException("create");

        foreach (var x in new Yielder<T>(create))
        {
            yield return x;
        }
    }
}

public interface IYielder<in T>
{
    IAwaitable Return(T value);
    IAwaitable Break();
}

public interface IAwaitable
{
    IAwaiter GetAwaiter();
}

public interface IAwaiter : ICriticalNotifyCompletion
{
    bool IsCompleted { get; }
    void GetResult();
}

public class Yielder<T> : IYielder<T>, IAwaitable, IAwaiter, ICriticalNotifyCompletion
{
    private readonly Action<Yielder<T>> _create;
    private bool _running;
    private bool _hasValue;
    private T _value;
    private bool _stopped;
    private Action _continuation;

    public Yielder(Action<Yielder<T>> create)
    {
        _create = create;
    }

    public IAwaitable Return(T value)
    {
        _hasValue = true;
        _value = value;
        return this;
    }

    public IAwaitable Break()
    {
        _stopped = true;
        return this;
    }

    public Yielder<T> GetEnumerator()
    {
        return this;
    }

    public bool MoveNext()
    {
        if (!_running)
        {
            _running = true;
            _create(this);
        }
        else
        {
            _hasValue = false;
            _continuation();
        }

        return !_stopped && _hasValue;
    }

    public T Current
    {
        get
        {
            return _value;
        }
    }

    public IAwaiter GetAwaiter()
    {
        return this;
    }

    public bool IsCompleted
    {
        get { return false; }
    }

    public void GetResult() { }


    public void OnCompleted(Action continuation)
    {
        _continuation = continuation;
    }

    public void UnsafeOnCompleted(Action continuation)
    {
        _continuation = continuation;
    }
}

ほぅ、わけわからん?若干トリッキーなので、順を追っていきますか。asyncについて考える前に、まず、基本的なforeachのルール。実はIEnumerableを実装している必要はなくて、GetEnumeratorという名前のメソッドがあればいい。同様にMoveNextとCurrentというメソッドがあればIEnumerator扱いされる。なので、foreach (var x in new Yielder(create)) されているYielderはIEnumerableじゃないし、GetEnumeratorでreturn thisされていますが、YielderはIEnumeratorでもない。でも、foreachでグルグル回せている、というわけです。挙動は通常のforeachと同じで、MoveNext→Current、といった形です。

あと、インターフェイスが、IAwaitableとかいっぱい再定義されてて、ワケワカランのですけれど、そこまで意味あるわけじゃないです。これはラムダ式にYielderを渡すわけですが、そこで内部の諸々が呼べちゃうのはイクナイので隠ぺいする、程度の意味合いでしかないので、これを実装するのにインターフェイスの再定義が必要!というわけは全然ないです。

で、コアになるのはMoveNext。

public bool MoveNext()
{
    if (!_running)
    {
        _running = true;
        _create(this);
    }
    else
    {
        _hasValue = false;
        _continuation();
    }

    return !_stopped && _hasValue;
}

そもそもyield returnで生成されたメソッドが最初に実行されるのは、GetEnumeratorのタイミングではなく、GetEnumeratorされて最初のMoveNextが走った時、なので、ここが本体になっているのはセマンティクス的に問題なし。

!_runnningは初回実行時の意味で、ここで_create(this)、によってラムダ式で書いた本体が走ります。

var seq = EnumerableEx.Create<int>(async Yield =>
{
    await Yield.Return(10);
    // ↑のとこがまず実行され始める
    await Yield.Return(100);
    await Yield.Return(1000);
});

public IAwaitable Return(T value)
{
    _hasValue = true;
    _value = value;
    return this;
}

まずはメソッド実行なのでReturn。これは値をセットして回っているだけ。そしてIAwaitableを返し、await。ここで流れは別のところに行きます。

public bool IsCompleted
{
    get { return false; }
}

public void GetResult() { }


public void OnCompleted(Action continuation)
{
    _continuation = continuation;
}

public void UnsafeOnCompleted(Action continuation)
{
    _continuation = continuation;
}

まず完了しているかどうかの確認(IsCompleted)が走りますが、この場合は常にfalseで(そうしないと終了ということになってラムダ式のほうに戻ってこなくなっちゃう)。これによってUnsafeOnCompleted(ICriticalNotifyCompletionが実装されている場合はこっちが走る)でcontinuation(メソッド本体)が走る。で、「次回用」に変数保存して、MoveNext(create(this)したとこの位置)に戻ってくる。あとはMoveNextがtrueを返すのでCurrentで値取得して、それがyield returnされる。

二度目のMoveNextでは

public bool MoveNext()
{
    if (!_running)
    {
        _running = true;
        _create(this);
    }
    else
    {
        _hasValue = false;
        _continuation(); // ここが呼び出されて
    }

    return !_stopped && _hasValue;
}

var seq = EnumerableEx.Create<int>(async Yield =>
{
    await Yield.Return(10);
    // ここから再度走り出す
    await Yield.Return(100);
    await Yield.Return(1000);
});

といった感じになって、以下繰り返し。良く出来てますね!ていうか、asyncなのに非同期全く関係ないのが素敵。そう、asyncは別に非同期関係なく使えちゃうわけです。ここ大事なので繰り返しましょう。asyncは別に非同期関係なく使うことができます。

まとめ

async、フツーに使うのもそろそろ飽きてき頃だと思うので、弄って遊ぶのは大正義。実際に投下しだすかどうかは判断次第。あと、↑のはまだ大事な要素ができていないので絶対使いませんけれど。大事な要素はIDisposableであること。foreachで大事だと思ってるのはDisposeしてくれるとこ!だとも思っているので、それが実現できてないのはナイナー、と。

そういえばAsyncについてですが、3/30の土曜にRoom metro #15でHttpClient(非同期の塊!)について話すので、まだ残席ありますので良ければお越しくだしあー。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive