如何识别方法实现是否标记为异步/可以异步调用,仅基于其使用 Roslyn 的接口?

How to identify if the method implementation is marked as async / can be called asynchronously, based only on its interface using Roslyn?

背景信息

我正在为 Visual Studio 构建基于 Roslyn 的 CodeFix,它处理 class 未实现接口(或缺少该接口的一部分)的情况。

接口通常是第三方代码,例如微软的 IDocumentClient.

然后我创建了该接口的一个实现,其中对方法和属性的调用是 'wrapped' 通过处理它们的实际执行由 3 个辅助方法中最相关的候选方法,作为装饰实现的一部分。这些辅助方法处理不同 return 类型的场景,包括 void return、非任务类型和通用任务类型。

辅助方法调用 Polly 库;在 returns generic Task types 的助手的情况下,特别是 Polly ExecuteAsync 方法,它执行传递的方法委托的执行,并根据用户指定的行为处理异常(重试、断路器等)。

我的项目代码可以在 Github、Polly.Contrib.Decorator.

上找到

问题

我需要能够通过接口声明中包含的信息来识别我正在创建的方法是否是异步的。

这将决定两件事:

  1. 如果我的实现应该用async修饰符标记。

  2. 如果实现可以被异步调用,允许我决定我的方法实现——它被包装了——可以然后,如果它应该 由我的包装代码异步处理。

我不能依赖任何其他外部信息。

我考虑过的

我看过使用 return 类型的方法来确定它是否是 Task,但在某些情况下,它的接口中的方法声明可能是 'returning void',即使它的实际实现标有 async 修饰符或可以异步方式调用。

检查Async后缀的名称显然不可靠;不是每个人都遵循这样的约定。

问题

是否有可靠的方法来识别方法实现是否异步,即它是否应该用 async 装饰,是否可以仅基于其接口声明使用 Roslyn 进行异步处理?

(请参考评论讨论,说明本题的演变)

Roslyn 有一个内部 IsAwaitableNonDynamic extension method 可以满足您的需求。

您可以复制它:

    /// <summary>
    /// If the <paramref name="symbol"/> is a method symbol, returns <see langword="true"/> if the method's return type is "awaitable", but not if it's <see langword="dynamic"/>.
    /// If the <paramref name="symbol"/> is a type symbol, returns <see langword="true"/> if that type is "awaitable".
    /// An "awaitable" is any type that exposes a GetAwaiter method which returns a valid "awaiter". This GetAwaiter method may be an instance method or an extension method.
    /// </summary>
    public static bool IsAwaitableNonDynamic(this ISymbol symbol, SemanticModel semanticModel, int position)
    {
        IMethodSymbol methodSymbol = symbol as IMethodSymbol;
        ITypeSymbol typeSymbol = null;

        if (methodSymbol == null)
        {
            typeSymbol = symbol as ITypeSymbol;
            if (typeSymbol == null)
            {
                return false;
            }
        }
        else
        {
            if (methodSymbol.ReturnType == null)
            {
                return false;
            }
        }

        // otherwise: needs valid GetAwaiter
        var potentialGetAwaiters = semanticModel.LookupSymbols(position,
                                                               container: typeSymbol ?? methodSymbol.ReturnType.OriginalDefinition,
                                                               name: WellKnownMemberNames.GetAwaiter,
                                                               includeReducedExtensionMethods: true);
        var getAwaiters = potentialGetAwaiters.OfType<IMethodSymbol>().Where(x => !x.Parameters.Any());
        return getAwaiters.Any(VerifyGetAwaiter);
    }

    private static bool VerifyGetAwaiter(IMethodSymbol getAwaiter)
    {
        var returnType = getAwaiter.ReturnType;
        if (returnType == null)
        {
            return false;
        }

        // bool IsCompleted { get }
        if (!returnType.GetMembers().OfType<IPropertySymbol>().Any(p => p.Name == WellKnownMemberNames.IsCompleted && p.Type.SpecialType == SpecialType.System_Boolean && p.GetMethod != null))
        {
            return false;
        }

        var methods = returnType.GetMembers().OfType<IMethodSymbol>();

        // NOTE: (vladres) The current version of C# Spec, §7.7.7.3 'Runtime evaluation of await expressions', requires that
        // NOTE: the interface method INotifyCompletion.OnCompleted or ICriticalNotifyCompletion.UnsafeOnCompleted is invoked
        // NOTE: (rather than any OnCompleted method conforming to a certain pattern).
        // NOTE: Should this code be updated to match the spec?

        // void OnCompleted(Action) 
        // Actions are delegates, so we'll just check for delegates.
        if (!methods.Any(x => x.Name == WellKnownMemberNames.OnCompleted && x.ReturnsVoid && x.Parameters.Length == 1 && x.Parameters.First().Type.TypeKind == TypeKind.Delegate))
        {
            return false;
        }

        // void GetResult() || T GetResult()
        return methods.Any(m => m.Name == WellKnownMemberNames.GetResult && !m.Parameters.Any());
    }

TL;DR

