Comment écrire une méthode asynchrone sans paramètre out?

176

Je veux écrire une méthode asynchrone avec un outparamètre, comme ceci:

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

Comment faire cela GetDataTaskAsync?

Jessé
la source

Réponses:

279

Vous ne pouvez pas avoir de méthodes asynchrones avec des paramètres refou 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);
}
décastro
la source
10
Loin d'être trop complexe, cela pourrait engendrer trop de problèmes. Jon Skeet l'a très bien expliqué ici stackoverflow.com/questions/20868103/…
MuiBienCarlota
3
Merci pour l' Tuplealternative. Très utile.
Luke Vo
19
c'est moche d'avoir Tuple. : P
tofutim
36
Je pense que Named Tuples en C # 7 sera la solution parfaite pour cela.
orad
3
@orad J'aime particulièrement ceci: Tâche asynchrone privée <(bool success, Job job, string message)> TryGetJobAsync (...)
J. Andrew Laughlin
51

Vous ne pouvez pas avoir de paramètres refou outdans les asyncmé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.

Alex
la source
2
Je préfère cette solution à la place en utilisant un Tuple. Plus propre!
MiBol
31

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;
jv_
la source
12

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ù myOpest définie si le résultat de la méthode est true. Sinon, vous vous en fichez myOp.

Scott Turner
la source
9

Une fonctionnalité intéressante des outparamè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 asyncméthode serait d'utiliser un nouvel objet pour contenir les données auxquelles la asyncmé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 outa. 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 refet outà utiliser avec des asyncméthodes et d'autres scénarios variés où refet outne 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.");
    }
}
binki
la source
6

J'adore le Trymotif. 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 asyncméthodes dans une quasi-version du Trymodèle.

Approche 1 - Sortie d'une structure

Cela ressemble le plus à une Tryméthode de synchronisation renvoyant uniquement un tupleau lieu d'un boolavec un outparamè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 truede falseet ne jette un exception.

N'oubliez pas que lancer une exception dans une Trymé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);
    }
}

Approche 2 - Passer dans les méthodes de rappel

Nous pouvons utiliser des anonymousmé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 Trymodèle mais définit les outparamè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.

Approche 3 - utiliser ContinueWith

Et si vous n'utilisez TPLque la version conçue? Pas de tuples. L'idée ici est que nous utilisons des exceptions pour rediriger ContinueWithvers 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 exceptioncas 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 ContinueWithqui gérera Task.Exceptiondans son bloc logique. Neat, hein?

Écoutez, il y a une raison pour laquelle nous aimons le Trymodè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.

Jerry Nixon
la source
1
Concernant la troisième approche, êtes-vous sûr que l'enchaînement des ContinueWithappels a le résultat escompté? Selon ma compréhension, le second ContinueWithvérifiera le succès de la première continuation, pas le succès de la tâche initiale.
Theodor Zoulias
1
Salut @TheodorZoulias, c'est un œil vif. Fixé.
Jerry Nixon
1
Lancer des exceptions pour le contrôle de flux est une énorme odeur de code pour moi - cela va améliorer vos performances.
Ian Kemp
Non, @IanKemp, c'est un concept assez ancien. Le compilateur a évolué.
Jerry Nixon
4

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:

  1. 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);
     }
  2. Définissez la méthode Try async comme ceci:

     public async Task<AsyncOut<bool, string>> TryReceiveAsync()
     {
         string message;
         bool success;
         // ...
         return (success, message);
     }
  3. 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.

Michael Gehling
la source
C'est une solution très intelligente!
Theodor Zoulias
2

La limitation des asyncméthodes n'acceptant pas de outparamètres s'applique uniquement aux méthodes asynchrones générées par le compilateur, celles-ci déclarées avec le asyncmot - clé. Cela ne s'applique pas aux méthodes asynchrones artisanales. En d'autres termes, il est possible de créer des Taskméthodes de retour acceptant des outparamètres. Par exemple, disons que nous avons déjà une ParseIntAsyncméthode qui lance, et que nous voulons créer une méthode qui ne lance TryParseIntAsyncpas. 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 TaskCompletionSourceet la ContinueWithméthode est un peu maladroit, mais il n'y a pas d' autre option , car nous ne pouvons pas utiliser le pratique awaitmot - 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 TaskCompletionSourceserait toujours nécessaire pour le outparamètre. Il est possible que le outparamè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, GetRawDataAsyncet FilterDataAsyncqui sont appelées successivement. Le outparamètre est complété à la fin de la deuxième méthode. La GetDataAsyncmé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 dataavant d'attendre le rawDataLengthest important dans cet exemple simplifié, car en cas d'exception, le outparamètre ne sera jamais complété.

Theodor Zoulias
la source
1
C'est une très bonne solution dans certains cas.
Jerry Nixon
1

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):
}
Paul Marangoni
la source
Vous n'avez pas besoin de NuGet si vous utilisez .net-4.7 ou netstandard-2.0.
binki
Hé, tu as raison! Je viens de désinstaller ce package NuGet et cela fonctionne toujours. Merci!
Paul Marangoni
1

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/

Jpsy
la source
-2

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
Payam Buroumand
la source
N'utilisez jamais .Result. C'est un anti-pattern. Merci!
Ben