Como escrever um método assíncrono sem o parâmetro?

176

Eu quero escrever um método assíncrono com um outparâmetro, como este:

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

Como faço isso GetDataTaskAsync?

Jessé
fonte

Respostas:

279

Você não pode ter métodos assíncronos com refou outparâmetros.

Lucian Wischik explica por que isso não é possível neste segmento do MSDN: http://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-async-methods-cannot-have -ref-ou-out-parâmetros

Por que os métodos assíncronos não oferecem suporte a parâmetros por referência? (ou parâmetros de referência?) Essa é uma limitação do CLR. Optamos por implementar métodos assíncronos de maneira semelhante aos métodos do iterador - ou seja, através do compilador transformando o método em um objeto de máquina de estado. O CLR não possui uma maneira segura de armazenar o endereço de um "parâmetro de saída" ou "parâmetro de referência" como um campo de um objeto. A única maneira de oferecer suporte a parâmetros fora de referência seria se o recurso assíncrono fosse executado por uma reescrita de baixo nível do CLR em vez de uma reescrita do compilador. Examinamos essa abordagem e ela tinha muito a oferecer, mas acabaria sendo tão caro que nunca teria acontecido.

Uma solução típica para essa situação é fazer com que o método assíncrono retorne uma Tupla. Você pode reescrever seu método como tal:

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);
}
dcastro
fonte
10
Longe de ser muito complexo, isso poderia produzir muitos problemas. Jon Skeet explicou muito bem aqui stackoverflow.com/questions/20868103/…
MuiBienCarlota
3
Obrigado pela Tuplealternativa. Muito útil.
Luke Vo
19
é feio ter Tuple. : P
tofutim 07/11
36
Eu acho que Tuplas nomeadas em C # 7 serão a solução perfeita para isso.
Orad 17/03
3
@orad Eu gosto especialmente disso: Tarefa assíncrona privada <(sucesso bool, Job job, mensagem de seqüência de caracteres)> TryGetJobAsync (...)
J. Andrew Laughlin #
51

Você não pode ter refou outparâmetros nos asyncmétodos (como já foi observado).

Isso exige alguma modelagem nos dados que se deslocam:

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;
}

Você ganha a capacidade de reutilizar seu código com mais facilidade, além de ser muito mais legível do que variáveis ​​ou tuplas.

Alex
fonte
2
Prefiro essa solução ao invés de usar uma tupla. Mais limpo!
MiBol
31

A solução C # 7 + é usar sintaxe implícita de tupla.

    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."
        }));
    }

O resultado de retorno utiliza os nomes de propriedades definidas pela assinatura do método. por exemplo:

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

Alex fez um grande ponto na legibilidade. Da mesma forma, uma função também é interface suficiente para definir o (s) tipo (s) que está sendo retornado e você também obtém nomes significativos de variáveis.

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

Os chamadores fornecem um lambda (ou uma função nomeada) e o intellisense ajuda copiando o (s) nome (s) da variável do delegado.

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

Essa abordagem específica é como um método "Try", onde myOpé definido se o resultado do método for true. Caso contrário, você não se importa myOp.

Scott Turner
fonte
9

Uma característica interessante dos outparâmetros é que eles podem ser usados ​​para retornar dados, mesmo quando uma função lança uma exceção. Eu acho que o equivalente mais próximo de fazer isso com um asyncmétodo seria usar um novo objeto para armazenar os dados aos quais o asyncmétodo e o chamador podem se referir. Outra maneira seria passar um delegado como sugerido em outra resposta .

Observe que nenhuma dessas técnicas terá o tipo de imposição do compilador que outpossui. Ou seja, o compilador não exigirá que você defina o valor no objeto compartilhado ou chame um delegado passado.

Aqui está um exemplo de implementação usando um objeto compartilhado para imitar refe outpara uso com asyncmétodos e outros cenários onde refe outnão estão disponíveis:

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
fonte
6

Eu amo o Trypadrão. É um padrão arrumado.

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

Mas, é um desafio async. Isso não significa que não temos opções reais. Aqui estão as três principais abordagens que você pode considerar para os asyncmétodos em uma quase versão do Trypadrão.

Abordagem 1 - produzir uma estrutura

Isso se parece mais com um Trymétodo de sincronização , retornando apenas um em tuplevez de a boolcom um outparâmetro, que todos sabemos que não é permitido em C #.

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

Com um método que retorna truede falsee nunca lança uma exception.

Lembre-se de que lançar uma exceção em um Trymétodo quebra todo o propósito do padrão.

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);
    }
}

Abordagem 2 - aprovação de métodos de retorno de chamada

Podemos usar anonymousmétodos para definir variáveis ​​externas. É uma sintaxe inteligente, embora um pouco complicada. Em pequenas doses, tudo bem.

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

O método obedece ao básico do Trypadrão, mas define os outparâmetros para serem passados ​​nos métodos de retorno de chamada. É feito assim.

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;
    }
}

Há uma pergunta em minha mente sobre desempenho aqui. Mas, o compilador C # é tão inteligente que acho que você está seguro escolhendo essa opção, quase com certeza.

Abordagem 3 - use ContinueWith

