【问题标题】:How to write an async method with out parameter?如何编写带 out 参数的异步方法?
【发布时间】:2013-09-14 01:05:41
【问题描述】:

我想写一个带有out参数的异步方法,像这样:

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

如何在GetDataTaskAsync 中执行此操作?

【问题讨论】:

    标签: c# async-await


    【解决方案1】:

    您不能使用带有 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 参数? (或 ref 参数?)这是 CLR 的限制。我们选择了 以与迭代器方法类似的方式实现异步方法——即 通过编译器将方法转换为 状态机对象。 CLR 没有安全的方法来存储 作为对象的字段的“输出参数”或“引用参数”。 支持 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);
    }
    

    【讨论】:

    • 远非太复杂,这可能会产生太多问题。 Jon Skeet 在这里解释得很好stackoverflow.com/questions/20868103/…
    • 感谢Tuple 替代方案。很有帮助。
    • Tuple 很难看。 :P
    • 我认为 C# 7 中的 Named Tuples 将是解决此问题的完美解决方案。
    • @orad 我特别喜欢这个:private async Task TryGetJobAsync(...)
    【解决方案2】:

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

        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;
    

    【讨论】:

    • 我要补充一点,我刚刚开始在我的一个应用程序中大量使用它。结果模式是金色的。
    【解决方案3】:

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

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

    【讨论】:

    • 我更喜欢这个解决方案,而不是使用元组。更干净!
    【解决方案4】:

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

    对我来说重要的是,我可以在单个 if 子句中调用 Try 方法,而不必预先定义 out-variables,但可以像以下示例中那样内联:

    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
       }
      

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

    【讨论】:

    • 这是一个非常聪明的解决方案!
    • 这不是最重要的答案之一,这个解决方案提供了与非异步版本的奇偶校验,不像其他让你返回一个元组然后运行你的 if 块的 anwsers。
    【解决方案5】:

    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

    【讨论】:

      【解决方案6】:

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

      【讨论】:

        【解决方案7】:

        我喜欢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
        }
        

        使用返回 falsetrue 并且从不抛出 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 个月内没有让你回答澄清问题。您的代码可能是开发人员所拥有的唯一文档。

        祝你好运。

        【讨论】:

        • 关于第三种方法,您确定链接ContinueWith 调用具有预期的结果吗?据我了解,第二个ContinueWith 将检查第一个延续的成功,而不是原始任务的成功。
        • 干杯@TheodorZoulias,这是一个敏锐的眼睛。固定。
        • 为流控制抛出异常对我来说是一种巨大的代码气味——它会降低你的性能。
        • 不,@IanKemp,这是一个非常古老的概念。编译器已经进化了。
        • 我真的很喜欢这里的方法 2。
        【解决方案8】:

        这是针对 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/

        【讨论】:

          【解决方案9】:

          async 方法不接受 out 参数的限制仅适用于编译器生成的异步方法,这些方法使用 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 参数将永远不会完成。

          【讨论】:

          • 在某些情况下这是一个非常好的解决方案。
          【解决方案10】:

          我认为像这样使用 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 包,它仍然可以工作。谢谢!
          【解决方案11】:

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

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

          【讨论】:

            【解决方案12】:

            您可以通过使用 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。这是一种反模式。谢谢!
            • 这个方法不是异步的。它没有回答问题。
            猜你喜欢
            • 2021-07-03
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多