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

时间:2013-09-10 10:50:03

标签: c# async-await

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

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

我如何在GetDataTaskAsync中执行此操作?

12 个答案:

答案 0 :(得分:208)

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

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

  

为什么异步方法不支持out-by-reference参数?   (或参考参数?)这是CLR的限制。我们选择了   以与迭代器方法类似的方式实现异步方法 - 即   通过编译器将方法转换为   状态机对象。 CLR没有安全存储地址的方法   “out参数”或“引用参数”作​​为对象的字段。   支持out-by-reference参数的唯一方法是if   异步功能是通过低级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);
}

答案 1 :(得分:41)

ref方法中不能包含outasync个参数(如前所述)。

对于数据移动中的一些建模而言,这尖叫着:

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 :(得分:8)

亚历克斯在可读性上提出了很好的观点。同样,函数也是足以定义返回类型的接口,并且您还可以获得有意义的变量名称。

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

这种特殊的方法就像一个&#34;尝试&#34;如果方法结果为myOp,则设置true的方法。否则,您不关心myOp

答案 3 :(得分:8)

out参数的一个很好的特性是,即使函数抛出异常,它们也可用于返回数据。我认为使用async方法执行此操作的最接近的等价物是使用新对象来保存async方法和调用方都可以引用的数据。另一种方式是pass a delegate as suggested in another answer

请注意,这些技术都不会具有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.");
    }
}

答案 4 :(得分:6)

The C#7+ Solution是使用隐式元组语法。

gcloud

返回结果使用方法签名定义的属性名称。 e.g:

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

答案 5 :(得分:3)

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

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

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

方法1-输出结构

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

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

使用返回true中的false且从不抛出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);
}

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

  

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

好运。

答案 6 :(得分:2)

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

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

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

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

  1. 定义一个辅助结构:

    public struct AsyncOutResult<T, OUT>
    {
        T returnValue;
        OUT result;
    
        public AsyncOutResult(T returnValue, OUT result)
        {
            this.returnValue = returnValue;
            this.result = result;
        }
    
        public T Result(out OUT result)
        {
            result = this.result;
            return returnValue;
        }
    }
    
  2. 像这样定义异步Try方法:

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

    if ((await TryReceiveAsync()).Result(out T msg))
    {
        // use msg
    }
    

如果您需要多个out参数,则当然可以定义额外的结构,如下所示:

public struct AsyncOutResult<T, OUT1, OUT2>
{
    T returnValue;
    OUT1 result1;
    OUT2 result2;

    public AsyncOutResult(T returnValue, OUT1 result1, OUT2 result2)
    {
        this.returnValue = returnValue;
        this.result1 = result1;
        this.result2 = result2;
    }

    public T Result(out OUT1 result1, out OUT2 result2)
    {
        result1 = this.result1;
        result2 = this.result2;
        return returnValue;
    }
}

答案 7 :(得分: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):
}

答案 8 :(得分:1)

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

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

用法示例:

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

更新:如果异步逻辑过于复杂而无法在没有await的情况下进行表达,则可以将其封装在嵌套的异步匿名委托中。 TaskCompletionSource参数仍然需要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);
    }))();
}

此示例假定存在三个异步方法GetResponseAsyncGetRawDataAsyncFilterDataAsync,它们被称为 陆续。 out参数在第二种方法完成时完成。 GetDataAsync方法可以这样使用:

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

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

答案 9 :(得分:1)

对于真正希望将其保留在参数中的开发人员,这里可能是另一种解决方法。

将参数更改为数组或列表以包装实际值。请记住在发送到方法之前初始化列表。返回后,请务必在使用之前检查值是否存在。谨慎编码。

答案 10 :(得分:0)

以下是使用命名元组和元组解构对C#7.0修改的@dcastro代码的代码,它简化了符号:

UNION

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

答案 11 :(得分:-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