潜在的问题是在接口中包装方法的库应该如何调用这些方法——是否使用 await。不仅是 async 是否在实现上被声明为不可检测的(从接口,调用站点的上下文之外),而且它不是确定的,为此目的。

  • (1) async 关键字是否出现在方法实现中 足以确定对其的调用是否可以或应该使用 await .
  • (2) 一个方法 可以 await 编辑当且仅当它的 return 类型是 await 可用的(这足以确定)。
  • (3) async void方法不改变以上结论; async void 方法不会 运行 异步,因为你不能用 await.
  • 调用它

(1) async 关键字是否出现在方法实现中 足以确定对其的调用是否可以或应该使用await.

async 不是方法签名的正式部分。所以在接口中找不到 async。所以你不能从一个接口中确定,如果接口的原始作者打算用 async 关键字编写方法实现或用 await.

调用

但是被调用的方法是否写成async关键字其实并不是决定(/甚至是充分决定)的因素,一个方法could/should是否被[=10调用=].在没有 async 关键字的情况下编写方法是有效的 return awaitables:

[一个]async-await eilision, as used extensively by Polly
[b] 接口主要用于 I/O 上的实现,因此使用 awaitable return 类型声明,但有时您可能还希望为此编写一个内存中(如此同步) implementation/interceptor。 Discussion of common case: sync in-memory cache around async I/O [c] 出于测试目的,使用内存中(同步)存根

消除一些异步依赖

async 不是方法签名的一部分实际上很好,因为它允许我们偶尔实现同步实现,如上所述。)

(2) 当且仅当它的 return 类型是可等待的(这足以确定)时,方法才能被等待;即 return 类型是 TaskTask<TResult>has a suitable GetAwaiter() method

唯一确定一个方法调用能否await编辑的唯一因素是它的return类型是否await可用。

(3)async void方法不改变以上结论

这详细地解决了 async void 方法,因为问题中的评论说 return 类型的方法可能不够,大概是因为 void 无法区分(在界面中)来自 async void.

出发点是async void方法不能等待,原因很简单,虽然用async关键字写的,但不return 任何可以被 await 编辑的类型。

这是否使我们的结论 (2) 无效? (我们可以唯一地使用方法 return 是否 await 来确定如何调用它)。我们是否 失去了 运行 async void 异步方法的一些能力,因为我们不能 await 它们?

具体来说:说接口背后的方法是async void Foo(),但是我们从接口上知道的是void Foo()。如果我们只调用它 Foo(),我们是否会失去从 Foo() 到 运行 的一些异步能力?

答案是否定的,因为 async 方法的运作方式。 async void 方法的行为与使用 await 调用的任何 async Task/Task<T> 方法一样:它们 运行 同步直到它们的第一个内部 await;然后他们 return(void,或代表他们承诺完成的 Task),并将被调用方法的剩余部分(第一个 await 之后的部分)安排为延续。该延续是 运行 异步的部分,在 awaited 完成后。 (这是一个相当简洁的描述,但它在博客上广为流传;example discussion。)

换句话说:async void方法的某些部分将运行异步的决定因素不是它被await调用,而是在[=145] =]它的 正文,它有一个 await 后面有一些有意义的工作。

(3b) async void 上的另一个角度

既然最重要的问题(为了 Polly.Contrib.Decorator 的目的)是我们应该如何 调用 包装方法,我们可以 运行 另一种思想实验围绕 async void 方法。如果我们可以(以某种方式)确定接口后面的 void 方法已被声明 async void 会怎么样?我们会用不同的方式称呼它吗?

回到我们的例子,async void Foo()。我们的选择是什么?我们可以直接 Foo()Task.Run(() => Foo()).

正如 Stephen Cleary 所说,a library shouldn't use Task.Run() on a caller's behalf。这样做意味着库正在选择将工作卸载到后台线程,拒绝调用者的选择。 (注意:根据上面关于 async void 方法如何操作的讨论,这仅适用于被调用方法中第一个 await 之前的工作。)

所以:即使我们知道接口中void Foo()背后的方法是async void Foo()Polly.Contrib.Decorator仍然应该只调用Foo()。如果用户想立即将工作卸载到不同的线程(例如,他们想将其从 GUI 线程卸载),他们将(在引入 Polly.Contrib.Decorator 之前)无论如何调用该接口方法 Task.Run(() => ...) .我们不需要再添加一个。

这符合 Polly 遵循的原则,我建议其他代理变形库应遵循:它对用户代理的影响应该最小(除了声明的预期效果)运行。


以上所有的关键是async关键字不会(本身)创建一个方法运行 异步 甚至并发,所以不是硬道理。 async 关键字只允许编译器在 await 语句处将一个方法分成一系列块;这种方法 运行 的块 2..n(异步;在不同的时间)作为前面 awaited 调用完成后的延续。调用者是(async void 方法除外)returned 一个 Task,这是一个 'promise',它将在 awaited 方法完成时完成。

被return编辑Task(或其他实现GetAwaiter()的东西)的事实是它是否可以用await调用的决定因素:如果returned 类型的方法实现了这个可等待的模式,它可以被等待。

存在 async/await 省略,特别是 sync-cache-over-async-io 模式,表明被调用方法的 return 类型是关键,而不是实现是否使用async 关键字。