Risposte:
Non puoi avere metodi asincroni con ref
o out
parametri.
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);
}
Tuple
alternativa. Molto utile.
Tuple
. : P
Non è possibile avere ref
o out
parametri nei async
metodi (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.
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;
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
.
Una bella caratteristica dei out
parametri è che possono essere utilizzati per restituire dati anche quando una funzione genera un'eccezione. Penso che l'equivalente più vicino a farlo con un async
metodo sarebbe usare un nuovo oggetto per contenere i dati a cui async
possono 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 out
ha. 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 ref
e out
per l'uso con async
metodi e altri vari scenari in cui ref
e out
non 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.");
}
}
Adoro lo Try
schema. È 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 async
metodi in una quasi versione del Try
modello.
Questo assomiglia di più a un Try
metodo di sincronizzazione che restituisce solo un parametro tuple
anziché un bool
con un out
parametro, 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 true
su false
e non genera exception
.
Ricorda, lanciare un'eccezione in un
Try
metodo 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);
}
}
Possiamo usare anonymous
metodi 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 Try
modello ma imposta i out
parametri 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.
Che cosa succede se si utilizza semplicemente TPL
come progettato? Nessuna tupla. L'idea qui è che usiamo le eccezioni per reindirizzare ContinueWith
a 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 exception
errore 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 ContinueWith
che gestirà Task.Exception
nel suo blocco logico. Pulito, eh?
Ascolta, c'è un motivo per cui adoriamo il
Try
modello. È 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.
ContinueWith
chiamate abbia il risultato atteso? Secondo la mia comprensione, il secondo ContinueWith
verificherà il successo della prima continuazione, non il successo del compito originale.
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:
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);
}
Definire un metodo Try asincrono in questo modo:
public async Task<AsyncOut<bool, string>> TryReceiveAsync()
{
string message;
bool success;
// ...
return (success, message);
}
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.
La limitazione dei async
metodi che non accettano out
parametri si applica solo ai metodi asincroni generati dal compilatore, questi dichiarati con la async
parola chiave. Non si applica ai metodi asincroni realizzati a mano. In altre parole, è possibile creare Task
metodi di ritorno che accettano out
parametri. Ad esempio, supponiamo che abbiamo già un ParseIntAsync
metodo che lancia e che vogliamo creare un metodo che TryParseIntAsync
non 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 TaskCompletionSource
e 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 TaskCompletionSource
sarebbe ancora necessario per il out
parametro. È possibile che il out
parametro 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
, GetRawDataAsync
e FilterDataAsync
che sono chiamati in successione. Il out
parametro è completato al completamento del secondo metodo. Il GetDataAsync
metodo 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 data
prima di attendere rawDataLength
è importante in questo esempio semplificato, perché in caso di eccezione il out
parametro non verrà mai completato.
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):
}
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/
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