260 votes

Comment écrire une méthode asynchrone avec un paramètre de sortie ?

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

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

Comment puis-je faire cela dans GetDataTaskAsync ?

9voto

binki Points 479

Une belle caractéristique des paramètres out est qu'ils peuvent être utilisés pour renvoyer des données même lorsqu'une fonction lance une exception. Je pense que l'équivalent le plus proche pour faire cela avec une méthode async serait d'utiliser un nouvel objet pour stocker les données auxquelles la méthode async 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.

Remarquez que aucune de ces techniques n'aura le même type d'application de la part du compilateur que out. Autrement dit, le compilateur ne vous obligera pas à définir la valeur sur l'objet partagé ou à appeler un délégué passé en paramètre.

Voici un exemple d'implémentation utilisant un objet partagé pour imiter ref et out pour une utilisation avec des méthodes async et d'autres scénarios divers où ref et out ne sont pas disponibles:

class Ref
{
    // Champs plutôt qu'une propriété pour supporter le passage aux fonctions
    // acceptant `ref T` ou `out T`.
    public T Value;
}

async Task OperationExampleAsync(Ref successfulLoopsRef)
{
    var things = new[] { 0, 1, 2, };
    var i = 0;
    while (true)
    {
        // La quatrième itération va déclencher une exception, mais nous aurons quand même
        // communiqué des données au rappelant via successfulLoopsRef.
        things[i] += i;
        successfulLoopsRef.Value++;
        i++;
    }
}

async Task UsageExample()
{
    var successCounterRef = new Ref();
    // Remarquez qu'il n'a pas de sens d'accéder à successCounterRef
    // avant que OperationExampleAsync ne se termine (échec ou succès)
    // car il n'y a pas de synchronisation. Ici, je pense à passer
    // la variable comme "donner temporairement la propriété" de l'objet référencé à OperationExampleAsync. Décider des conventions relève de
    // vous et cela appartient à la documentation ^^.
    try
    {
        await OperationExampleAsync(successCounterRef);
    }
    finally
    {
        Console.WriteLine($"Had {successCounterRef.Value} successful loops.");
    }
}

9voto

Jerry Nixon - MSFT Points 12256

J'adore le motif Essayer. C'est un motif soigné.

if (double.TryParse(name, out var result))
{
    // gérer le succès
}
else
{
    // gérer l'erreur
}

Mais, c'est difficile avec async. Cela ne signifie pas que nous n'avons pas de réelles options. Voici les trois approches principales que vous pouvez envisager pour les méthodes async dans une version quasi du motif Essayer.

Approche 1 - sortir une structure

Cela ressemble le plus à une méthode sync Essayer retournant seulement un tuple au lieu d'un booléen avec un paramètre out, ce qui n'est pas autorisé en C#.

var result = await DoAsync(name);
if (result.Success)
{
    // gérer le succès
}
else
{
    // gérer l'erreur
}

Avec une méthode qui retourne true ou false et ne lance jamais d'exception.

N'oubliez pas, lancer une exception dans une méthode Essayer casse tout l'objectif du motif.

async Task<(bool Succès, StorageFile Fichier, Exception exception)> DoAsync(string fileName)
{
    try
    {
        var dossier = ApplicationData.Current.LocalCacheFolder;
        return (true, await dossier.GetFileAsync(fileName), null);
    }
    catch (Exception exception)
    {
        return (false, null, exception);
    }
}

Approche 2 - passer en paramètre des méthodes de rappel

Nous pouvons utiliser des méthodes anonymes pour définir des variables externes. C'est une syntaxe intelligente, bien que légèrement compliquée. En petites quantités, c'est bien.

var fichier = default(StorageFile);
var exception = default(Exception);
if (await DoAsync(name, x => fichier = x, x => exception = x))
{
    // gérer le succès
}
else
{
    // gérer l'échec
}

La méthode respecte les bases du motif Essayer mais défini les paramètres out des méthodes de rappel passées en paramètre. C'est fait de cette manière.

async Task DoAsync(string fileName, Action fichier, Action erreur)
{
    try
    {
        var dossier = ApplicationData.Current.LocalCacheFolder;
        fichier?.Invoke(await dossier.GetFileAsync(fileName));
        return true;
    }
    catch (Exception exception)
    {
        erreur?.Invoke(exception);
        return false;
    }
}

