【问题标题】:Would a Task<T>.Convert<TResult> extension method be useful or does it have hidden dangers?Task<T>.Convert<TResult> 扩展方法有用还是有隐患?
【发布时间】:2017-04-21 07:34:57
【问题描述】:

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

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

目前我们为此使用异步方法,但是:

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

Task&lt;TResult&gt;.ContinueWith 听起来是个好主意,但我读过Stephen Cleary's blog post 反对它,理由似乎很合理。我们正在考虑为Task&lt;T&gt; 添加一个扩展方法,如下所示:

可能的扩展方法

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


【解决方案1】:

转换 await 的结果在优先级方面很烦人

我通常更喜欢引入本地变量,但正如您所指出的,这会阻止表达式主体方法。

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

由于您正在开发一个库并且应该使用ConfigureAwait(false) 在任何地方,使用强制执行的代码分析器可能是值得的 ConfigureAwait 用法。有一个ReSharper plugin 和一个VS plugin 可以做到这一点。不过,我自己还没有尝试过。

Task&lt;TResult&gt;.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/Thenbind for the Future monad,所以可以 用于实现 LINQ-over-futures。斯蒂芬·图布也 published code for this(此时已过时,但很有趣)。

我曾多次考虑将Then 添加到我的 AsyncEx 库中, 但每次它都没有成功,因为它几乎是一样的 就像await。它唯一的好处是通过允许方法链接来解决优先级问题。我认为它在框架中不存在 同样的原因。

也就是说,实现你自己的当然没有错 Convert 方法。这样做将避免括号/额外的本地 变量并允许使用表达式主体的方法。

我知道这会改变验证的时间

这是我成为wary of eliding async/await 的原因之一(我的博文中有更多原因)。

在这种情况下,我认为无论哪种方式都可以,因为“设置请求的简短同步工作”是一个先决条件检查,而 IMO 将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);

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多