Respostas:
Você não pode ter métodos assíncronos com ref
ou out
parâ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);
}
Tuple
alternativa. Muito útil.
Tuple
. : P
Você não pode ter ref
ou out
parâmetros nos async
mé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 out
parâ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 async
método seria usar um novo objeto para armazenar os dados aos quais o async
mé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 out
possui. 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 ref
e out
para uso com async
métodos e outros cenários onde ref
e out
nã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 Try
padrã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 async
métodos em uma quase versão do Try
padrão.
Isso se parece mais com um Try
método de sincronização , retornando apenas um em tuple
vez de a bool
com um out
parâ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 true
de false
e nunca lança uma exception
.
Lembre-se de que lançar uma exceção em um
Try
mé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 anonymous
mé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 Try
padrão, mas define os out
parâ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 TPL
como projetado? Sem tuplas. A idéia aqui é que usamos exceções para redirecionar ContinueWith
para 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 exception
quando 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 ContinueWith
que será manipulada Task.Exception
em seu bloco lógico. Legal, não é?
Ouça, há uma razão pela qual amamos o
Try
padrã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.
ContinueWith
chamadas em cadeia têm o resultado esperado? Segundo meu entendimento, o segundo ContinueWith
verificará 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 async
métodos que não aceitam out
parâmetros se aplica apenas aos métodos assíncronos gerados pelo compilador, declarados com a async
palavra - chave. Não se aplica a métodos assíncronos criados à mão. Em outras palavras, é possível criar Task
métodos de retorno que aceitam out
parâmetros. Por exemplo, digamos que já temos um ParseIntAsync
método que lança, e queremos criar um TryParseIntAsync
que 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 TaskCompletionSource
e o ContinueWith
mé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 TaskCompletionSource
ainda seria necessário para o out
parâmetro. É possível que o out
parâ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
, GetRawDataAsync
e FilterDataAsync
que são chamados em sucessão. O out
parâmetro é concluído na conclusão do segundo método. O GetDataAsync
mé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 data
antes de aguardar rawDataLength
é importante neste exemplo simplificado, porque, no caso de uma exceção, o out
parâ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