Task .Convert 扩展方法是否有用或是否存在隐患?

我正在为Google Cloud API编写客户端库,这些库具有相当常见的异步辅助重载模式:

  • 做一些简短的同步工作来设置请求
  • 发出异步请求
  • 以简单的方式转换结果

目前我们正在使用异步方法,但是:

  • 转换await的结果最终会在优先级方面令人讨厌 – 我们最终需要(await foo.Bar().ConfigureAwait(false)).TransformToBaz()和括号很烦人。 使用两个语句可以提高可读性,但这意味着我们不能使用表达式身体方法。
  • 我们偶尔会忘记ConfigureAwait(false) – 这在某种程度上可以通过工具解决,但它仍然有点嗅觉

Task.ContinueWith听起来好主意,但我读过斯蒂芬克莱里的博客文章推荐反对它,原因似乎是合理的。 我们正在考虑为Task添加一个扩展方法,如下所示:

潜在的延伸方法

 public static async Task Convert( this Task task, Func projection) { var result = await task.ConfigureAwait(false); return projection(result); } 

然后我们可以非常简单地从同步方法中调用它,例如

 public async Task BarAsync() { var fooRequest = BuildFooRequest(); return FooAsync(fooRequest).Convert(foo => new Bar(foo)); } 

甚至:

 public Task BarAsync() => FooAsync(BuildFooRequest()).Convert(foo => new Bar(foo)); 

它看起来如此简单和有用,我有点惊讶,没有已经可用的东西。

作为我使用它来使表达式方法工作的一个例子,在Google.Cloud.Translation.V2代码中我有两种方法来翻译纯文本:一个接受一个字符串,一个接受多个字符串。 单字符串版本的三个选项(在参数方面有所简化):

常规异步方法

 public async Task TranslateTextAsync( string text, string targetLanguage) { GaxPreconditions.CheckNotNull(text, nameof(text)); var results = await TranslateTextAsync(new[] { text }, targetLanguage).ConfigureAwait(false); return results[0]; } 

表达式异步方法

 public async Task TranslateTextAsync( string text, string targetLanguage) => (await TranslateTextAsync(new[] { GaxPreconditions.CheckNotNull(text, nameof(text)) }, targetLanguage) .ConfigureAwait(false))[0]; 

使用Convert的表达式同步方法

 public Task TranslateTextAsync( string text, string targetLanguage) => TranslateTextAsync(new[] { GaxPreconditions.CheckNotNull(text, nameof(text)) }, targetLanguage) .Convert(results => results[0]); 

我个人更喜欢最后一个。

我知道这会改变validation的时间 – 在最后一个例子中,为text传递null值会立即抛出ArgumentNullException而为targetLanguage传递null值将返回一个错误的任务(因为TranslateTextAsync将异步失败)。 这是我愿意接受的差异。

我应该注意调度或性能的差异吗? (我们仍在构建两个状态机,因为Convert方法将创建一个。使用Task.ContineWith可以避免这种情况,但是在博客文章中提到了所有问题。 Convert方法可能会被更改为小心使用ContinueWith 。)

(我有点想在CodeReview上发布这个,但我怀疑答案中的信息会更有用,除了这是否特别好。如果其他人不同意,我很乐意移动它。)

转换await的结果最终会在优先级方面令人讨厌

我通常更喜欢引入局部变量,但正如您所指出的那样,这会阻止表达式方法。

我们偶尔会忘记ConfigureAwait(false) – 这在某种程度上可以解决工具问题

由于您正在使用库并且应该在任何地方使用ConfigureAwait(false) ,因此使用强制执行ConfigureAwait使用的代码分析器可能是值得的。 这是一个ReSharper插件和一个VS插件 。 不过,我自己也没试过。

Task.ContinueWith听起来好主意,但我读过斯蒂芬克莱里的博客文章推荐反对它,原因似乎是合理的。

如果使用ContinueWith ,则必须显式指定TaskScheduler.Default (这是ContinueWith等效于ConfigureAwait(false) ),并且还要考虑添加DenyChildAttach等标志。 IMO很难记住如何正确使用ContinueWith不是记住ConfigureAwait(false)

另一方面,虽然ContinueWith是一种低级,危险的方法,但如果你正确使用它,那么它可以为你提供较小的性能改进。 特别是,使用state参数可以为您节省委托分配。 这是TPL和其他Microsoft库通常采用的方法,但是对于大多数库而言,IMO会降低可维护性。

它看起来如此简单和有用,我有点惊讶,没有已经可用的东西。

你建议的Convert方法已经非正式地存在了 。 斯蒂芬并没有这么说,但我认为名字Then来自JavaScript世界 ,其中承诺是等同的任务(它们都是期货 )。

在旁注中, 斯蒂芬的博客文章将这个概念带到了一个有趣的结论。 Convert / Then是Future monad的bind ,因此它可以用于实现LINQ-over-futures。 Stephen Toub也为此发布了代码(此时已过时,但很有趣)。

我曾经想过几次将Then添加到我的AsyncEx库中,但每次都没有进行切割,因为它几乎与await相同。 它唯一的好处是允许方法链接解决优先级问题。 我认为它出于同样的原因在框架中不存在。

也就是说,实现自己的Convert方法肯定没有错。 这样做将避免使用括号/额外局部变量并允许表达式身体方法。

我知道这会改变validation的时间

这是我担心await async / await原因之一(我的博客文章有更多原因)。

在这种情况下,我认为这两种方式都没问题,因为“设置请求的简短同步工作”是一个先决条件检查,IMO并不重要抛出愚蠢的exception (因为它们不应该被捕获) 。

如果“简短的同步工作”更复杂 – 如果它可能抛出,或者可能在某人从现在开始重构之后合理抛出 – 那么我会使用async / await 。 您仍然可以使用Convert来避免优先级问题:

 public async Task TranslateTextAsync(string text, string targetLanguage) => await TranslateTextAsync(SomthingThatCanThrow(text), targetLanguage) .Convert(results => results[0]) .ConfigureAwait(false);