Respuestas:
No puede tener métodos asincrónicos con ref
o out
parámetros.
Lucian Wischik explica por qué esto no es posible en este hilo de MSDN: http://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-async-methods-cannot-have -ref-or-out-parámetros
¿Por qué los métodos asíncronos no admiten parámetros de referencia? (¿o parámetros de referencia?) Esa es una limitación del CLR. Elegimos implementar métodos asincrónicos de manera similar a los métodos iteradores, es decir, a través del compilador transformando el método en un objeto máquina de estado. El CLR no tiene una forma segura de almacenar la dirección de un "parámetro de salida" o "parámetro de referencia" como un campo de un objeto. La única forma de admitir parámetros de salida por referencia sería si la función asincrónica se realizara mediante una reescritura CLR de bajo nivel en lugar de una reescritura del compilador. Examinamos ese enfoque, y tenía muchas posibilidades, pero en última instancia habría sido tan costoso que nunca hubiera sucedido.
Una solución alternativa típica para esta situación es hacer que el método asíncrono devuelva una Tupla. Podrías reescribir tu 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. Muy útil.
Tuple
. : P
No puede tener ref
ni out
parámetros en los async
métodos (como ya se señaló).
Esto grita un poco de modelado en los datos que se mueven:
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;
}
Obtiene la capacidad de reutilizar su código más fácilmente, además es mucho más legible que las variables o las tuplas.
La solución C # 7 + es usar sintaxis 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."
}));
}
El resultado de retorno utiliza los nombres de propiedad definidos por la firma del método. p.ej:
var foo = await TryLogin(request);
if (foo.IsSuccess)
return foo.Result;
Alex hizo un gran punto sobre la legibilidad. De manera equivalente, una función también es lo suficientemente interfaz como para definir los tipos que se devuelven y también se obtienen nombres de variables significativos.
delegate void OpDelegate(int op);
Task<bool> GetDataTaskAsync(OpDelegate callback)
{
bool canGetData = true;
if (canGetData) callback(5);
return Task.FromResult(canGetData);
}
Las personas que llaman proporcionan una lambda (o una función con nombre) e intellisense ayuda copiando los nombres de las variables del delegado.
int myOp;
bool result = await GetDataTaskAsync(op => myOp = op);
Este enfoque particular es como un método "Probar" donde myOp
se establece si el resultado del método es true
. De lo contrario, no te importa myOp
.
Una buena característica de los out
parámetros es que pueden usarse para devolver datos incluso cuando una función arroja una excepción. Creo que el equivalente más cercano a hacer esto con un async
método sería usar un nuevo objeto para contener los datos a los que tanto el async
método como la persona que llama pueden referirse. Otra forma sería pasar un delegado como se sugiere en otra respuesta .
Tenga en cuenta que ninguna de estas técnicas tendrá el tipo de aplicación del compilador que out
tiene. Es decir, el compilador no requerirá que establezca el valor en el objeto compartido o llame a un delegado pasado.
Aquí hay una implementación de ejemplo usando un objeto compartido para imitar ref
y out
para usar con async
métodos y otros escenarios diversos donde ref
y out
no están 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.");
}
}
Amo el Try
patrón Es un patrón ordenado.
if (double.TryParse(name, out var result))
{
// handle success
}
else
{
// handle error
}
Pero, es un desafío con async
. Eso no significa que no tengamos opciones reales. Estos son los tres enfoques principales que puede considerar para los async
métodos en una versión cuasi del Try
patrón.
Esto se parece más a un Try
método de sincronización que solo devuelve un en tuple
lugar de un bool
con un out
parámetro, que todos sabemos que no está permitido en C #.
var result = await DoAsync(name);
if (result.Success)
{
// handle success
}
else
{
// handle error
}
Con un método que devuelve true
de false
y nunca tira una exception
.
Recuerde, lanzar una excepción en un
Try
método rompe todo el propósito del patrón.
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 establecer variables externas. Es una sintaxis inteligente, aunque un poco complicada. En pequeñas dosis, está bien.
var file = default(StorageFile);
var exception = default(Exception);
if (await DoAsync(name, x => file = x, x => exception = x))
{
// handle success
}
else
{
// handle failure
}
El método obedece los conceptos básicos del Try
patrón, pero establece los out
parámetros que se pasan en los métodos de devolución de llamada. Se hace así.
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;
}
}
Hay una pregunta en mi mente sobre el rendimiento aquí. Pero, el compilador de C # es tan increíblemente inteligente, que creo que está seguro de elegir esta opción, casi con seguridad.
¿Qué pasa si solo usa el TPL
diseño? No hay tuplas La idea aquí es que usemos excepciones para redirigir ContinueWith
a dos caminos diferentes.
await DoAsync(name).ContinueWith(task =>
{
if (task.Exception != null)
{
// handle fail
}
if (task.Result is StorageFile sf)
{
// handle success
}
});
Con un método que arroja un exception
cuando hay algún tipo de falla. Eso es diferente a devolver a boolean
. Es una forma de comunicarse con el TPL
.
async Task<StorageFile> DoAsync(string fileName)
{
var folder = ApplicationData.Current.LocalCacheFolder;
return await folder.GetFileAsync(fileName);
}
En el código anterior, si no se encuentra el archivo, se genera una excepción. Esto invocará la falla ContinueWith
que manejará Task.Exception
en su bloque lógico. Aseado, ¿eh?
Escucha, hay una razón por la que amamos el
Try
patrón. Es fundamentalmente tan ordenado y legible y, como resultado, mantenible. A medida que elija su enfoque, vigile la legibilidad. Recuerde el próximo desarrollador que en 6 meses y no tiene que responder preguntas aclaratorias. Su código puede ser la única documentación que tendrá un desarrollador.
La mejor de las suertes.
ContinueWith
llamadas tiene el resultado esperado? Según tengo entendido, el segundo ContinueWith
verificará el éxito de la primera continuación, no el éxito de la tarea original.
Tuve el mismo problema que me gusta usar el patrón de método Try, que básicamente parece ser incompatible con el paradigma async-waitit ...
Importante para mí es que puedo llamar al método Try dentro de una sola cláusula if y no tengo que predefinir las variables out antes, pero puedo hacerlo en línea como en el siguiente ejemplo:
if (TryReceive(out string msg))
{
// use msg
}
Entonces se me ocurrió la siguiente solución:
Defina una estructura 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 el método de prueba asíncrono de esta manera:
public async Task<AsyncOut<bool, string>> TryReceiveAsync()
{
string message;
bool success;
// ...
return (success, message);
}
Llame al método Try asíncrono de esta manera:
if ((await TryReceiveAsync()).Out(out string msg))
{
// use msg
}
Para múltiples parámetros de salida, puede definir estructuras adicionales (por ejemplo, AsyncOut <T, OUT1, OUT2>) o puede devolver una tupla.
La limitación de los async
métodos que no aceptan out
parámetros se aplica solo a los métodos asíncronos generados por el compilador, estos declarados con la async
palabra clave. No se aplica a los métodos asíncronos hechos a mano. En otras palabras, es posible crear Task
métodos de retorno que acepten out
parámetros. Por ejemplo, digamos que ya tenemos un ParseIntAsync
método que arroja, y queremos crear uno TryParseIntAsync
que no arroje. Podríamos implementarlo así:
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);
}
Utilizando el TaskCompletionSource
y el ContinueWith
método es un poco incómodo, pero no hay otra opción, ya que no podemos usar el cómodo await
palabra clave dentro de este método.
Ejemplo de uso:
if (await TryParseIntAsync("-13", out var result))
{
Console.WriteLine($"Result: {await result}");
}
else
{
Console.WriteLine($"Parse failed");
}
Actualización: si la lógica asincrónica es demasiado compleja para expresarse sin ella await
, entonces podría encapsularse dentro de un delegado anónimo asíncrono anidado. A TaskCompletionSource
todavía sería necesario para el out
parámetro. Es posible que el out
parámetro se pueda completar antes de completar la tarea principal, como en el siguiente ejemplo:
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 ejemplo supone la existencia de tres métodos asincrónicos GetResponseAsync
, GetRawDataAsync
y FilterDataAsync
que se llaman en sucesión. El out
parámetro se completa al completar el segundo método. El GetDataAsync
método podría usarse así:
var data = await GetDataAsync("http://example.com", out var rawDataLength);
Console.WriteLine($"Data: {data}");
Console.WriteLine($"RawDataLength: {await rawDataLength}");
Esperar lo data
anterior antes de esperar rawDataLength
es importante en este ejemplo simplificado, porque en caso de una excepción, el out
parámetro nunca se completará.
Creo que usar ValueTuples como este puede funcionar. Sin embargo, primero debe agregar el paquete 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):
}
Aquí está el código de la respuesta de @ dcastro modificado para C # 7.0 con tuplas con nombre y deconstrucción de tuplas, que simplifica la notación:
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 obtener detalles sobre las nuevas tuplas con nombre, tuples literales y deconstrucciones de tuplas, consulte: https://blogs.msdn.microsoft.com/dotnet/2017/03/09/new-features-in-c-7-0/
Puede hacerlo utilizando TPL (biblioteca paralela de tareas) en lugar de usar directamente la palabra clave 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