Come scrivere un metodo asincrono senza il parametro out?


176

Voglio scrivere un metodo asincrono con un outparametro, come questo:

public async void Method1()
{
    int op;
    int result = await GetDataTaskAsync(out op);
}

Come faccio a farlo GetDataTaskAsync?

Risposte:


279

Non puoi avere metodi asincroni con refo outparametri.

Lucian Wischik spiega perché questo non è possibile su questo thread MSDN: http://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-async-methods-cannot-have -ref-o-uscita-parametri

Perché i metodi asincroni non supportano i parametri fuori riferimento? (o parametri di riferimento?) Questa è una limitazione del CLR. Abbiamo scelto di implementare metodi asincroni in modo simile ai metodi iteratori, ovvero attraverso il compilatore che trasforma il metodo in un oggetto macchina-stato. Il CLR non ha un modo sicuro per memorizzare l'indirizzo di un "parametro esterno" o "parametro di riferimento" come campo di un oggetto. L'unico modo per supportare parametri fuori riferimento sarebbe se la funzione asincrona fosse eseguita da una riscrittura CLR di basso livello anziché da una riscrittura del compilatore. Abbiamo esaminato questo approccio, e aveva molto da fare, ma alla fine sarebbe stato così costoso che non sarebbe mai successo.

Una soluzione alternativa tipica per questa situazione è che il metodo asincrono restituisca invece una Tupla. È possibile riscrivere il metodo come tale:

public async Task Method1()
{
    var tuple = await GetDataTaskAsync();
    int op = tuple.Item1;
    int result = tuple.Item2;
}

public async Task<Tuple<int, int>> GetDataTaskAsync()
{
    //...
    return new Tuple<int, int>(1, 2);
}

10
Lungi dall'essere troppo complesso, questo potrebbe causare troppi problemi. Jon Skeet ha spiegato molto bene qui stackoverflow.com/questions/20868103/...
MuiBienCarlota

3
Grazie per l' Tuplealternativa. Molto utile.
Luke Vo,

19
è brutto Tuple. : P
tofutim,

36
Penso che Named Tuples in C # 7 sarà la soluzione perfetta per questo.
orad

3
@orad Mi piace in particolare: Task asincrono privato <(bool success, Job job, messaggio stringa)> TryGetJobAsync (...)
J. Andrew Laughlin,

51

Non è possibile avere refo outparametri nei asyncmetodi (come è stato già notato).

Questo urla per alcuni modelli nei dati che si spostano:

public class Data
{
    public int Op {get; set;}
    public int Result {get; set;}
}

public async void Method1()
{
    Data data = await GetDataTaskAsync();
    // use data.Op and data.Result from here on
}

public async Task<Data> GetDataTaskAsync()
{
    var returnValue = new Data();
    // Fill up returnValue
    return returnValue;
}

Ottieni la possibilità di riutilizzare il tuo codice più facilmente, inoltre è molto più leggibile di variabili o tuple.


2
Preferisco questa soluzione invece di usare una Tupla. Più pulito!
MiBol

31

La soluzione C # 7 + consiste nell'utilizzare la sintassi della tupla implicita.

    private async Task<(bool IsSuccess, IActionResult Result)> TryLogin(OpenIdConnectRequest request)
    { 
        return (true, BadRequest(new OpenIdErrorResponse
        {
            Error = OpenIdConnectConstants.Errors.AccessDenied,
            ErrorDescription = "Access token provided is not valid."
        }));
    }

il risultato di ritorno utilizza i nomi di proprietà definiti dalla firma del metodo. per esempio:

var foo = await TryLogin(request);
if (foo.IsSuccess)
     return foo.Result;

12

Alex ha sottolineato la leggibilità. Allo stesso modo, una funzione è anche abbastanza interfaccia per definire i tipi che vengono restituiti e si ottengono anche nomi di variabili significativi.

delegate void OpDelegate(int op);
Task<bool> GetDataTaskAsync(OpDelegate callback)
{
    bool canGetData = true;
    if (canGetData) callback(5);
    return Task.FromResult(canGetData);
}

I chiamanti forniscono una lambda (o una funzione denominata) e intellisense aiuta copiando i nomi delle variabili dal delegato.

int myOp;
bool result = await GetDataTaskAsync(op => myOp = op);

