首页 文章

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

提问于
浏览
0

Background Information

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

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

然后我创建了一个该接口的实现,其中对方法和属性的调用是通过由3个辅助方法中最相关的候选者处理它们的实际执行来“封装”的,作为装饰实现的一部分 . 这些帮助器方法处理不同返回类型的方案,包括void返回,非Task类型和通用Task类型 .

辅助方法调用 Polly 库;对于返回通用Task类型的helper,特别是 Polly ExecuteAsync 方法,它执行传递的方法委托,并根据用户指定的行为处理异常(重试,断路器等) .

我的项目代码可以在Github上找到,Polly.Contrib.Decorator .

Problem

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

这将决定两件事:

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

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

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

What I have considered

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

检查Async后缀的名称显然不可靠;不是每个人都遵循这样的惯例 .

Question

是否有一种可靠的方法来识别方法实现是否是异步的,即是否应该使用 async 进行修饰,并且可以使用Roslyn仅根据其接口声明进行异步处理吗?

(请参阅评论讨论,这表明了这个问题的演变)

2 回答

  • 3

    TL;DR

    基本问题是如何在接口中包装方法的库应该调用这些方法 - 使用 await . 不仅是 async 是否在不可检测的实现上声明(来自接口,在调用站点的上下文之外),而且它没有为此确定 .

    • (1) async 关键字是否出现在方法实现中并不足以确定对它的调用是否可以使用 await .

    • (2)如果一个方法的返回类型是 await 能够(这足以确定),则该方法可以 await .

    • (3) async void 方法不改变上述结论;对于无法使用 await 调用它的事实, async void 方法不会异步运行 .

    (1) Whether the async keyword appears in a method implementation is not sufficiently determining for whether a call to it can or should use await.

    async 不是方法签名的正式部分 . 所以在接口中找不到 async . 因此,您可以使用 async 关键字编写原始作者的预期方法实现,或使用 await 调用 .

    但是,对于是否可以/应该使用 await 调用方法,是否使用 async 关键字编写被调用的方法实际上并不是确定(甚至是充分确定的)因子 . 在没有 async 关键字的情况下编写方法的有效情况会返回 await ables:

    [a] async-await eilision,as used extensively by Polly
    [b]一个主要用于I / O实现的接口,因此使用 await able返回类型声明,但您可能还需要编写内存(如此同步)实现/拦截器 . Discussion of common case: sync in-memory cache around async I/O [c]用于测试目的,用内存(同步)存根删除一些异步依赖

    (实际上 async 不是方法签名的一部分,因为它允许我们偶尔使用同步实现来实现,如上所述 . )

    (2) A method can be awaited if-and-only-if its return type is awaitable (this is sufficiently determining); ie the return type is Task, Task<TResult> or has a suitable GetAwaiter() method

    唯一确定方法调用是否为 await -ed的唯一方法是它的返回类型是否为 await 能 .

    (3) async void methods do not change the above conclusions

    这个地址 async void 方法的长度,因为问题中的注释可能是方法的返回类型不足,可能是因为 void 无法从 async void 区分(在界面中) .

    起点是无法等待 async void 方法,原因很简单,虽然使用 async 关键字编写,但它们不会返回任何可以 await -ed的类型 .

    这是否会使我们的结论无效(2)? (我们可以唯一地使用方法是否返回 await 能够确定如何调用它) . 我们是否正在失去一些异步运行 async void 方法的能力,因为我们不能 await 吗?

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

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

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

    (3b) Another angle on async void

    由于基本q(为了 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在前面的 await ed调用完成之后作为延续运行(异步;在不同的时间) . 调用者(除了 async void 方法)返回 Task ,这是一个'promise',它将在 await ed方法完成时完成 .

    返回 Task (或实现 GetAwaiter() 的其他内容)的事实是决定是否可以使用 await 调用它的事实:如果返回的方法类型实现了这个等待模式,则可以等待它 .

    特别是async / await elision和sync-cache-over-async-io模式的存在证明了被调用方法的返回类型是关键,而不是实现是否使用 async 关键字 .

  • 2

    罗斯林有一个内部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());
        }
    

相关问题