J'ai une question concernant les performances 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 utilisez simplement le TPL tel qu'il est conçu ? 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(tâche =>
{
    if (tâche.Exception != null)
    {
        // gérer l'échec
    }
    if (tâche.Result is StorageFile sf)
    {
        // gérer le succès
    }
});

Avec une méthode qui lance une exception en cas d'échec. C'est différent du retour d'un booléen. C'est une façon de communiquer avec le TPL.

async Task DoAsync(string fileName)
{
    var dossier = ApplicationData.Current.LocalCacheFolder;
    return await dossier.GetFileAsync(fileName);
}

Dans le code ci-dessus, si le fichier n'est pas trouvé, une exception est lancée. Cela invoquera l'échec de ContinueWith qui gérera Task.Exception dans son bloc logique. Pas mal, n'est-ce pas ?

Écoutez, il y a une raison pour laquelle nous aimons le motif Essayer. C'est tellement propre, lisible et, par conséquent, maintenable. En choisissant votre approche, surveillez la lisibilité. Rappelez-vous du prochain développeur qui sera là dans 6 mois et n'aura personne pour répondre à ses questions. Votre code peut être la seule documentation qu'un développeur aura jamais.

Bonne chance.

5voto

Jpsy Points 2811

Voici le code de la réponse de @dcastro modifié pour C# 7.0 avec tuples nommés et déconstruction de tuples, ce qui simplifie la notation :

public async void Method1()
{
    // Version 1, tuples nommés :
    // juste pour montrer comment ça fonctionne
    /*
    var tuple = await GetDataTaskAsync();
    int op = tuple.paramOp;
    int result = tuple.paramResult;
    */

    // Version 2, déconstruction de tuple :
    // beaucoup plus court, plus élégant
    (int op, int result) = await GetDataTaskAsync();
}

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

Pour plus de détails sur les nouveaux tuples nommés, littéraux de tuple et déconstructions de tuple, consultez : https://blogs.msdn.microsoft.com/dotnet/2017/03/09/new-features-in-c-7-0/

5voto

Theodor Zoulias Points 1088

La limitation des méthodes async de n'accepter pas les paramètres out s'applique uniquement aux méthodes async générées par le compilateur, celles déclarées avec le mot-clé async. Cela ne s'applique pas aux méthodes async faites à la main. En d'autres termes, il est possible de créer des méthodes renvoyant des Task acceptant des paramètres out. Par exemple, disons que nous avons déjà une méthode ParseIntAsync qui lance une exception, et nous voulons créer une méthode TryParseIntAsync qui ne lance pas d'exception. Nous pourrions l'implémenter de cette manière :

public static Task TryParseIntAsync(string s, out Task result)
{
    var tcs = new TaskCompletionSource();
    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);
}

L'utilisation de la TaskCompletionSource et la méthode ContinueWith est un peu maladroite, mais il n'y a pas d'autre option car nous ne pouvons pas utiliser le mot-clé await pratique à l'intérieur de cette méthode.

Exemple d'utilisation :

if (await TryParseIntAsync("-13", out var result))
{
    Console.WriteLine($"Résultat: {await result}");
}
else
{
    Console.WriteLine($"Échec de l'analyse");
}

Mise à jour : Si la logique async est trop complexe pour être exprimée sans await, elle peut être encapsulée à l'intérieur d'une déléguée anonyme asynchrone imbriquée. Un TaskCompletionSource serait tout de même nécessaire pour le paramètre out. Il est possible que le paramètre out soit complété avant l'achèvement de la tâche principale, comme dans l'exemple ci-dessous :

public static Task GetDataAsync(string url, out Task rawDataLength)
{
    var tcs = new TaskCompletionSource();
    rawDataLength = tcs.Task;
    return ((Func>)(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 paramètre out est complété à la fin de la deuxième méthode. La méthode GetDataAsync pourrait être utilisée comme ceci :

var data = await GetDataAsync("http://example.com", out var rawDataLength);
Console.WriteLine($"Données : {data}");
Console.WriteLine($"Longueur des données brutes : {await rawDataLength}");

Attendre les data avant d'attendre les rawDataLength est important dans cet exemple simplifié, car en cas d'exception le paramètre out ne sera jamais complété.

2voto

Paul Marangoni Points 77

Je pense que l'utilisation de ValueTuples comme ceci peut fonctionner. Vous devez d'abord ajouter le package 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):
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X