非同期の戻り値の型 (C#)

非同期メソッドには、次の戻り値の型があります。

  • Task: 操作を実行し、値を返さない非同期メソッドの場合。
  • Task<TResult>: 値を返す非同期メソッドの場合。
  • void: イベント ハンドラーの場合。
  • アクセス可能な GetAwaiter メソッドを持つ任意の型です。 System.Runtime.CompilerServices.ICriticalNotifyCompletion メソッドによって返されるオブジェクトは、GetAwaiter インターフェイスを実装する必要があります。
  • "非同期ストリーム" を返す非同期メソッドの場合は IAsyncEnumerable<T>

非同期メソッドの詳細については、「Async および Await を使用した非同期プログラミング (C#)」を参照してください。

他にも、Windows ワークロードに固有の型がいくつか存在します。

Task の戻り値の型

return ステートメントを含まない非同期メソッド、またはオペランドを返さない return ステートメントを含む非同期メソッドは、通常は Task の戻り値の型を指定します。 こうしたメソッドは、同期的に実行するように作成されている場合に void を返します。 非同期メソッドに戻り値の型 Task を使用した場合、呼び出し元のメソッドは await 演算子を使って、呼び出された async のメソッドが終了するまで、呼び出し元の完了を中断します。

次の例では、WaitAndApologizeAsync メソッドに return ステートメントが含まれていないため、メソッドからは Task オブジェクトが返されます。 Task を返すことで、WaitAndApologizeAsync を待機できるようになります。 Task 型には戻り値がないため、Result プロパティを含みません。

public static async Task DisplayCurrentInfoAsync()
{
    await WaitAndApologizeAsync();

    Console.WriteLine($"Today is {DateTime.Now:D}");
    Console.WriteLine($"The current time is {DateTime.Now.TimeOfDay:t}");
    Console.WriteLine("The current temperature is 76 degrees.");
}

static async Task WaitAndApologizeAsync()
{
    await Task.Delay(2000);

    Console.WriteLine("Sorry for the delay...\n");
}
// Example output:
//    Sorry for the delay...
//
// Today is Monday, August 17, 2020
// The current time is 12:59:24.2183304
// The current temperature is 76 degrees.

WaitAndApologizeAsync を待機するには、void を返す同期メソッドを呼び出す場合と同様に、await 式でなく、await ステートメントを使用します。 この場合、await 演算子の適用によって値は生成されません。 await の右オペランドが Task<TResult> の場合、await 式によって、T の結果が生成されます。 await の右オペランドが、Task の場合、await とそのオペランドはステートメントです。

次のコードを見るとわかるように、WaitAndApologizeAsync の呼び出しを await 演算子の適用から分離することができます。 ただし TaskResult プロパティを持たないこと、また await 演算子が Task に適用されるときに値は生成されないことに注意します。

次のコードは、WaitAndApologizeAsync メソッドの呼び出しを、そのメソッドが返すタスクの待機から分離します。

Task waitAndApologizeTask = WaitAndApologizeAsync();

string output =
    $"Today is {DateTime.Now:D}\n" +
    $"The current time is {DateTime.Now.TimeOfDay:t}\n" +
    "The current temperature is 76 degrees.\n";

await waitAndApologizeTask;
Console.WriteLine(output);

Task<TResult> の戻り値の型

戻り値の型 Task<TResult> は、オペランドが TResult である return ステートメントを含む非同期メソッドに使用されます。

次の例の GetLeisureHoursAsync メソッドには、整数を返す return ステートメントが含まれています。 このメソッド宣言では、戻り値の型として Task<int> が指定される必要があります。 非同期メソッド FromResult は、DayOfWeek を返す操作に対するプレースホルダーです。

public static async Task ShowTodaysInfoAsync()
{
    string message =
        $"Today is {DateTime.Today:D}\n" +
        "Today's hours of leisure: " +
        $"{await GetLeisureHoursAsync()}";

    Console.WriteLine(message);
}

static async Task<int> GetLeisureHoursAsync()
{
    DayOfWeek today = await Task.FromResult(DateTime.Now.DayOfWeek);

    int leisureHours =
        today is DayOfWeek.Saturday || today is DayOfWeek.Sunday
        ? 16 : 5;

    return leisureHours;
}
// Example output:
//    Today is Wednesday, May 24, 2017
//    Today's hours of leisure: 5

GetLeisureHoursAsyncShowTodaysInfo メソッドの await 式の中から呼び出されると、await 式は GetLeisureHours メソッドから返されるタスクに格納されている整数値 (leisureHours の値) を取得します。 await 式の詳細については、「await」を参照してください。

次のコードが示すように、GetLeisureHoursAsync への呼び出しを await のアプリケーションから分離すると、awaitTask<T> から結果を取得する方法をよりよく理解できます。 メソッドの宣言から予想されるように、直ちに待機しない GetLeisureHoursAsync メソッドの呼び出しは、Task<int> を返します。 タスクは、この例の getLeisureHoursTask 変数に割り当てられます。 getLeisureHoursTaskTask<TResult> であるため、Result 型の TResult プロパティが含まれています。 この場合、TResult は整数型を表します。 awaitgetLeisureHoursTask に適用されると、getLeisureHoursTaskResult プロパティの内容が await 式の評価となります。 この値は ret 変数に割り当てられます。

重要

Result プロパティは Blocking プロパティです。 タスクが終了する前にアクセスしようとすると、現在アクティブなスレッドは、タスクが完了して値が使用可能になるまで、ブロックされます。 多くの場合、プロパティに直接アクセスする代わりに、await を使用して値にアクセスする必要があります。

前の例では、アプリケーションが終了する前に Main メソッドで message を出力できるように、Result プロパティの値を取得してメイン スレッドをブロックしました。

var getLeisureHoursTask = GetLeisureHoursAsync();

string message =
    $"Today is {DateTime.Today:D}\n" +
    "Today's hours of leisure: " +
    $"{await getLeisureHoursTask}";

Console.WriteLine(message);

Void の戻り値の型

void 戻り値の型は、void 戻り値の型が必要な非同期イベント ハンドラーで使用します。 値を返さないイベント ハンドラー以外のメソッドについては、Task を返す必要があります。これは、void を返す非同期メソッドを待機できないためです。 このようなメソッドの呼び出し元は、呼び出された非同期メソッドが完了するまで待機せずに、完了するまで続行する必要があります。 呼び出し元は、非同期メソッドによって生成される値や例外から独立している必要があります。

void を返す非同期メソッドの呼び出し元は、このメソッドがスローする例外をキャッチできません。 このような未処理の例外は、お使いのアプリケーションを失敗させる可能性があります。 Task または Task<TResult> を返すメソッドから例外がスローされた場合、例外は返されたタスクに格納されます。 タスクを待機しているときは、例外が再スローされます。 例外を生成する場合があるすべての非同期メソッドの戻り値の型は、Task または Task<TResult> で、メソッドに対するこの呼び出しは待機されます。

次の例では、非同期イベント ハンドラーの動作を示します。 コード例では、非同期イベント ハンドラーが終了したとき、その非同期イベント ハンドラーからメイン スレッドに通知が送られる必要があります。 このため、メイン スレッドでは、非同期イベント ハンドラーの終了を待ってから、プログラムを終了することができます。

public class NaiveButton
{
    public event EventHandler? Clicked;

    public void Click()
    {
        Console.WriteLine("Somebody has clicked a button. Let's raise the event...");
        Clicked?.Invoke(this, EventArgs.Empty);
        Console.WriteLine("All listeners are notified.");
    }
}

public class AsyncVoidExample
{
    static readonly TaskCompletionSource<bool> s_tcs = new TaskCompletionSource<bool>();

    public static async Task MultipleEventHandlersAsync()
    {
        Task<bool> secondHandlerFinished = s_tcs.Task;

        var button = new NaiveButton();

        button.Clicked += OnButtonClicked1;
        button.Clicked += OnButtonClicked2Async;
        button.Clicked += OnButtonClicked3;

        Console.WriteLine("Before button.Click() is called...");
        button.Click();
        Console.WriteLine("After button.Click() is called...");

        await secondHandlerFinished;
    }

    private static void OnButtonClicked1(object? sender, EventArgs e)
    {
        Console.WriteLine("   Handler 1 is starting...");
        Task.Delay(100).Wait();
        Console.WriteLine("   Handler 1 is done.");
    }

    private static async void OnButtonClicked2Async(object? sender, EventArgs e)
    {
        Console.WriteLine("   Handler 2 is starting...");
        Task.Delay(100).Wait();
        Console.WriteLine("   Handler 2 is about to go async...");
        await Task.Delay(500);
        Console.WriteLine("   Handler 2 is done.");
        s_tcs.SetResult(true);
    }

    private static void OnButtonClicked3(object? sender, EventArgs e)
    {
        Console.WriteLine("   Handler 3 is starting...");
        Task.Delay(100).Wait();
        Console.WriteLine("   Handler 3 is done.");
    }
}
// Example output:
//
// Before button.Click() is called...
// Somebody has clicked a button. Let's raise the event...
//    Handler 1 is starting...
//    Handler 1 is done.
//    Handler 2 is starting...
//    Handler 2 is about to go async...
//    Handler 3 is starting...
//    Handler 3 is done.
// All listeners are notified.
// After button.Click() is called...
//    Handler 2 is done.

一般化された非同期の戻り値の型と ValueTask<TResult>

非同期メソッドでは、"awaiter 型" のインスタンスを返すアクセス可能な GetAwaiter メソッドがある任意の型を返すことができます。 さらに、GetAwaiter メソッドから返される型には System.Runtime.CompilerServices.AsyncMethodBuilderAttribute 属性が必要です。 詳細については、コンパイラによって読み取られる属性に関する記事、または「タスクの種類のビルダー パターン」の C# 仕様を確認してください。

この機能は、await のオペランドの要件を記述する待機可能な式を補完するものです。 一般化された非同期の戻り値の型により、コンパイラではさまざまな型を返す async メソッドを生成できます。 一般化された非同期の戻り値の型により、.NET ライブラリのパフォーマンスの向上が可能になりました。 Task および Task<TResult> は参照型であるため、特に厳密なループ処理で割り当てが発生すると、パフォーマンスが重要なパスのメモリ割り当てが、パフォーマンスに悪影響を及ぼすことがあります。 一般化された戻り値の型のサポートにより、参照型ではなく、軽量な値の型を返すことができ、追加のメモリ割り当てを回避することが可能です。

.NET には、一般化されたタスク戻り値の軽量な実装として、System.Threading.Tasks.ValueTask<TResult> 構造が用意されています。 次の例では、ValueTask<TResult> 構造を使用して、2 つのさいころを転がしたときの値を取得します。

class Program
{
    static readonly Random s_rnd = new Random();

    static async Task Main() =>
        Console.WriteLine($"You rolled {await GetDiceRollAsync()}");

    static async ValueTask<int> GetDiceRollAsync()
    {
        Console.WriteLine("Shaking dice...");

        int roll1 = await RollAsync();
        int roll2 = await RollAsync();

        return roll1 + roll2;
    }

    static async ValueTask<int> RollAsync()
    {
        await Task.Delay(500);

        int diceRoll = s_rnd.Next(1, 7);
        return diceRoll;
    }
}
// Example output:
//    Shaking dice...
//    You rolled 8

一般化された非同期の戻り値の型は高度なシナリオで記述され、特殊な環境での使用を対象としています。 代わりに、非同期コードのほとんどのシナリオに対応する TaskTask<T>ValueTask<T> 型の使用を検討してください。

C# 10 以降では、AsyncMethodBuilder 属性を (非同期の戻り値の型宣言ではなく) 非同期メソッドに適用して、その型のビルダーをオーバーライドできます。 通常、この属性は、.NET ランタイムで提供される別のビルダーを使用するために適用します。

IAsyncEnumerable<T> を使用する非同期ストリーム

非同期メソッドから IAsyncEnumerable<T> で表される "非同期ストリーム" が返される場合があります。 非同期ストリームを使用すると、非同期呼び出しが繰り返される要素がチャンクで生成されるときに、ストリームから読み取られた項目を列挙できます。 次の例は、非同期ストリームを生成する非同期メソッドを示しています。

static async IAsyncEnumerable<string> ReadWordsFromStreamAsync()
{
    string data =
        @"This is a line of text.
              Here is the second line of text.
              And there is one more for good measure.
              Wait, that was the penultimate line.";

    using var readStream = new StringReader(data);

    string? line = await readStream.ReadLineAsync();
    while (line != null)
    {
        foreach (string word in line.Split(' ', StringSplitOptions.RemoveEmptyEntries))
        {
            yield return word;
        }

        line = await readStream.ReadLineAsync();
    }
}

前の例では、文字列から非同期に行を読み取ります。 各行が読み取られると、コードによって文字列内の各単語が列挙されます。 呼び出し元では、await foreach ステートメントを使用して各単語を列挙します。 このメソッドを使用すると、ソース文字列から次の行を非同期的に読み取る必要があるときに待機できます。

関連項目