Questo particolare approccio è come un metodo "Try" dove myOpè impostato se il risultato del metodo è true. Altrimenti, non ti interessa myOp.


9

Una bella caratteristica dei outparametri è che possono essere utilizzati per restituire dati anche quando una funzione genera un'eccezione. Penso che l'equivalente più vicino a farlo con un asyncmetodo sarebbe usare un nuovo oggetto per contenere i dati a cui asyncpossono riferirsi sia il metodo che il chiamante. Un altro modo sarebbe passare un delegato come suggerito in un'altra risposta .

Nota che nessuna di queste tecniche avrà alcun tipo di applicazione dal compilatore che outha. Vale a dire, il compilatore non richiederà di impostare il valore sull'oggetto condiviso o di chiamare un delegato passato.

Ecco un'implementazione di esempio che utilizza un oggetto condiviso per imitare refe outper l'uso con asyncmetodi e altri vari scenari in cui refe outnon sono disponibili:

class Ref<T>
{
    // Field rather than a property to support passing to functions
    // accepting `ref T` or `out T`.
    public T Value;
}

async Task OperationExampleAsync(Ref<int> successfulLoopsRef)
{
    var things = new[] { 0, 1, 2, };
    var i = 0;
    while (true)
    {
        // Fourth iteration will throw an exception, but we will still have
        // communicated data back to the caller via successfulLoopsRef.
        things[i] += i;
        successfulLoopsRef.Value++;
        i++;
    }
}

async Task UsageExample()
{
    var successCounterRef = new Ref<int>();
    // Note that it does not make sense to access successCounterRef
    // until OperationExampleAsync completes (either fails or succeeds)
    // because there’s no synchronization. Here, I think of passing
    // the variable as “temporarily giving ownership” of the referenced
    // object to OperationExampleAsync. Deciding on conventions is up to
    // you and belongs in documentation ^^.
    try
    {
        await OperationExampleAsync(successCounterRef);
    }
    finally
    {
        Console.WriteLine($"Had {successCounterRef.Value} successful loops.");
    }
}

6

Adoro lo Tryschema. È un modello ordinato.

if (double.TryParse(name, out var result))
{
    // handle success
}
else
{
    // handle error
}

Ma è una sfida async. Ciò non significa che non abbiamo opzioni reali. Ecco i tre approcci di base che puoi prendere in considerazione per i asyncmetodi in una quasi versione del Trymodello.

Approccio 1: output di una struttura

Questo assomiglia di più a un Trymetodo di sincronizzazione che restituisce solo un parametro tupleanziché un boolcon un outparametro, che sappiamo tutti che non è consentito in C #.

var result = await DoAsync(name);
if (result.Success)
{
    // handle success
}
else
{
    // handle error
}

Con un metodo che restituisce truesu falsee non genera exception.

Ricorda, lanciare un'eccezione in un Trymetodo interrompe l'intero scopo del modello.

async Task<(bool Success, StorageFile File, Exception exception)> DoAsync(string fileName)
{
    try
    {
        var folder = ApplicationData.Current.LocalCacheFolder;
        return (true, await folder.GetFileAsync(fileName), null);
    }
    catch (Exception exception)
    {
        return (false, null, exception);
    }
}

Approccio 2: passa i metodi di callback

Possiamo usare anonymousmetodi per impostare variabili esterne. È una sintassi intelligente, sebbene leggermente complicata. A piccole dosi, va bene.

var file = default(StorageFile);
var exception = default(Exception);
if (await DoAsync(name, x => file = x, x => exception = x))
{
    // handle success
}
else
{
    // handle failure
}

Il metodo obbedisce alle basi del Trymodello ma imposta i outparametri da passare nei metodi di callback. È fatto così.

async Task<bool> DoAsync(string fileName, Action<StorageFile> file, Action<Exception> error)
{
    try
    {
        var folder = ApplicationData.Current.LocalCacheFolder;
        file?.Invoke(await folder.GetFileAsync(fileName));
        return true;
    }
    catch (Exception exception)
    {
        error?.Invoke(exception);
        return false;
    }
}

C'è una domanda nella mia mente sulle prestazioni qui. Ma il compilatore C # è così intelligente, che penso che tu sia sicuro di scegliere questa opzione, quasi sicuramente.

