问题描述
我正在为Google Cloud API编写客户端库,这些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];
使用CONVERT的表达式正文同步方法
public Task<TranslationResult> TranslateTextAsync(
string text, string targetLanguage) =>
TranslateTextAsync(new[] { GaxPreconditions.CheckNotNull(text, nameof(text)) }, targetLanguage)
.Convert(results => results[0]);
我个人更喜欢最后一个。
我知道这会改变验证的时间--在最后一个示例中,为text
传递null
值将立即抛出ArgumentNullException
,而为targetLanguage
传递null
值将返回出错的任务(因为TranslateTextAsync
将异步失败)。这是我愿意接受的差别。
在日程安排或性能方面是否有我应该注意的差异?(我们仍在构造两个状态机,因为Convert
方法将创建一个状态机。使用Task.ContineWith
可以避免这种情况,但存在博客文章中提到的所有问题。Convert
方法可能会更改为谨慎使用ContinueWith
。)
(我有点想在CodeReview上发布这篇文章,但我怀疑答案中的信息除了这是否确实是一个好主意之外,通常还会更有用。如果其他人不同意,我很乐意提出。)
推荐答案
转换等待的结果最终会使人讨厌优先级
我通常更喜欢引入局部变量,但正如您所指出的,这会阻止表达式体方法。
我们偶尔会忘记
ConfigureAwait(false)
-这在某种程度上是可以通过工具解决的
由于您正在使用库,应使用ConfigureAwait(false)
在任何地方,可能都值得使用代码分析器来强制
ConfigureAwait
用法。有一个ReSharper plugin和一个VS plugin可以做到这一点。不过,我自己还没有试过。
如果使用
Task<TResult>.ContinueWith
听起来是个好主意,但我读过Stephen Cleary的博客文章建议不要这么做,理由似乎很充分。
ContinueWith
,则必须显式指定
(这是的等价物
ConfigureAwait(false)
),并考虑添加诸如
DenyChildAttach
。我很难记住如何使用ContinueWith
比记住ConfigureAwait(false)
更准确。
另一方面,虽然ContinueWith
是一种低级的、危险的方法,但如果您正确使用它,可以给您带来轻微的性能改进。特别是,使用state
参数可以节省委托分配。这是TPL和其他Microsoft库通常采用的方法,但它大大降低了大多数库的可维护性。
它看起来如此简单和有用,以至于我有点惊讶还没有可用的东西。
您建议的Convert
方法有existed informally as Then
。史蒂芬没有这么说,但我假设这个名字Then
来自
JavaScript世界,承诺是任务的等价物(它们是
两者Futures)。
Convert
/Then
是bind
for the Future monad,因此
用于实现LINQ-Over-Futures。斯蒂芬·图布也
published code for this(此时已过时,但很有趣)。
我曾多次考虑将Then
添加到我的AsyncEx库中,
但每次都没有入围,因为它几乎是一样的
仅为await
。它唯一的好处是通过允许方法链接来解决优先级问题。我假设它不存在于
原因相同。
这就是说,实现您自己的当然没有错
Convert
方法。这样做可以避免括号/额外的局部
变量并允许使用以表达式为主体的方法。
我知道这会更改验证的时间
这是我wary of eliding async
/await
的原因之一(我的博客帖子中有更多原因)。
在这种情况下,我认为这两种方式都很好,因为"设置请求的简短同步工作"是一个前提检查,而在我看来,抛出boneheaded exceptions在哪里并不重要(因为它们无论如何都不应该被捕获)。
如果"简短的同步工作"更复杂--如果它是可以抛出的东西,或者在一年后有人重新考虑它后可以合理抛出的东西--那么我会使用async
/await
。您仍然可以使用Convert
来避免优先级问题:
public async Task<TranslationResult> TranslateTextAsync(string text, string targetLanguage) =>
await TranslateTextAsync(SomthingThatCanThrow(text), targetLanguage)
.Convert(results => results[0])
.ConfigureAwait(false);
这篇关于Task&;lt;T&;gt;.Convert&;lt;TResult&;gt;扩展方法是否有用,或者它是否存在隐患?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!