如何用out参数编写异步方法?

jes*_*sse 148 c# async-await

我想用out参数编写一个异步方法,如下所示:

public async void Method1()
{
    int op;
    int result = await GetDataTaskAsync(out op);
}
Run Code Online (Sandbox Code Playgroud)

我该怎么办GetDataTaskAsync

dca*_*tro 238

您不能使用refout参数的异步方法.

Lucian Wischik解释了为什么在这个MSDN线程上无法做到这一点:http://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-async-methods-cannot-have -ref或出局参数

至于为什么异步方法不支持out-by-reference参数?(或参考参数?)这是CLR的限制.我们选择以与迭代器方法类似的方式实现异步方法 - 即通过编译器将方法转换为状态机对象.CLR没有安全的方法将"out参数"或"引用参数"的地址存储为对象的字段.支持out-by-reference参数的唯一方法是,如果异步功能是由低级CLR重写而不是编译器重写完成的.我们检查了这种方法,并且它有很多用途,但它最终会如此昂贵,以至于它永远不会发生.

这种情况的典型解决方法是让异步方法返回一个元组.你可以这样重写你的方法:

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);
}
Run Code Online (Sandbox Code Playgroud)

  • 我认为C#7中的[Named Tuples](http://www.thomaslevesque.com/2016/07/25/tuples-in-c-7/)将是完美的解决方案. (32认同)
  • "Tuple```很难看.:P (14认同)
  • 这可能会产生太多问题,而不是太复杂.Jon Skeet在这里解释得非常好http://stackoverflow.com/questions/20868103/ref-and-out-arguments-in-async-method (9认同)
  • 感谢`Tuple`替代方案.很有帮助. (3认同)
  • @orad我特别喜欢这样:private异步Task &lt;(bool成功,作业,字符串消息)&gt; TryGetJobAsync(...) (2认同)
  • 请不要鼓励`async void Foo()`。这是一种不好的做法,由于缺少替代方法,因此仅应在UI回调中使用。 (2认同)
  • 为什么编译器不在幕后创建元组? (2认同)
  • 元组返回值,无论是否命名,在我的书中都是一种代码味道,因为它们往往会积累新成员,直到变得难以使用。另外,您没有继承链,因此如果您想编写代码来对方法的不同形状的元组返回值进行操作,那么您就不走运了(而且您还面临相反的问题,即您的代码不够具体)根据你的方法)。tl;dr 使用元组作为 retvals 与“动态”相比几乎没有进步 - C# 是一种 OOP 语言,因此使用适当的类来允许继承,例如 /sf/answers/1672393481/ (2认同)
  • 为了更简洁,您可以在一行中解压元组: var (op, result) = wait GetDataTaskAsync(); (2认同)

Ale*_*lex 46

您不能拥有方法中的参数refout参数async(如前所述).

这对数据移动中的一些建模感到尖叫:

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;
}
Run Code Online (Sandbox Code Playgroud)

您可以更轻松地重用代码,并且它比变量或元组更具可读性.

  • 我更喜欢这个解决方案而不是使用元组。更干净! (2认同)

小智 18

C#7 +解决方案是使用隐式元组语法.

    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."
        }));
    }
Run Code Online (Sandbox Code Playgroud)

返回结果使用方法签名定义的属性名称.例如:

var foo = await TryLogin(request);
if (foo.IsSuccess)
     return foo.Result;
Run Code Online (Sandbox Code Playgroud)

  • 为什么你将“true”硬编码为“isSuccess”返回值,但返回 BadRequest 作为结果? (3认同)

小智 12

亚历克斯在可读性方面做得很好.同样,函数也是足以定义返回类型的接口,您还可以获得有意义的变量名称.

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

调用者通过从委托中复制变量名来提供lambda(或命名函数)和intellisense帮助.

int myOp;
bool result = await GetDataTaskAsync(op => myOp = op);
Run Code Online (Sandbox Code Playgroud)

这种特殊的方法就像一个"Try"方法,myOp如果方法结果是设置的话true.否则,你不在乎myOp.


Mic*_*ing 12

我遇到了与我喜欢使用 Try-method-pattern 相同的问题,它基本上似乎与 async-await-paradigm 不兼容......

对我来说重要的是,我可以在单个 if 子句中调用 Try 方法,而不必预先定义输出变量,但可以像以下示例一样在线执行:

if (TryReceive(out string msg))
{
    // use msg
}
Run Code Online (Sandbox Code Playgroud)

所以我想出了以下解决方案:

  1. 定义一个辅助结构:

     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);
     }
    
    Run Code Online (Sandbox Code Playgroud)
  2. 像这样定义异步尝试方法:

     public async Task<AsyncOut<bool, string>> TryReceiveAsync()
     {
         string message;
         bool success;
         // ...
         return (success, message);
     }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 像这样调用异步 Try 方法:

     if ((await TryReceiveAsync()).Out(out string msg))
     {
         // use msg
     }
    
    Run Code Online (Sandbox Code Playgroud)

