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

本文关键字:参数 异步方法 out 何编写 | 更新日期: 2023-09-27 18:30:43

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

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

如何在GetDataTaskAsync中执行此操作?

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

不能使用具有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

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

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;

async方法中不能有refout参数(如前所述)。

这要求在

移动的数据中进行一些建模:

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

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

我遇到了同样的问题,因为我喜欢使用 Try-method-pattern 基本上似乎与异步等待范式不兼容......

对我来说重要的是,我可以在单个 if 子句中调用 Try-方法,并且不必在之前预定义 out-variables,但可以像以下示例一样内联执行此操作:

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

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

注意:新的解决方案更胜一筹,因为它可以与仅返回元组的方法一起使用,如此处的许多其他答案中所述,这些方法通常可以在现有代码中找到!

新解决方案:

  1. 为值元组创建扩展方法:

    public static class TupleExtensions
    {
      public static bool TryOut<P2>(this ValueTuple<bool, P2> tuple, out P2 p2)
      {
         bool p1;
         (p1, p2) = tuple;
         return p1;
      }
      public static bool TryOut<P2, P3>(this ValueTuple<bool, P2, P3> tuple, out P2 p2, out P3 p3)
      {
         bool p1;
         (p1, p2, p3) = tuple;
         return p1;
      }
      // continue to support larger tuples...
    }
    
  2. 定义异步尝试方法,如下所示:

     public async Task<(bool, string)> TryReceiveAsync()
     {
         string message;
         bool success;
         // ...
         return (success, message);
     }
    
  3. 调用异步 Try 方法,如下所示:

     if ((await TryReceiveAsync()).TryOut(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. 定义异步尝试方法,如下所示:

     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>),也可以返回元组。

Alex在

可读性方面提出了一个很好的观点。 等效地,函数也有足够的接口来定义要返回的类型,并且您还可以获得有意义的变量名称。

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

调用方提供 lambda(或命名函数),智能感知通过从委托复制变量名称来提供帮助。

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

这种特殊方法类似于"Try"方法,如果方法结果true,则设置myOp。否则,你不在乎myOp.

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

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

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

方法 1 - 输出结构

这看起来最像一个同步Try方法,只返回一个tuple,而不是一个带有out参数的bool,我们都知道这在 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 - 使用"继续"

如果您只是按设计使用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);
}

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

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

祝你好运。

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

下面是针对 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);
}

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

不接受out参数的async方法的限制仅适用于编译器生成的异步方法,这些方法使用 async 关键字声明。它不适用于手工制作的异步方法。换句话说,可以创建接受out参数的Task返回方法。例如,假设我们已经有一个抛出的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的情况下表达,那么它可以封装在嵌套的异步匿名委托中。out参数仍需要TaskCompletionSourceout参数有可能在之前完成主要任务的完成,如以下示例所示:

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

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

我认为像这样使用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):
}
<</div> div class="answers">

模式匹配救援!C#9(我认为)以后:

// example of a method that would traditionally would use an out parameter
public async Task<(bool success, int? value)> TryGetAsync()
{
    int? value = // get it from somewhere
    
    return (value.HasValue, value);
}

像这样使用它:

if (await TryGetAsync() is (true, int value))
{
    Console.WriteLine($"This is the value: {value}");
}

这与Michael Gehling提供的答案非常相似,但我有自己的解决方案,直到我找到他并注意到我不是第一个想到使用隐式转换的人。

无论如何,我想分享,因为我也支持nullable设置为enable

public readonly struct TryResult<TOut>
{
    #region constructors
    public TryResult(bool success, TOut? value) => (Success, Value) = (success, value);
    #endregion
    #region properties
    public                                            bool  Success { get; init; }
    [MemberNotNullWhen(true, nameof(Success))] public TOut? Value   { get; init; }
    #endregion
    #region methods
    public static implicit operator bool(TryResult<TOut> result) => result.Success;
    public static implicit operator TryResult<TOut>(TOut value) => new (true, value);
    public void Deconstruct(out bool success, out TOut? value) => (success, value) = (Success, Value);
    public TryResult<TOut> Out([NotNullWhen(true)] out TOut? value)
    {
        value = Value;
        return this;
    }
    #endregion
}

然后你可以编写一个 Try 方法,如下所示:

public static async Task<TryResult<byte[]>> TryGetBytesAsync(string file) =>
    File.Exists(file)
        ? await File.ReadAllBytesAsync(file)
        : default(TryResult<byte[]>);

并像这样称呼它:

if ((await TryGetBytesAsync(file)).Out(out var bytes))
    Console.WriteLine($"File has {bytes.Length} bytes.");

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

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

您可以通过使用

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