E se você apenas usar o TPLcomo projetado? Sem tuplas. A idéia aqui é que usamos exceções para redirecionar ContinueWithpara dois caminhos diferentes.

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

Com um método que lança exceptionquando existe algum tipo de falha. Isso é diferente de retornar a boolean. É uma maneira de se comunicar com o TPL.

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

No código acima, se o arquivo não for encontrado, uma exceção será lançada. Isso invocará a falha ContinueWithque será manipulada Task.Exceptionem seu bloco lógico. Legal, não é?

Ouça, há uma razão pela qual amamos o Trypadrão. É fundamentalmente tão limpo e legível e, como resultado, sustentável. Ao escolher sua abordagem, observe a legibilidade. Lembre-se do próximo desenvolvedor que, em 6 meses, não precisa que você responda perguntas esclarecedoras. Seu código pode ser a única documentação que um desenvolvedor terá.

Boa sorte.

Jerry Nixon
fonte
1
Sobre a terceira abordagem, você tem certeza de que as ContinueWithchamadas em cadeia têm o resultado esperado? Segundo meu entendimento, o segundo ContinueWithverificará o sucesso da primeira continuação, não o sucesso da tarefa original.
Theodor Zoulias 30/09/19
1
Cheers @TheodorZoulias, isso é um olho afiado. Fixo.
Jerry Nixon
1
Lançar exceções para controle de fluxo é um enorme cheiro de código para mim - isso vai prejudicar seu desempenho.
22619 Ian Kemp
Não, @IanKemp, esse é um conceito bastante antigo. O compilador evoluiu.
Jerry Nixon
4

Eu tive o mesmo problema que eu gosto de usar o padrão Try-method, que basicamente parece ser incompatível com o paradigma assíncrono-aguarde ...

Importante para mim é que eu posso chamar o método Try dentro de uma única cláusula if e não preciso pré-definir as variáveis ​​out antes, mas posso fazê-lo em linha, como no exemplo a seguir:

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

Então, eu vim com a seguinte solução:

  1. Defina uma estrutura auxiliar:

     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. Defina o método Try assíncrono como este:

     public async Task<AsyncOut<bool, string>> TryReceiveAsync()
     {
         string message;
         bool success;
         // ...
         return (success, message);
     }
  3. Chame o método Try assíncrono como este:

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

Para vários parâmetros de saída, você pode definir estruturas adicionais (por exemplo, AsyncOut <T, OUT1, OUT2>) ou pode retornar uma tupla.

Michael Gehling
fonte
Esta é uma solução muito inteligente!
Theodor Zoulias 07/07
2

A limitação dos asyncmétodos que não aceitam outparâmetros se aplica apenas aos métodos assíncronos gerados pelo compilador, declarados com a asyncpalavra - chave. Não se aplica a métodos assíncronos criados à mão. Em outras palavras, é possível criar Taskmétodos de retorno que aceitam outparâmetros. Por exemplo, digamos que já temos um ParseIntAsyncmétodo que lança, e queremos criar um TryParseIntAsyncque não seja lançado. Nós poderíamos implementá-lo assim:

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);
}

Usando o TaskCompletionSourcee o ContinueWithmétodo é um pouco estranho, mas não há outra opção, uma vez que não podemos usar a convenienteawait palavra-chave dentro deste método.

Exemplo de uso:

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

Atualização: se a lógica assíncrona for muito complexa para ser expressa sem await, ela poderá ser encapsulada dentro de um delegado anônimo assíncrono aninhado. A TaskCompletionSourceainda seria necessário para o outparâmetro. É possível que o outparâmetro possa ser concluído antes da conclusão da tarefa principal, como no exemplo abaixo:

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);
    }))();
}

Este exemplo assume a existência de três métodos assíncronos GetResponseAsync, GetRawDataAsynce FilterDataAsyncque são chamados em sucessão. O outparâmetro é concluído na conclusão do segundo método. O GetDataAsyncmétodo pode ser usado assim:

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

Aguardar o dataantes de aguardar rawDataLengthé importante neste exemplo simplificado, porque, no caso de uma exceção, o outparâmetro nunca será concluído.

Theodor Zoulias
fonte
1
Esta é uma solução muito agradável para alguns casos.
perfil completo de Jerry Nixon
1

Eu acho que usar ValueTuples assim pode funcionar. Você deve adicionar primeiro o pacote 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
fonte
Você não precisa do NuGet se estiver usando .net-4.7 ou netstandard-2.0.
binki
Ei, você está certo! Acabei de desinstalar o pacote NuGet e ele ainda funciona. Obrigado!
Paul Marangoni
1

Aqui está o código da resposta do @ dcastro modificado para C # 7.0 com tuplas nomeadas e desconstrução de tuplas, que simplifica a notação:

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);
}

Para obter detalhes sobre as novas tuplas nomeadas, literais de tupla e desconstruções de tupla, consulte: https://blogs.msdn.microsoft.com/dotnet/2017/03/09/new-features-in-c-7-0/

Jpsy
fonte
-2

Você pode fazer isso usando a TPL (biblioteca paralela de tarefas) em vez de usar diretamente a palavra-chave wait.

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
fonte
Nunca use .Result. É um anti-padrão. Obrigado!
Ben