对于多个输出参数,您可以定义额外的结构(例如 AsyncOut<T,OUT1, OUT2>),或者您可以返回一个元组。

  • 这怎么不是最重要的答案之一,这个解决方案提供了与非异步版本的奇偶校验,与其他让您返回一个元组然后运行 ​​if 块的答案不同。 (4认同)

Jer*_*xon 9

我喜欢这个Try图案。这是一个整洁的模式。

if (double.TryParse(name, out var result))
{
    // handle success
}
else
{
    // handle error
}
Run Code Online (Sandbox Code Playgroud)

但是,它具有挑战性async。这并不意味着我们没有真正的选择。以下是您可以为模式async的准版本中的方法考虑的三种核心方法Try

方法 1 - 输出结构

这看起来最像一个同步Try方法,只返回一个参数tuple而不是bool一个out参数,我们都知道在 C# 中是不允许的。

var result = await DoAsync(name);
if (result.Success)
{
    // handle success
}
else
{
    // handle error
}
Run Code Online (Sandbox Code Playgroud)

与回报的方法truefalse,从来没有抛出exception

请记住,在Try方法中抛出异常会破坏模式的全部目的。

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

方法 2 - 传入回调方法

我们可以使用anonymous方法来设置外部变量。这是聪明的语法,虽然有点复杂。小剂量,没问题。

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

该方法遵循Try模式的基础,但设置out参数以在回调方法中传递。它是这样完成的。

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我心中有一个关于性能的问题。但是,C# 编译器非常聪明,我认为您几乎可以肯定选择此选项是安全的。

方法 3 - 使用 ContinueWith

如果你只是TPL按照设计使用怎么办?没有元组。这里的想法是我们使用异常重定向ContinueWith到两个不同的路径。

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

使用一种exception在出现任何类型的失败时抛出 an 的方法。这与返回 a 不同boolean。这是一种与TPL.

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

在上面的代码中,如果找不到文件,则抛出异常。这将调用ContinueWithTask.Exception在其逻辑块中处理的故障。整洁吧?

听着,我们喜欢这种Try模式是有原因的。从根本上说,它是如此整洁和可读,因此,它是可维护的。当你选择你的方法时,看门狗的可读性。记住下一个开发人员,他在 6 个月内没有让您回答澄清问题。您的代码可能是开发人员将拥有的唯一文档。

祝你好运。

  • 对我来说,抛出流量控制异常是一种巨大的代码味道——它会降低你的性能。 (2认同)

bin*_*nki 8

out参数的一个很好的特性是,即使函数抛出异常,它们也可用于返回数据.我认为async使用方法执行此操作的最接近的等价物是使用新对象来保存async方法和调用者可以引用的数据.另一种方法是按照另一个答案中的建议传递委托.

请注意,这些技术都没有来自编译器的任何强制执行out.即,编译器不要求您在共享对象上设置值或调用传入的委托.

这是一个使用共享对象模拟refout使用async方法和其他各种场景的示例实现,其中refout不可用:

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.");
    }
}
Run Code Online (Sandbox Code Playgroud)


Jps*_*psy 7

下面是针对 C# 7.0 修改的 @dcastro 答案的代码,其中包含命名元组和元组解构,它简化了符号:

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);
}
Run Code Online (Sandbox Code Playgroud)

有关新命名元组、元组文字和元组解构的详细信息,请参阅: https: //blogs.msdn.microsoft.com/dotnet/2017/03/09/new-features-in-c-7-0/


The*_*ias 6

async不接受参数的方法的限制out仅适用于编译器生成的异步方法,这些方法使用async关键字声明。它不适用于手工制作的异步方法。换句话说,可以创建Task接受参数的返回方法out。例如,假设我们已经有一个ParseIntAsync会抛出异常的方法,而我们想要创建一个TryParseIntAsync不会抛出异常的方法。我们可以这样实现:

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);
}
Run Code Online (Sandbox Code Playgroud)

使用TaskCompletionSourceand theContinueWith方法有点尴尬,但没有其他选择,因为我们不能await在这个方法中使用方便的关键字。

使用示例:

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

更新:如果异步逻辑太复杂而无法在没有 的情况下表达await,则可以将其封装在嵌套的异步匿名委托内。该参数TaskCompletionSource仍然需要A。outout参数有可能在主任务完成之前完成,如下例所示:

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);
    }))();
}
Run Code Online (Sandbox Code Playgroud)

此示例假设存在三个异步方法GetResponseAsyncGetRawDataAsync并且FilterDataAsync连续调用这些方法。该out参数在第二个方法完成时完成。该GetDataAsync方法可以这样使用:

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

data在这个简化的示例中,在等待之前等待rawDataLength很重要,因为如果发生异常,out参数将永远不会完成。