Tipi restituiti asincroni (C#)

I metodi asincroni possono avere i seguenti tipi restituiti:

Per altre informazioni sulla funzionalità dei metodi asincroni, vedere Programmazione asincrona con async e await (C#).

Esistono anche diversi altri tipi specifici per i carichi di lavoro di Windows:

Tipo restituito attività

I metodi asincroni che non contengono un'istruzione return o che contengono un'istruzione return che non restituisce un operando hanno in genere il tipo restituito Task. Questi metodi restituiscono void se eseguiti in modo sincrono. Se si usa un tipo restituito Task per un metodo asincrono, un metodo chiamante può usare un operatore await per sospendere il completamento del chiamante fino a quando il metodo asincrono chiamato non abbia terminato l'operazione.

Nell'esempio seguente, il metodo asincrono WaitAndApologizeAsync non contiene un'istruzione return, quindi il metodo restituisce un oggetto Task. La restituzione di un Task consente di attendere WaitAndApologizeAsync. Il tipo Task non include una proprietà Result perché non ha un valore restituito.

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.

L'attesa per WaitAndApologizeAsync viene impostata usando un'istruzione await invece di un'espressione await, in modo simile all'istruzione di chiamata per un metodo sincrono che restituisce void. L'applicazione di un operatore await in questo caso non produce un valore. Quando l'operando destro di un await è un Task<TResult>, l'espressione await produce un risultato di T. Quando l'operando destro di un await è un Task, await e il relativo operando sono un'istruzione.

È possibile separare la chiamata a WaitAndApologizeAsync dall'applicazione di un operatore await, come illustrato nel codice seguente. Tuttavia, si noti che un tipo Task non ha una proprietà Result e che quando viene applicato un operatore await a un tipo Task non viene prodotto alcun valore.

Il codice seguente separa la chiamata del metodo WaitAndApologizeAsync dall'attesa dell'attività restituita dal metodo.

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);

Tipo restituito<TResult> attività

Il tipo restituito Task<TResult> viene usato per un metodo asincrono che contiene un'istruzione return in cui il tipo dell'operando è TResult.

Nell'esempio seguente, il metodo GetLeisureHoursAsync contiene un'istruzione return che restituisce un valore intero. La dichiarazione del metodo deve quindi specificare un tipo restituito di Task<int>. Il metodo asincrono FromResult è un segnaposto per un'operazione che restituisce una 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

Quando GetLeisureHoursAsync viene chiamato da un'espressione await nel metodo ShowTodaysInfo, l'espressione recupera il valore intero (valore di leisureHours) archiviato nell'attività restituita dal metodo GetLeisureHours. Per altre informazioni sulle espressioni await, vedere await.

È possibile comprendere meglio come await recupera il risultato da un Task<T> separando la chiamata a GetLeisureHoursAsync dall'applicazione di await, come illustrato nel codice seguente. Una chiamata al metodo GetLeisureHoursAsync che non viene immediatamente attesa restituisce un tipo Task<int>, come ci si aspetterebbe dalla dichiarazione del metodo. Nell'esempio, l'attività viene assegnata alla variabile getLeisureHoursTask. Poiché getLeisureHoursTask è un Task<TResult>, contiene una proprietà Result di tipo TResult. In questo caso, TResult rappresenta un tipo Integer. Quando si applica await a getLeisureHoursTask, l'espressione await restituisce il contenuto della proprietà Result di getLeisureHoursTask. Il valore viene assegnato alla variabile ret.

Importante

La proprietà Result è una proprietà di blocco. Se si prova ad accedervi prima del completamento dell'attività, il thread attualmente attivo viene bloccato fino a quando l'attività non viene completata e il valore non è disponibile. Nella maggior parte dei casi, è consigliabile accedere al valore usando await invece di accedere direttamente alla proprietà.

Nell'esempio precedente, è stato recuperato il valore della proprietà Result per bloccare il thread principale in modo che il metodo Main possa stampare message nella console prima del termine dell'applicazione.

var getLeisureHoursTask = GetLeisureHoursAsync();

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

Console.WriteLine(message);

Tipo restituito void

Usare il tipo restituito void nei gestori eventi asincroni, che richiedono un tipo restituito void. Per i metodi diversi dai gestori eventi che non restituiscono un valore, è invece necessario restituire Task, perché non è possibile impostare l'attesa per un metodo asincrono che restituisce void. Qualsiasi chiamante di tale metodo deve continuare a essere completato senza attendere il completamento del metodo asincrono chiamato. Il chiamante deve essere indipendente da qualsiasi valore o eccezione generato dal metodo asincrono.

Il chiamante di un metodo asincrono che restituisce void non può acquisire le eccezioni generate dal metodo. Tali eccezioni non gestite potrebbero causare l'esito negativo dell'applicazione. Se un metodo che restituisce Task o Task<TResult> genera un'eccezione, l'eccezione viene archiviata nell'attività restituita. L'eccezione viene rigenerata quando l'attività è attesa. Verificare che qualsiasi metodo asincrono in grado di produrre un'eccezione abbia un tipo restituito Task o Task<TResult> e che le chiamate al metodo siano attese.

L'esempio seguente mostra il comportamento di un gestore dell'evento asincrono. Nel codice di esempio, un gestore dell'evento asincrono deve indicare al thread principale quando viene completato. Il thread principale può attendere quindi che un gestore dell'evento asincrono venga completato prima di uscire dal programma.

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.

Tipi restituiti asincroni generalizzati e ValueTask<TResult>

Un metodo asincrono può restituire qualsiasi tipo con un metodo accessibile GetAwaiter che restituisce un'istanza di un tipo awaiter. Inoltre, il tipo restituito dal metodo GetAwaiter deve avere l'attributo System.Runtime.CompilerServices.AsyncMethodBuilderAttribute. Per altre informazioni, vedere l'articolo sugli Attributi letti dal compilatore o la specifica C# per il Modello generatore di tipi di attività.

Questa funzionalità è il complemento per le espressioni awaitable, che descrive i requisiti per l'operando di await. I tipi restituiti asincroni generalizzati consentono al compilatore di generare async metodi che restituiscono tipi diversi. I tipi restituiti asincroni generalizzati abilitano i miglioramenti delle prestazioni nelle librerie .NET. Dato che Task e Task<TResult> sono tipi riferimento, l'allocazione della memoria in percorsi critici per le prestazioni, in particolare quando le allocazioni si verificano in cicli ravvicinati, può influire negativamente sulle prestazioni. Il supporto dei tipi restituiti generalizzati implica la possibilità di restituire un tipo valore leggero anziché un tipo riferimento per evitare allocazioni di memoria aggiuntive.

.NET offre la struttura System.Threading.Tasks.ValueTask<TResult> come implementazione leggera di un valore generalizzato per la restituzione di attività. L'esempio seguente usa la struttura ValueTask<TResult> per recuperare il valore di due tiri di dadi.

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

La scrittura di un tipo restituito asincrono generalizzato è uno scenario avanzato ed è destinata all'uso in ambienti specializzati. Prendere in considerazione l'uso dei tipi Task, Task<T> e ValueTask<T>, che coprono la maggior parte degli scenari per il codice asincrono.

In C# 10 e versioni successive, è possibile applicare l'attributo AsyncMethodBuilder a un metodo asincrono (anziché la dichiarazione del tipo restituito asincrono) per eseguire l'override del generatore per tale tipo. In genere, si applica questo attributo per usare un generatore diverso fornito nel runtime .NET.

Flussi asincroni con IAsyncEnumerable<T>

Un metodo asincrono può restituire un flusso asincrono, rappresentato da IAsyncEnumerable<T>. Un flusso asincrono consente di enumerare gli elementi letti da un flusso, quando gli elementi vengono generati in blocchi con chiamate asincrone ripetute. L'esempio seguente illustra un metodo asincrono che genera un flusso asincrono:

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();
    }
}

L'esempio precedente legge le righe da una stringa in modo asincrono. Dopo aver letto ogni riga, il codice enumera ogni parola nella stringa. I chiamanti enumererebbero ogni parola usando l'istruzione await foreach. Il metodo attende quando deve leggere in modo asincrono la riga successiva dalla stringa di origine.

Vedi anche