Approccio 3: utilizzare Continua con

Che cosa succede se si utilizza semplicemente TPLcome progettato? Nessuna tupla. L'idea qui è che usiamo le eccezioni per reindirizzare ContinueWitha due percorsi diversi.

await DoAsync(name).ContinueWith(task =>
{
    if (task.Exception != null)
    {
        // handle fail
    }
    if (task.Result is StorageFile sf)
    {
        // handle success
    }
});

Con un metodo che genera un exceptionerrore in caso di errore. È diverso dal restituire a boolean. È un modo per comunicare con TPL.

async Task<StorageFile> DoAsync(string fileName)
{
    var folder = ApplicationData.Current.LocalCacheFolder;
    return await folder.GetFileAsync(fileName);
}

Nel codice sopra, se il file non viene trovato, viene generata un'eccezione. Questo invocherà l'errore ContinueWithche gestirà Task.Exceptionnel suo blocco logico. Pulito, eh?

Ascolta, c'è un motivo per cui adoriamo il Trymodello. È fondamentalmente così pulito e leggibile e, di conseguenza, mantenibile. Mentre scegli il tuo approccio, watchdog per la leggibilità. Ricorda il prossimo sviluppatore che tra 6 mesi e non ha bisogno di rispondere alle domande di chiarimento. Il tuo codice può essere l'unica documentazione che uno sviluppatore avrà mai.

Buona fortuna.


1
A proposito del terzo approccio, sei sicuro che il concatenamento delle ContinueWithchiamate abbia il risultato atteso? Secondo la mia comprensione, il secondo ContinueWithverificherà il successo della prima continuazione, non il successo del compito originale.
Theodor Zoulias,

1
Saluti @TheodorZoulias, è un occhio acuto. Fisso.
Jerry Nixon,

1
Generare eccezioni per il controllo del flusso è un odore enorme di codice per me - aumenterà le tue prestazioni.
Ian Kemp,

No, @IanKemp, è un concetto piuttosto vecchio. Il compilatore si è evoluto.
Jerry Nixon,

4

Ho avuto lo stesso problema che mi piace usare il modello del metodo Try che sostanzialmente sembra incompatibile con il paradigma async-waitit ...

Per me è importante poter chiamare il metodo Try all'interno di una singola clausola if e non dover pre-definire le variabili out prima, ma posso farlo in linea come nell'esempio seguente:

if (TryReceive(out string msg))
{
    // use msg
}

Quindi ho pensato alla seguente soluzione:

  1. Definire una struttura di supporto:

     public struct AsyncOut<T, OUT>
     {
         private readonly T returnValue;
         private readonly OUT result;
    
         public AsyncOut(T returnValue, OUT result)
         {
             this.returnValue = returnValue;
             this.result = result;
         }
    
         public T Out(out OUT result)
         {
             result = this.result;
             return returnValue;
         }
    
         public T ReturnValue => returnValue;
    
         public static implicit operator AsyncOut<T, OUT>((T returnValue ,OUT result) tuple) => 
             new AsyncOut<T, OUT>(tuple.returnValue, tuple.result);
     }
  2. Definire un metodo Try asincrono in questo modo:

     public async Task<AsyncOut<bool, string>> TryReceiveAsync()
     {
         string message;
         bool success;
         // ...
         return (success, message);
     }
  3. Chiama il metodo Try asincrono in questo modo:

     if ((await TryReceiveAsync()).Out(out string msg))
     {
         // use msg
     }

Per più parametri di uscita è possibile definire ulteriori strutture (ad es. AsyncOut <T, OUT1, OUT2>) oppure è possibile restituire una tupla.


Questa è una soluzione molto intelligente!
Theodor Zoulias

2

La limitazione dei asyncmetodi che non accettano outparametri si applica solo ai metodi asincroni generati dal compilatore, questi dichiarati con la asyncparola chiave. Non si applica ai metodi asincroni realizzati a mano. In altre parole, è possibile creare Taskmetodi di ritorno che accettano outparametri. Ad esempio, supponiamo che abbiamo già un ParseIntAsyncmetodo che lancia e che vogliamo creare un metodo che TryParseIntAsyncnon getti. Potremmo implementarlo in questo modo:

