かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

await可能なクラスを作ってみよう

C#の新機能というにはもう古いかもしれないasync/awaitですが…こいつはTaskに対して使うものだ…!という理解でも、ほぼほぼ使うぶんには問題ありません。

でも、実際はTaskやTaskじゃなくてもawaitできるという作りになってるのでその気になればなんだってawait出来るようになる・・・!実用性があるのというのになると、ちょっと実例を挙げるのは難しいですが…。ということで、ここでは、なんとなく雰囲気をつかむためにawaitするとHello worldを返すものを作ってみようと思います。

軽い前提知識

await可能なクラスには以下のメソッドがあること

  • T GetAwaiter()

GetAwaiterメソッドの返すクラスには以下のプロパティとメソッドがあること

  • bool IsCompleted { get; }
  • void OnCompleted(Action continuation)
  • T GetResult()

深く知りたいという人はMSDNマガジンの以下の記事をおすすめしておきます。

await可能なクラスを作るための下準備

コンソールアプリケーションにRunというメソッドを作って、そのメソッドをMainメソッドから呼び出します。Runメソッドにはasyncを付けて中でawaitが使えるようにしましょう。Runメソッドの呼び出しのあとには、一応Console.ReadLine()でアプリがストンと落ちないようにしておきます。

using System;

namespace ConsoleApplication4
{
    class Program
    {
        static void Main(string[] args)
        {
            Run();
            Console.ReadLine();
        }

        private static async void Run()
        {
        }
    }
}

このRunメソッド内に処理を書いていきます。まずawaitするためのクラスを作ります。クラス名はHello worldらしくHelloWorldAwaitableにしました。GetAwaiterメソッドを作ってHelloWorldAwaiter構造体を返すようにしてます。

class HelloWorldAwaitable
{
    public HelloWorldAwaiter GetAwaiter()
    {
        return new HelloWorldAwaiter();
    }
}

struct HelloWorldAwaiter
{
}

さて、Runメソッド内でawaitしてみます。

private static async void Run()
{
    var result = await new HelloWorldAwaitable();
}

この状態でビルドすると以下のようなコンパイルエラーが出ます。

error CS0117: 'ConsoleApplication4.HelloWorldAwaiter' に 'IsCompleted' の定義がありません。

最初に書いたawait可能な条件を満たしてないということですねIsCompletedプロパティを以下のように追加してみました。今回は、常にawait呼び出した直後では処理が終わってないということにしたいのでfalseを返すだけの実装にしてます。

struct HelloWorldAwaiter
{
    public bool IsCompleted { get { return false; } }
}

これでビルドするとコンパイルエラーが以下のように変わります。

error CS4027: 'ConsoleApplication4.HelloWorldAwaiter' does not implement 'System.Runtime.CompilerServices.INotifyCompletion'

INotifyCompletionという馴染みのないインターフェースを実装していないといわれました。

こいつは、OnCompletedメソッドを持ってるだけのシンプルなインターフェースです。確か必須条件の中に、そんなメソッドがありましたね。ということで実装します。*1

struct HelloWorldAwaiter : INotifyCompletion
{
    public bool IsCompleted { get { return false; } }

    public void OnCompleted(Action continuation)
    {
    }
}

この状態でコンパイルをすると、今度は以下のようにコンパイルエラーのメッセージが変わります。

error CS0117: 'ConsoleApplication4.HelloWorldAwaiter' に 'GetResult' の定義がありません。

なんかawaitできるっぽいけど最後に結果をとるのがうまくいってないみたいですね。ということで、GetResultを以下のように実装します。今回は、Hello worldを返すだけにしてます。

struct HelloWorldAwaiter : INotifyCompletion
{
    public bool IsCompleted { get { return false; } }

    public void OnCompleted(Action continuation)
    {
    }

    public string GetResult() { return "Hello world"; }
}

これでコンパイルエラーが消えるのでRunメソッドの中身の続きを書いていきます。awaitした結果を標準出力に出力します。

private static async void Run()
{
    var result = await new HelloWorldAwaitable();
    // awaitした結果を出力!
    Console.WriteLine(result);
}

これで実行すると…何も出ません。awaitを呼び出した後の処理を適切に行おうと思ったらHelloWorldAwaiterのOnCompletedメソッドの中身を実装する必要があります。OnCompletedメソッドの引数にわたってきているActionに、awaitした続きの処理が入っているようなイメージなので、こいつを呼び出してやるとOKです。ということで、OnCompletedメソッドの中身は以下のようになります。

struct HelloWorldAwaiter : INotifyCompletion
{
    public bool IsCompleted { get { return false; } }

    public void OnCompleted(Action continuation)
    {
        // awaitの続きを頼む
        continuation();
    }

    public string GetResult() { return "Hello world"; }
}

これでプログラムを実行すると以下のような結果になります!

Hello world

ということでawait可能なクラスを作るHello worldプログラムでした。気になる箇所にブレークポイントをはって、ステップ実行してみると面白いかも?

*1:OnCompletedメソッドがあればいいのかと思ったら、このインターフェース実装してないとコンパイルエラーになった。う~ん