次の方法で共有


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

非同期メソッドには、次の戻り値の型を指定できます。

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

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

Windows ワークロードに固有のその他の種類も存在します。

Task の戻り値の型

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

次の例では、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 演算子のアプリケーションから分離できます。 ただし、Task には Result プロパティがなく、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

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

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

重要

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

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

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 属性が必要です。 詳細については、コンパイラ によって読み取られた 属性、または Task 型ビルダー パターンの C# 仕様に関する記事を参照してください。

この機能は、await のオペランドの要件を記述する待機可能な式を補完するものです。 一般化された非同期戻り値の型を使用すると、コンパイラは異なる型を返す async メソッドを生成できます。 一般化された非同期戻り値の型により、.NET ライブラリのパフォーマンスが向上しました。 TaskTask<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> 型を使用することを検討してください。非同期コードのほとんどのシナリオに対応します。

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 ステートメントを使用して各単語を列挙します。 メソッドは、ソース文字列から次の行を非同期的に読み取る必要があるときに待機します。

関連項目