如何编写带有out参数的异步方法?


176

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

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

我该怎么做GetDataTaskAsync

Answers:


279

您不能使用带有refout参数的异步方法。

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

至于为什么异步方法不支持按引用引用的参数?(或ref参数?)这是CLR的局限性。我们选择以与迭代器方法类似的方式来实现异步方法-即通过编译器将方法转换为状态机对象。CLR没有安全的方法将“输出参数”或“参考参数”的地址存储为对象的字段。支持按引用引用参数的唯一方法是,如果异步功能是通过低级CLR重写而不是通过编译器重写完成的。我们研究了这种方法,并为此做了很多工作,但是最终它会非常昂贵,以至于从来没有发生过。

这种情况的典型解决方法是让async方法返回一个Tuple。您可以这样重写方法:

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);
}

10
与其说过于复杂,不如说它可能会产生太多问题。乔恩斯基特解释得很好这里stackoverflow.com/questions/20868103/...
MuiBienCarlota

3
感谢您的Tuple选择。很有帮助。
路加福音

19
有丑陋的Tuple。:P
tofutim '16

36
我认为C#7中的命名元组将是完美的解决方案。
orad

3
@orad我特别喜欢这样:private异步Task <(bool成功,作业,字符串消息)> TryGetJobAsync(...)
J. Andrew Laughlin,

51

方法中不能包含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;
}

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


2
我更喜欢这种解决方案,而不是使用元组。更干净!
MiBol

31

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."
        }));
    }

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

var foo = await TryLogin(request);
if (foo.IsSuccess)
     return foo.Result;

12

Alex强调可读性。同样,一个函数的接口也足以定义要返回的类型,并且您还将获得有意义的变量名。

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

调用者提供一个lambda(或命名函数),并且intellisense通过从委托中复制变量名称来提供帮助。

int myOp;
bool result = await GetDataTaskAsync(op => myOp = op);

此特定方法类似于“尝试”方法,myOp如果方法结果为,则在其中设置true。否则,您将不在乎myOp


9

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.");
    }
}

6

我喜欢这种Try模式。这是一个整齐的模式。

if (double.TryParse(name, out var result))
{
    // handle success
}
else
{
    // handle error
}

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

方法1-输出结构

这看起来最像是同步Try方法,仅返回带有参数的a tuple而不是a boolout众所周知,C#不允许这样做。

var result = await DoAsync(name);
if (result.Success)
{
    // handle success
}
else
{
    // handle error
}

与回报的方法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);
    }
}

方法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
}

该方法遵循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;
    }
}

我对这里的性能存在疑问。但是,C#编译器非常聪明,以至于几乎可以肯定,您选择此选项是安全的。

方法3-使用ContinueWith

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

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

使用一种exception在出现任何类型的故障时引发的方法。这与传回的方式不同boolean。这是与沟通的一种方式TPL

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

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

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

祝你好运。


1
关于第三种方法,您确定链接ContinueWith调用具有预期的结果吗?根据我的理解,第二个ContinueWith将检查第一个延续的成功,而不是原始任务的成功。
Theodor Zoulias

1
欢呼@TheodorZoulias,那是敏锐的眼睛。固定。
杰里·尼克松

1
对我来说,抛出流控制异常是一种巨大的代码味道-它会破坏您的性能。
伊恩·肯普

不,@ IanKemp,这是一个非常古老的概念。编译器已经发展。
杰里·尼克松

4

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

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

if (TryReceive(out string msg))
{
    // use msg
}

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

  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);
     }
  2. 像这样定义异步Try方法:

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

     if ((await TryReceiveAsync()).Out(out string msg))
     {
         // use msg
     }

对于多个out参数,您可以定义其他结构(例如AsyncOut <T,OUT1,OUT2>),也可以返回一个元组。


这是一个非常聪明的解决方案!
Theodor Zoulias

2

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);
}

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

用法示例:

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

更新:如果异步逻辑过于复杂而无法使用表示await,则可以将其封装在嵌套的异步匿名委托中。参数TaskCompletionSource仍然需要A。out这可能是该out参数可以在主任务完成之前完成,如示例波纹管:

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);
    }))();
}

本示例假定存在三个异步方法GetResponseAsyncGetRawDataAsync并且FilterDataAsync它们被连续调用。的out参数完成在第二方法的完成。该GetDataAsync方法可以这样使用:

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

等待data等待之前rawDataLength在这个简化的例子很重要,因为在异常情况下的out参数永远不能完成的。


1
在某些情况下,这是一个非常好的解决方案。
杰里·尼克松

1

我认为像这样使用ValueTuples可以工作。但是,您必须先添加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):
}

如果使用.net-4.7或netstandard-2.0,则不需要NuGet。
宾基

嘿,你是对的!我刚刚卸载了该NuGet软件包,它仍然可以工作。谢谢!
Paul Marangoni,

1

以下是@dcastro答案的代码,该代码针对C#7.0进行了修改,其中包含命名元组和元组解构,从而简化了表示法:

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);
}

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


-2

您可以通过使用TPL(任务并行库)而不是直接使用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

永远不要使用.Result。这是一种反模式。谢谢!
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.