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