Respostas:
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);
}
Tuplealternativa. Muito útil.
Tuple. : P
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.
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;
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.
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.");
}
}
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.
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);
}
}
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.
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.
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.
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:
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);
}
Defina o método Try assíncrono como este:
public async Task<AsyncOut<bool, string>> TryReceiveAsync()
{
string message;
bool success;
// ...
return (success, message);
}
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.
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.
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):
}
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/
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