问题描述
我正在为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.)
推荐答案
我通常更喜欢引入局部变量,但正如您所指出的那样,它可以防止表达式约束方法.
I generally prefer to introduce a local var, but as you noted, that prevents expression-bodied methods.
由于您正在使用图书馆,并且应该使用ConfigureAwait(false)
在任何地方,使用强制执行的代码分析器可能都是值得的ConfigureAwait
用法.有一个 ReSharper插件和一个 VS插件.不过,我自己还没有尝试过.
Since you're working on a library and should use ConfigureAwait(false)
everywhere, it may be worthwhile to use a code analyzer that enforcesConfigureAwait
usage. There's a ReSharper plugin and a VS plugin that do this. I haven't tried them myself, though.
如果使用ContinueWith
,则必须明确指定TaskScheduler.Default
(这是ContinueWith
的等价物,ConfigureAwait(false)
),并考虑添加诸如DenyChildAttach
. IMO很难记住如何使用ContinueWith
比记住ConfigureAwait(false)
正确.
If you used ContinueWith
, you'd have to explicitly specifyTaskScheduler.Default
(this is the ContinueWith
equivalent ofConfigureAwait(false)
), and also consider adding flags such asDenyChildAttach
. 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.
您建议的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 theJavaScript world, where promises are the task equivalent (they areboth Futures).
另一方面,斯蒂芬的博客帖子将这个概念引入了一个有趣的结论. Convert
/Then
是 bind
对于Future monad ,因此它可以用于实现LINQ-over-futures.斯蒂芬·图布(Stephen Toub)也为此发布的代码(此时已过时,但很有趣).
On a side note, Stephen's blog post takes this concept to an interestingconclusion. Convert
/Then
is the bind
for the Future monad, so it canbe used to implement LINQ-over-futures. Stephen Toub has alsopublished 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 sameas just await
. Its only benefit is solving the precedence problem by allowing method chaining. I assume it doesn't exist in the framework for thesame reason.
也就是说,实施自己的应用肯定没有错Convert
方法.这样做可以避免括号/多余的局部变量,并允许使用表达形式的方法.
That said, there's certainly nothing wrong with implementing your ownConvert
method. Doing so will avoid the parenthesis / extra localvariable and allow for expression-bodied methods.
这是我厌倦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< T> .Convert< TResult>扩展方法有用还是有隐患?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!