Task< T> .Convert< TResult>扩展方法有用还是有隐患? [英] Would a Task<T>.Convert<TResult> extension method be useful or does it have hidden dangers?

查看:65
本文介绍了Task< T> .Convert< TResult>扩展方法有用还是有隐患?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm writing client libraries for Google Cloud APIs which have a fairly common pattern for async helper overloads:

  • 进行一些简短的同步工作以建立请求
  • 发出异步请求
  • 以简单的方式转换结果

当前,我们正在为此使用异步方法,但是:

Currently we're using async methods for that, but:

  • 根据优先级来改变await的结果最终会令人讨厌-我们最终需要(await foo.Bar().ConfigureAwait(false)).TransformToBaz(),并且括号令人讨厌.使用两个语句可以提高可读性,但是这意味着我们不能使用基于表达式的方法.
  • 我们偶尔会忘记ConfigureAwait(false)-在某种程度上可以通过工具解决,但是仍然有点味道
  • Transforming the result of await ends up being annoying in terms of precedence - we end up needing (await foo.Bar().ConfigureAwait(false)).TransformToBaz() and the brackets are annoying. Using two statements improves readability, but means we can't use an expression-bodied method.
  • We occasionally forget ConfigureAwait(false) - this is solvable with tooling to some extent, but it's still a bit of a smell

Task<TResult>.ContinueWith 听起来像一个好主意,但我已阅读 Stephen Cleary的博客文章对此提出了建议,其原因似乎很合理.我们正在考虑为Task<T>添加一个扩展方法,如下所示:

Task<TResult>.ContinueWith sounds like a good idea, but I've read Stephen Cleary's blog post recommending against it, and the reasons seem sound. We're considering adding an extension method for Task<T> like this:

潜在的扩展方式

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);
}

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

We can then call this from a synchronous method really simply, e.g.

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));

它看起来是如此简单和有用,令我有些惊讶的是还没有可用的东西.

It seems so simple and useful that I'm slightly surprised there isn't something already available.

作为我在哪里使用此表达式来使表达式结合的方法起作用的示例,在Google.Cloud.Translation.V2代码中,我有两种方法来转换纯文本:一种采用单个字符串,而另一种则采用多个字符串.单字符串版本的三个选项是(在参数方面进行了一些简化):

As an example of where I'd use this to make an expression-bodied method work, in the Google.Cloud.Translation.V2 code I have two methods to translate plain text: one takes a single string and one takes multiple strings. The three options for the single-string version are (simplified somewhat in terms of parameters):

常规异步方法

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]);

我个人更喜欢其中的最后一个.

I personally prefer the last of these.

我知道这会改变验证的时间-在最后一个示例中,为text传递null值将立即引发ArgumentNullException,而为targetLanguage传递null值将返回错误的任务(因为TranslateTextAsync将异步失败).我愿意接受这一点.

I'm aware that this changes the timing of the validation - in the final example, passing a null value for text will immediately throw an ArgumentNullException whereas passing a null value for targetLanguage will return a faulted task (because TranslateTextAsync will fail asynchronously). That's a difference I'm willing to accept.

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

