¿Cómo escribir un método asíncrono sin parámetro?

176

Quiero escribir un método asíncrono con un outparámetro, como este:

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

¿Cómo hago esto GetDataTaskAsync?

jesse
fuente

Respuestas:

279

No puede tener métodos asincrónicos con refo outpará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);
}
dcastro
fuente
10
Lejos de ser demasiado complejo, esto podría producir muchos problemas. Jon Skeet lo explicó muy bien aquí stackoverflow.com/questions/20868103/…
MuiBienCarlota
3
Gracias por la Tuplealternativa. Muy útil.
Luke Vo
19
es feo tener Tuple. : P
tofutim
36
Creo que las tuplas con nombre en C # 7 serán la solución perfecta para esto.
orad
3
@orad Me gusta especialmente esto: Tarea asincrónica privada <(éxito de bool, Trabajo, mensaje de cadena)> TryGetJobAsync (...)
J. Andrew Laughlin
51

No puede tener refni outparámetros en los asyncmé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.

Alex
fuente
2
Prefiero esta solución en lugar de usar una Tupla. ¡Más claro!
MiBol
31

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;
jv_
fuente
12

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 myOpse establece si el resultado del método es true. De lo contrario, no te importa myOp.

Scott Turner
fuente
9

Una buena característica de los outpará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 asyncmétodo sería usar un nuevo objeto para contener los datos a los que tanto el asyncmé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 outtiene. 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 refy outpara usar con asyncmétodos y otros escenarios diversos donde refy outno 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.");
    }
}
binki
fuente
6

Amo el Trypatró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 asyncmétodos en una versión cuasi del Trypatrón.

Enfoque 1: generar una estructura

Esto se parece más a un Trymétodo de sincronización que solo devuelve un en tuplelugar de un boolcon un outpará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 truede falsey nunca tira una exception.

Recuerde, lanzar una excepción en un Trymé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);
    }
}

Enfoque 2: pasar los métodos de devolución de llamada

Podemos usar anonymousmé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 Trypatrón, pero establece los outpará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.

Enfoque 3: use ContinueWith

¿Qué pasa si solo usa el TPLdiseño? No hay tuplas La idea aquí es que usemos excepciones para redirigir ContinueWitha 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 exceptioncuando 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 ContinueWithque manejará Task.Exceptionen su bloque lógico. Aseado, ¿eh?

Escucha, hay una razón por la que amamos el Trypatró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.

Jerry Nixon
fuente
1
Sobre el tercer enfoque, ¿estás seguro de que encadenar ContinueWithllamadas tiene el resultado esperado? Según tengo entendido, el segundo ContinueWithverificará el éxito de la primera continuación, no el éxito de la tarea original.
Theodor Zoulias
1
Saludos @TheodorZoulias, ese es un ojo agudo. Fijo.
Jerry Nixon
1
Lanzar excepciones para el control de flujo es un olor de código masivo para mí: va a afectar su rendimiento.
Ian Kemp
No, @IanKemp, ese es un concepto bastante antiguo. El compilador ha evolucionado.
Jerry Nixon
4

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:

  1. 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);
     }
  2. 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);
     }
  3. 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.

Michael Gehling
fuente
¡Esta es una solución muy inteligente!
Theodor Zoulias
2

La limitación de los asyncmétodos que no aceptan outparámetros se aplica solo a los métodos asíncronos generados por el compilador, estos declarados con la asyncpalabra clave. No se aplica a los métodos asíncronos hechos a mano. En otras palabras, es posible crear Taskmétodos de retorno que acepten outparámetros. Por ejemplo, digamos que ya tenemos un ParseIntAsyncmétodo que arroja, y queremos crear uno TryParseIntAsyncque 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 TaskCompletionSourcey el ContinueWithmétodo es un poco incómodo, pero no hay otra opción, ya que no podemos usar el cómodo awaitpalabra 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 TaskCompletionSourcetodavía sería necesario para el outparámetro. Es posible que el outpará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, GetRawDataAsyncy FilterDataAsyncque se llaman en sucesión. El outparámetro se completa al completar el segundo método. El GetDataAsyncmé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 dataanterior antes de esperar rawDataLengthes importante en este ejemplo simplificado, porque en caso de una excepción, el outparámetro nunca se completará.

Theodor Zoulias
fuente
1
Esta es una solución muy buena para algunos casos.
Jerry Nixon
1

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):
}
Paul Marangoni
fuente
No necesita NuGet si utiliza .net-4.7 o netstandard-2.0.
binki
Hey, tienes razon! Acabo de desinstalar ese paquete NuGet y todavía funciona. ¡Gracias!
Paul Marangoni
1

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/

Jpsy
fuente
-2

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
Payam Buroumand
fuente
Nunca use .Resultado. Es un antipatrón. ¡Gracias!
Ben