Task <T> .Convert <TResult>扩展方法是否有用或是否存在隐患?

Jon*_*eet 33 c# task async-await

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

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

目前我们正在使用异步方法,但是:

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

Task<TResult>.ContinueWith听起来是个好主意,但我读过Stephen Cleary的博客文章推荐反对它,原因看似合理.我们正在考虑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);
}
Run Code Online (Sandbox Code Playgroud)

然后我们可以非常简单地从同步方法中调用它,例如

public async Task<Bar> BarAsync()
{
    var fooRequest = BuildFooRequest();
    return FooAsync(fooRequest).Convert(foo => new Bar(foo));
}
Run Code Online (Sandbox Code Playgroud)

甚至:

public Task<Bar> BarAsync() =>
    FooAsync(BuildFooRequest()).Convert(foo => new Bar(foo));
Run Code Online (Sandbox Code Playgroud)

它看起来如此简单和有用,我有点惊讶没有已经可用的东西.

作为我使用它来使表达式方法工作的一个例子,在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];
}
Run Code Online (Sandbox Code Playgroud)

表达式异步方法

public async Task<TranslationResult> TranslateTextAsync(
    string text, string targetLanguage) =>
    (await TranslateTextAsync(new[] { GaxPreconditions.CheckNotNull(text, nameof(text)) }, targetLanguage)
        .ConfigureAwait(false))[0];
Run Code Online (Sandbox Code Playgroud)

使用Convert的表达式同步方法

public Task<TranslationResult> TranslateTextAsync(
    string text, string targetLanguage) =>
    TranslateTextAsync(new[] { GaxPreconditions.CheckNotNull(text, nameof(text)) }, targetLanguage)
        .Convert(results => results[0]);
Run Code Online (Sandbox Code Playgroud)

我个人更喜欢最后一个.

我知道这会改变验证的时间 - 在最后的例子中,传递一个nulltext将立即抛出一个ArgumentNullException而传递一个nulltargetLanguage将返回一个故障任务(因为TranslateTextAsync将异步失败).这是我愿意接受的差异.

我应该注意调度或性能的差异吗?(我们仍在构建两个状态机,因为该Convert方法将创建一个.使用Task.ContineWith可以避免这种情况,但是在博客文章中提到了所有问题.该Convert方法可能会被更改为ContinueWith谨慎使用.)

(我有点想在CodeReview上发布这个,但我怀疑答案中的信息将更有用,除了这是否是一个好主意.如果其他人不同意,我很乐意移动它.)

Ste*_*ary 23

转换await的结果最终会在优先级方面令人讨厌

我通常更喜欢引入局部变量,但正如您所指出的那样,这会阻止表达式方法.

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

由于您正在使用库并且应该ConfigureAwait(false) 在任何地方使用,因此使用强制使用的代码分析器可能是值得的 ConfigureAwait.这是一个ReSharper插件和一个VS插件.不过,我自己也没试过.

Task<TResult>.ContinueWith 听起来是个好主意,但我读过Stephen Cleary的博客文章推荐反对它,原因看似合理.

如果您使用过ContinueWith,则必须明确指定 TaskScheduler.Default(这ContinueWith相当于 ConfigureAwait(false)),并且还要考虑添加标记,例如 DenyChildAttach.IMO很难记住如何使用ContinueWith 正确而不是记住ConfigureAwait(false).

另一方面,虽然ContinueWith是一种低级,危险的方法,但如果你正确使用它,那么它可以为你提供较小的性能改进.特别是,使用该state参数可以为您节省委托分配.这是TPL和其他Microsoft库通常采用的方法,但是对于大多数库而言,IMO会降低可维护性.

它看起来如此简单和有用,我有点惊讶没有已经可用的东西.

Convert你建议的方法非正式地存在Then.斯蒂芬并没有这么说,但我认为这个名字Then来自JavaScript世界,承诺是等同于任务的(它们都是Futures).

在旁注中,斯蒂芬的博客文章将这个概念带到了一个有趣的结论.Convert/ Thenbind未来的monad,因此它可用于实现LINQ-over-futures.Stephen Toub也 为此发布了代码(相当过时,但很有趣).

我曾经想过几次添加Then到我的AsyncEx库中,但每次都没有进行切割,因为它与刚刚完全一样await.它唯一的好处是允许方法链接解决优先级问题.我认为它出于同样的原因在框架中不存在.

也就是说,实现自己的Convert方法肯定没有错 .这样做将避免使用括号/额外局部变量并允许表达式身体方法.

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

这是我担心躲避asyncawait的原因之一/(我的博客文章涉及更多原因).

在这种情况下,我认为这两种方式都没问题,因为"设置请求的简短同步工作"是一个先决条件检查,IMO并不重要抛出愚蠢的异常(因为它们不应该被捕获) .

如果"简短的同步工作"更复杂 - 如果它可以抛出,或者可以合理地抛出一个人从现在开始重构它 - 那么我会使用async/ await.您仍然可以使用Convert以避免优先级问题:

public async Task<TranslationResult> TranslateTextAsync(string text, string targetLanguage) =>
  await TranslateTextAsync(SomthingThatCanThrow(text), targetLanguage)
  .Convert(results => results[0])
  .ConfigureAwait(false);
Run Code Online (Sandbox Code Playgroud)