public static Task<bool> TryParseIntAsync(string s, out Task<int> result)
{
    var tcs = new TaskCompletionSource<int>();
    result = tcs.Task;
    return ParseIntAsync(s).ContinueWith(t =>
    {
        if (t.IsFaulted)
        {
            tcs.SetException(t.Exception.InnerException);
            return false;
        }
        tcs.SetResult(t.Result);
        return true;
    }, default, TaskContinuationOptions.None, TaskScheduler.Default);
}

Utilizzando il TaskCompletionSourcee ilContinueWith metodo è un po 'imbarazzante, ma non c'è altra opzione in quanto non possiamo usare il comodoawait parola chiave all'interno di questo metodo.

Esempio di utilizzo:

if (await TryParseIntAsync("-13", out var result))
{
    Console.WriteLine($"Result: {await result}");
}
else
{
    Console.WriteLine($"Parse failed");
}

Aggiornamento: se la logica asincrona è troppo complessa per essere espressa senza await, potrebbe essere incapsulata all'interno di un delegato anonimo asincrono nidificato. A TaskCompletionSourcesarebbe ancora necessario per il outparametro. È possibile che il outparametro possa essere completato prima del completamento dell'attività principale, come nell'esempio seguente:

public static Task<string> GetDataAsync(string url, out Task<int> rawDataLength)
{
    var tcs = new TaskCompletionSource<int>();
    rawDataLength = tcs.Task;
    return ((Func<Task<string>>)(async () =>
    {
        var response = await GetResponseAsync(url);
        var rawData = await GetRawDataAsync(response);
        tcs.SetResult(rawData.Length);
        return await FilterDataAsync(rawData);
    }))();
}

Questo esempio presuppone l'esistenza di tre metodi asincroni GetResponseAsync, GetRawDataAsynce FilterDataAsyncche sono chiamati in successione. Il outparametro è completato al completamento del secondo metodo. Il GetDataAsyncmetodo potrebbe essere utilizzato in questo modo:

var data = await GetDataAsync("http://example.com", out var rawDataLength);
Console.WriteLine($"Data: {data}");
Console.WriteLine($"RawDataLength: {await rawDataLength}");

Attendere il dataprima di attendere rawDataLengthè importante in questo esempio semplificato, perché in caso di eccezione il outparametro non verrà mai completato.


1
Questa è una soluzione molto bella per alcuni casi.
Jerry Nixon,

1

Penso che usare ValueTuples in questo modo possa funzionare. Prima devi aggiungere il pacchetto NuGet ValueTuple:

public async void Method1()
{
    (int op, int result) tuple = await GetDataTaskAsync();
    int op = tuple.op;
    int result = tuple.result;
}

public async Task<(int op, int result)> GetDataTaskAsync()
{
    int x = 5;
    int y = 10;
    return (op: x, result: y):
}

NuGet non è necessario se si utilizza .net-4.7 o netstandard-2.0.
binki,

Ehi, hai ragione! Ho appena disinstallato quel pacchetto NuGet e funziona ancora. Grazie!
Paul Marangoni,

1

Ecco il codice della risposta di @ dcastro modificato per C # 7.0 con tuple nominate e decostruzione delle tuple, che semplifica la notazione:

public async void Method1()
{
    // Version 1, named tuples:
    // just to show how it works
    /*
    var tuple = await GetDataTaskAsync();
    int op = tuple.paramOp;
    int result = tuple.paramResult;
    */

    // Version 2, tuple deconstruction:
    // much shorter, most elegant
    (int op, int result) = await GetDataTaskAsync();
}

public async Task<(int paramOp, int paramResult)> GetDataTaskAsync()
{
    //...
    return (1, 2);
}

Per dettagli sulle nuove tuple, letterali di tuple e decostruzioni di tuple, consultare: https://blogs.msdn.microsoft.com/dotnet/2017/03/09/new-features-in-c-7-0/


-2

Puoi farlo usando TPL (task parallel library) invece di usare direttamente la parola chiave waitit.

private bool CheckInCategory(int? id, out Category category)
    {
        if (id == null || id == 0)
            category = null;
        else
            category = Task.Run(async () => await _context.Categories.FindAsync(id ?? 0)).Result;

        return category != null;
    }

if(!CheckInCategory(int? id, out var category)) return error

Non usare mai. È un anti-schema. Grazie!
Ben
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.