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

时间:2017-04-21 07:34:57

标签: c# async-await task

我正在为Google Cloud 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];

使用转换

的表达式同步方法
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上发布这个,但我怀疑答案中的信息会更有用,除了这是否特别好。如果其他人不同意,我很乐意移动它。)

1 个答案:

答案 0 :(得分:21)

  

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

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

  

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

由于您正在使用库而应该使用ConfigureAwait(false) 在任何地方,使用强制执行的代码分析器可能是值得的 ConfigureAwait用法。这样做的是ReSharper pluginVS 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 / Thenbind 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);