【发布时间】:2017-04-21 07:34:57
【问题描述】:
我正在为 Google Cloud API 编写客户端库,这些 API 具有相当常见的异步助手重载模式:
- 做一些简短的同步工作来设置请求
- 发出异步请求
- 以简单的方式转换结果
目前我们为此使用异步方法,但是:
- 转换 await 的结果最终在优先级方面很烦人 - 我们最终需要
(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 上发布此内容,但我怀疑答案中的信息将更普遍有用,而不是这是否是一个特别好的主意。如果其他人不同意,我很乐意移动它。)
【问题讨论】:
-
您正在向管道添加另一个状态机。肯定会有性能损失。
(await foo.Bar().ConfigureAwait(false)).TransformToBaz()怎么了? -
@PauloMorgado:如何添加另一个状态机?它从异步方法
BarAsync中的一个状态机(我刚刚注意到没有明确声明为异步)到从 non- 调用异步Convert方法(具有状态机)异步BarAsync方法。 “出了什么问题”是:1)它不像IMO那样可读; 2)很容易忘记ConfigureAwait(false),而该方法将其放在一个地方。 -
当然编写的代码是供人类阅读的,但是,如果你引入
Convert方法,你就没有在教育任何人。在运行时执行数百万次的单行代码将改变程序的性能和资源分配。现在,如果使用您的 API 通常需要转换,为什么不使用选择器函数添加重载? -
@PauloMorgado:因为它是一个 RPC API - 它没有重载的概念,并且添加额外的 RPC 对 IMO 没有意义。将重载添加到公共客户端库 API 是为了使其尽可能有用,而不会在 RPC API 中引入冗余。现在,你最初说它正在将另一个状态机引入管道 - 你支持吗?如果是这样,我肯定错过了一些东西。
-
你对
Convert的实现。通过使用async关键字,引入了状态机。看看Eduasync。 :D
标签: c# async-await task