我正在为Google Cloud API编写客户端库,这些库具有相当常见的异步辅助重载模式:
目前我们正在使用异步方法,但是:
(await foo.Bar().ConfigureAwait(false)).TransformToBaz()
并且括号很烦人。使用两个语句可以提高可读性,但这意味着我们不能使用表达式方法。ConfigureAwait(false)
- 这在某种程度上可以通过工具解决,但它仍然有点气味 Task<TResult>.ContinueWith
听起来不错,但我已经阅读Stephen Cleary's blog post推荐反对它,原因似乎很合理。我们正在考虑为此Task<T>
添加扩展方法:
潜在的扩展方法
public static async Task<TResult> Convert<TSource, TResult>(
this Task<TSource> task, Func<TSource, TResult> projection)
{
var result = await task.ConfigureAwait(false);
return projection(result);
}
然后我们可以非常简单地从同步方法中调用它,例如
public async Task<Bar> BarAsync()
{
var fooRequest = BuildFooRequest();
return FooAsync(fooRequest).Convert(foo => new Bar(foo));
}
甚至:
public Task<Bar> BarAsync() =>
FooAsync(BuildFooRequest()).Convert(foo => new Bar(foo));
看起来如此简单和有用,我有点惊讶,因为没有可用的东西。
作为我使用它来使表达式方法工作的一个例子,在Google.Cloud.Translation.V2
代码中我有两种方法来翻译纯文本:一个接受一个字符串,一个接受多个字符串。单字符串版本的三个选项(在参数方面有所简化):
常规异步方法
public async Task<TranslationResult> 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<TranslationResult> TranslateTextAsync(
string text, string targetLanguage) =>
(await TranslateTextAsync(new[] { GaxPreconditions.CheckNotNull(text, nameof(text)) }, targetLanguage)
.ConfigureAwait(false))[0];
使用转换
的表达式同步方法public Task<TranslationResult> TranslateTextAsync(
string text, string targetLanguage) =>
TranslateTextAsync(new[] { GaxPreconditions.CheckNotNull(text, nameof(text)) }, targetLanguage)
.Convert(results => results[0]);
我个人更喜欢最后一种。
我知道这会改变验证的时间 - 在最后一个示例中,为null
传递text
值会立即抛出ArgumentNullException
,而传递{{1} null
的值将返回一个错误的任务(因为targetLanguage
将异步失败)。这是我愿意接受的差异。
我应该注意调度或性能的差异吗? (我们仍在构建两个状态机,因为TranslateTextAsync
方法会创建一个。使用Convert
可以避免这种情况,但是在博文中提到了所有问题。Task.ContineWith
方法可能会被更改为谨慎使用Convert
。)
(我有点想在CodeReview上发布这个,但我怀疑答案中的信息会更有用,除了这是否特别好。如果其他人不同意,我很乐意移动它。)
答案 0 :(得分:21)
转换await的结果最终会在优先级方面令人讨厌
我通常更喜欢引入局部变量,但正如您所指出的那样,这会阻止表达式方法。
我们偶尔会忘记
ConfigureAwait(false)
- 这在某种程度上可以通过工具解决
由于您正在使用库而应该使用ConfigureAwait(false)
在任何地方,使用强制执行的代码分析器可能是值得的
ConfigureAwait
用法。这样做的是ReSharper plugin和VS plugin。不过,我自己还没试过。
Task<TResult>.ContinueWith
听起来是个好主意,但我已经阅读了Stephen Cleary的博客文章推荐反对它,原因看起来很合理。
如果您使用ContinueWith
,则必须明确指定
TaskScheduler.Default
(这是ContinueWith
相当于
ConfigureAwait(false)
),并考虑添加标记,如
DenyChildAttach
。 IMO很难记住如何使用ContinueWith
正确而不是记住ConfigureAwait(false)
。
另一方面,虽然ContinueWith
是一种低级别,危险的方法,但如果您正确使用它,则可以为您提供较小的性能改进。特别是,使用state
参数可以为您节省委托分配。这是TPL和其他Microsoft库通常采用的方法,但是对于大多数库来说,IMO会降低可维护性。
看起来如此简单和实用,我感到有些惊讶,因为现有的东西还没有。
您建议的Convert
方法有existed informally as Then
。斯蒂芬并没有这么说,但我认为the name Then
is from the
JavaScript world,承诺是等同的任务(他们是
两个Futures)。
另一方面,Stephen's blog post将这个概念变得有趣
结论。 Convert
/ Then
是bind
for the Future monad,因此可以
用于实现LINQ-over-futures。 Stephen Toub也有
published code for this(此时相当陈旧,但有趣)。
我想过几次将Then
添加到我的AsyncEx库中,
但每次它都没有削减,因为它几乎是一样的
只是await
。它唯一的好处是允许方法链接解决优先级问题。我认为它并不存在于框架中
同样的原因。
那就是说,实施你自己的确没有错
Convert
方法。这样做将避免括号/额外的本地
变量并允许表达式方法。
我知道这会改变验证时间
这是我wary of eliding async
/await
的原因之一(我的博客文章涉及更多原因)。
在这种情况下,我认为它很好,因为&#34;简短的同步工作来设置请求&#34;是一个先决条件检查,IMO并不重要boneheaded exceptions抛出的地方(因为它们不应该被捕获)。
如果&#34;简短同步工作&#34;更复杂 - 如果它是一个可以扔掉的东西,或者可以在一个人从现在开始重构它之后合理抛出 - 那么我会使用async
/ await
。您仍然可以使用Convert
来避免优先问题:
public async Task<TranslationResult> TranslateTextAsync(string text, string targetLanguage) =>
await TranslateTextAsync(SomthingThatCanThrow(text), targetLanguage)
.Convert(results => results[0])
.ConfigureAwait(false);