Réponses:
Vous ne pouvez pas avoir de méthodes asynchrones avec des paramètres ref
ou out
.
Lucian Wischik explique pourquoi cela n'est pas possible sur ce fil MSDN: http://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-async-methods-cannot-have -ref-or-out-paramètres
Quant à savoir pourquoi les méthodes asynchrones ne prennent pas en charge les paramètres out-by-reference? (ou paramètres de référence?) C'est une limitation du CLR. Nous avons choisi d'implémenter les méthodes asynchrones de la même manière que les méthodes itératrices - c'est-à-dire via le compilateur transformant la méthode en un objet machine à états. Le CLR ne dispose d'aucun moyen sûr de stocker l'adresse d'un "paramètre de sortie" ou d'un "paramètre de référence" en tant que champ d'un objet. La seule façon d'avoir pris en charge les paramètres out-by-reference serait si la fonction asynchrone était effectuée par une réécriture CLR de bas niveau au lieu d'une réécriture par le compilateur. Nous avons examiné cette approche, et elle avait beaucoup à offrir, mais elle aurait finalement été si coûteuse qu'elle ne se serait jamais produite.
Une solution de contournement typique pour cette situation consiste à demander à la méthode async de renvoyer un Tuple à la place. Vous pouvez réécrire votre méthode comme telle:
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
alternative. Très utile.
Tuple
. : P
Vous ne pouvez pas avoir de paramètres ref
ou out
dans les async
méthodes (comme cela a déjà été noté).
Cela crie pour une certaine modélisation dans les données en mouvement:
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;
}
Vous gagnez la possibilité de réutiliser votre code plus facilement, et il est bien plus lisible que les variables ou les tuples.
La solution C # 7 + consiste à utiliser la syntaxe de tuple implicite.
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."
}));
}
return result utilise les noms de propriété définis par la signature de méthode. par exemple:
var foo = await TryLogin(request);
if (foo.IsSuccess)
return foo.Result;
Alex a fait un excellent point sur la lisibilité. De manière équivalente, une fonction est également une interface suffisante pour définir le ou les types renvoyés et vous obtenez également des noms de variables significatifs.
delegate void OpDelegate(int op);
Task<bool> GetDataTaskAsync(OpDelegate callback)
{
bool canGetData = true;
if (canGetData) callback(5);
return Task.FromResult(canGetData);
}
Les appelants fournissent un lambda (ou une fonction nommée) et intellisense aide en copiant le (s) nom (s) de variable du délégué.
int myOp;
bool result = await GetDataTaskAsync(op => myOp = op);
Cette approche particulière est comme une méthode "Try" où myOp
est définie si le résultat de la méthode est true
. Sinon, vous vous en fichez myOp
.
Une fonctionnalité intéressante des out
paramètres est qu'ils peuvent être utilisés pour renvoyer des données même lorsqu'une fonction lève une exception. Je pense que l'équivalent le plus proche de faire cela avec une async
méthode serait d'utiliser un nouvel objet pour contenir les données auxquelles la async
méthode et l'appelant peuvent se référer. Une autre façon serait de passer un délégué comme suggéré dans une autre réponse .
Notez qu'aucune de ces techniques n'aura le type d'application du compilateur qui out
a. Par exemple, le compilateur ne vous demandera pas de définir la valeur de l'objet partagé ou d'appeler un délégué passé.
Voici un exemple d'implémentation utilisant un objet partagé à imiter ref
et out
à utiliser avec des async
méthodes et d'autres scénarios variés où ref
et out
ne sont pas disponibles:
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.");
}
}
J'adore le Try
motif. C'est un modèle bien rangé.
if (double.TryParse(name, out var result))
{
// handle success
}
else
{
// handle error
}
Mais c'est difficile avec async
. Cela ne veut pas dire que nous n'avons pas de vraies options. Voici les trois approches de base que vous pouvez envisager pour les async
méthodes dans une quasi-version du Try
modèle.
Cela ressemble le plus à une Try
méthode de synchronisation renvoyant uniquement un tuple
au lieu d'un bool
avec un out
paramètre, ce qui, nous le savons tous, n'est pas autorisé en C #.
var result = await DoAsync(name);
if (result.Success)
{
// handle success
}
else
{
// handle error
}
Avec une méthode qui retourne true
de false
et ne jette un exception
.
N'oubliez pas que lancer une exception dans une
Try
méthode brise tout l'objectif du modèle.
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);
}
}
Nous pouvons utiliser des anonymous
méthodes pour définir des variables externes. C'est une syntaxe intelligente, bien que légèrement compliquée. À petites doses, ça va.
var file = default(StorageFile);
var exception = default(Exception);
if (await DoAsync(name, x => file = x, x => exception = x))
{
// handle success
}
else
{
// handle failure
}
La méthode obéit aux principes de base du Try
modèle mais définit les out
paramètres à transmettre dans les méthodes de rappel. C'est fait comme ça.
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;
}
}
Il y a une question dans mon esprit sur la performance ici. Mais le compilateur C # est tellement intelligent que je pense que vous êtes sûr de choisir cette option, presque à coup sûr.
Et si vous n'utilisez TPL
que la version conçue? Pas de tuples. L'idée ici est que nous utilisons des exceptions pour rediriger ContinueWith
vers deux chemins différents.
await DoAsync(name).ContinueWith(task =>
{
if (task.Exception != null)
{
// handle fail
}
if (task.Result is StorageFile sf)
{
// handle success
}
});
Avec une méthode qui lève un en exception
cas d'échec. C'est différent de retourner un fichier boolean
. C'est un moyen de communiquer avec le TPL
.
async Task<StorageFile> DoAsync(string fileName)
{
var folder = ApplicationData.Current.LocalCacheFolder;
return await folder.GetFileAsync(fileName);
}
Dans le code ci-dessus, si le fichier n'est pas trouvé, une exception est levée. Cela invoquera l'échec ContinueWith
qui gérera Task.Exception
dans son bloc logique. Neat, hein?
Écoutez, il y a une raison pour laquelle nous aimons le
Try
modèle. C'est fondamentalement si net et lisible et, par conséquent, maintenable. Au fur et à mesure que vous choisissez votre approche, veillez à la lisibilité. Rappelez-vous le prochain développeur qui dans 6 mois et ne vous a pas à répondre à des questions de clarification. Votre code peut être la seule documentation qu'un développeur aura jamais.
Bonne chance.
ContinueWith
appels a le résultat escompté? Selon ma compréhension, le second ContinueWith
vérifiera le succès de la première continuation, pas le succès de la tâche initiale.
J'ai eu le même problème que j'aime en utilisant le modèle Try-method-pattern qui semble fondamentalement incompatible avec le paradigme async-await-paradigm ...
Ce qui est important pour moi, c'est que je peux appeler la méthode Try dans une seule clause if et que je n'ai pas à prédéfinir les variables de sortie avant, mais que je peux le faire en ligne comme dans l'exemple suivant:
if (TryReceive(out string msg))
{
// use msg
}
J'ai donc proposé la solution suivante:
Définissez une structure d'assistance:
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);
}
Définissez la méthode Try async comme ceci:
public async Task<AsyncOut<bool, string>> TryReceiveAsync()
{
string message;
bool success;
// ...
return (success, message);
}
Appelez la méthode Try async comme ceci:
if ((await TryReceiveAsync()).Out(out string msg))
{
// use msg
}
Pour plusieurs paramètres de sortie, vous pouvez définir des structures supplémentaires (par exemple AsyncOut <T, OUT1, OUT2>) ou vous pouvez renvoyer un tuple.
La limitation des async
méthodes n'acceptant pas de out
paramètres s'applique uniquement aux méthodes asynchrones générées par le compilateur, celles-ci déclarées avec le async
mot - clé. Cela ne s'applique pas aux méthodes asynchrones artisanales. En d'autres termes, il est possible de créer des Task
méthodes de retour acceptant des out
paramètres. Par exemple, disons que nous avons déjà une ParseIntAsync
méthode qui lance, et que nous voulons créer une méthode qui ne lance TryParseIntAsync
pas. Nous pourrions l'implémenter comme ceci:
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);
}
Utilisation de la TaskCompletionSource
et la ContinueWith
méthode est un peu maladroit, mais il n'y a pas d' autre option , car nous ne pouvons pas utiliser le pratique await
mot - clé dans cette méthode.
Exemple d'utilisation:
if (await TryParseIntAsync("-13", out var result))
{
Console.WriteLine($"Result: {await result}");
}
else
{
Console.WriteLine($"Parse failed");
}
Mise à jour: si la logique asynchrone est trop complexe pour être exprimée sans await
, elle pourrait être encapsulée dans un délégué anonyme asynchrone imbriqué. Un TaskCompletionSource
serait toujours nécessaire pour le out
paramètre. Il est possible que le out
paramètre puisse être complété avant la fin de la tâche principale, comme dans l'exemple ci-dessous:
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);
}))();
}
Cet exemple suppose l'existence de trois méthodes asynchrones GetResponseAsync
, GetRawDataAsync
et FilterDataAsync
qui sont appelées successivement. Le out
paramètre est complété à la fin de la deuxième méthode. La GetDataAsync
méthode pourrait être utilisée comme ceci:
var data = await GetDataAsync("http://example.com", out var rawDataLength);
Console.WriteLine($"Data: {data}");
Console.WriteLine($"RawDataLength: {await rawDataLength}");
Attendre le data
avant d'attendre le rawDataLength
est important dans cet exemple simplifié, car en cas d'exception, le out
paramètre ne sera jamais complété.
Je pense que l'utilisation de ValueTuples comme celle-ci peut fonctionner. Vous devez d'abord ajouter le package ValueTuple NuGet:
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):
}
Voici le code de la réponse de @ dcastro modifiée pour C # 7.0 avec des tuples nommés et une déconstruction de tuple, qui rationalise la notation:
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);
}
Pour plus d'informations sur les nouveaux tuples nommés, les littéraux de tuple et les déconstructions de tuple, voir: https://blogs.msdn.microsoft.com/dotnet/2017/03/09/new-features-in-c-7-0/
Vous pouvez le faire en utilisant TPL (bibliothèque parallèle de tâches) au lieu d'utiliser directement le mot-clé await.
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