Are there differences in scheduling or performance that I should be aware of? (We're still constructing two state machines, because the Convert method will create one. Using Task.ContineWith would avoid that, but has all the problems mentioned in the blog post. The Convert method could potentially be changed to use ContinueWith carefully.)

(我有点想将其发布到CodeReview上,但是我怀疑答案中的信息将在更广泛的意义上有用,这是否是一个特别好的主意.如果其他人不同意,我很乐意提出.)

(I'm somewhat tempted to post this on CodeReview, but I suspect the information in the answers will be more generally useful beyond whether this is specifically a good idea. If others disagree, I'm happy to move it.)

推荐答案

改变等待结果的结果在优先级方面变得很烦人

Transforming the result of await ends up being annoying in terms of precedence

我通常更喜欢引入局部变量,但正如您所指出的那样,它可以防止表达式约束方法.

I generally prefer to introduce a local var, but as you noted, that prevents expression-bodied methods.

我们有时会忘记ConfigureAwait(false)-使用工具可以解决此问题

We occasionally forget ConfigureAwait(false) - this is solvable with tooling to some extent

由于您正在使用图书馆,并且应该使用ConfigureAwait(false) 在任何地方,使用强制执行的代码分析器可能都是值得的 ConfigureAwait用法.有一个 ReSharper插件和一个

Since you're working on a library and should use ConfigureAwait(false) everywhere, it may be worthwhile to use a code analyzer that enforces ConfigureAwait usage. There's a ReSharper plugin and a VS plugin that do this. I haven't tried them myself, though.

Task<TResult>.ContinueWith听起来是个好主意,但是我已经阅读了Stephen Cleary的博客文章,建议不要这样做,而且原因似乎很合理.

Task<TResult>.ContinueWith sounds like a good idea, but I've read Stephen Cleary's blog post recommending against it, and the reasons seem sound.

如果使用ContinueWith,则必须明确指定 TaskScheduler.Default(这是ContinueWith的等价物, ConfigureAwait(false)),并考虑添加诸如 DenyChildAttach. IMO很难记住如何使用ContinueWith 比记住ConfigureAwait(false)正确.

If you used ContinueWith, you'd have to explicitly specify TaskScheduler.Default (this is the ContinueWith equivalent of ConfigureAwait(false)), and also consider adding flags such as DenyChildAttach. IMO it's harder to remember how to use ContinueWith correctly than it is to remember ConfigureAwait(false).

另一方面,虽然ContinueWith是一种低级的危险方法,但如果正确使用它,则可以会给您带来较小的性能改进.特别是,使用state参数可以节省您的委托分配.这是TPL和其他Microsoft库通常采用的方法,但是IMO降低了大多数库的可维护性.

On the other hand, while ContinueWith is a low-level, dangerous method, if you use it correctly then it can give you minor performance improvements. In particular, using the state parameter can save you a delegate allocation. This is the approach commonly taken by the TPL and other Microsoft libraries, but IMO it lowers the maintainability too much for most libraries.

它看起来是如此简单和有用,令我有些惊讶的是还没有可用的东西.

It seems so simple and useful that I'm slightly surprised there isn't something already available.

您建议的Convert方法具有非正式地以Then 的形式存在. Stephen并未这么说,但我认为名称Then来自 JavaScript世界,其中promise是任务的等价物(它们是 都期货).

The Convert method you suggest has existed informally as Then. Stephen doesn't say so, but I assume that the name Then is from the JavaScript world, where promises are the task equivalent (they are both Futures).

另一方面,斯蒂芬的博客帖子将这个概念引入了一个有趣的 结论. Convert/Then bind对于Future monad ,因此它可以 用于实现LINQ-over-futures.斯蒂芬·图布(Stephen Toub)也 为此发布的代码(此时已过时,但很有趣).

On a side note, Stephen's blog post takes this concept to an interesting conclusion. Convert/Then is the bind for the Future monad, so it can be used to implement LINQ-over-futures. Stephen Toub has also published code for this (rather dated at this point, but interesting).

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

I have thought a few times about adding Then to my AsyncEx library, but each time it didn't make the cut because it's pretty much the same as just await. Its only benefit is solving the precedence problem by allowing method chaining. I assume it doesn't exist in the framework for the same reason.

也就是说,实施自己的应用肯定没有错 Convert方法.这样做可以避免括号/多余的局部 变量,并允许使用表达形式的方法.

That said, there's certainly nothing wrong with implementing your own Convert method. Doing so will avoid the parenthesis / extra local variable and allow for expression-bodied methods.

我知道这会更改验证时间

I'm aware that this changes the timing of the validation

这是我厌倦async/await (我的博客文章有更多原因).

This is one of the reasons that I'm wary of eliding async/await (my blog post goes into more reasons).

在这种情况下,我认为这两种方法都很好,因为建立请求的简短同步工作"是前提条件检查,而IMO在

In this case, I think it's fine either way, since the "brief synchronous work to set up a request" is a preconditions check, and IMO it doesn't matter where boneheaded exceptions are thrown (because they shouldn't be caught anyway).

如果简短同步工作"更复杂-如果它可能在某人重构一年后抛出,或者可能在某人对其进行合理重构后抛出,那么我将使用async/await.您仍然可以使用Convert来避免出现优先级问题:

If the "brief synchronous work" was more complex - if it was something that could throw, or could reasonably throw after someone refactors it a year from now - then I would use async/await. You could still use Convert to avoid the precedence issue:

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;扩展方法有用还是有隐患?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