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

时间:2017-07-09 18:18:55

标签: c# roslyn roslyn-code-analysis

背景资料

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

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

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

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

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

问题

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

这将决定两件事:

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

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

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

    我考虑的是

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

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

    问题

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

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

2 个答案:

答案 0 :(得分:3)

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

答案 1 :(得分:2)

TL; DR

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

  • (1)async关键字是否出现在方法实现中 是否足以确定对其的调用是否可以使用await
  • (2)如果的返回类型为await能够(这足以确定),则 可以<{em>} await
  • (3)async void方法不会改变上述结论; async void方法不能以异步方式运行,因为您无法使用await调用它。

(1)async关键字是否出现在方法实现中 是否足以确定对其的调用是否可以使用await。< /强>

async不是正式的方法签名的一部分。因此在接口中找不到async。因此,您无法从界面确定界面的原始作者是否希望使用async关键字编写方法实现,还是使用await调用。

然而,对于是否可以/应该使用async调用方法,被调用的方法是否使用await关键字编写实际上并不是决定(甚至是充分确定)的因素。编写方法的有效案例没有返回async ables的await关键字:

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

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

(2)如果 - 并且 - 如果它的返回类型是等待的(这足以确定),则可以等待方法;即返回类型为TaskTask<TResult>has a suitable GetAwaiter() method

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

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

这最终解决了async void个方法,因为问题中的注释表明方法的返回类型可能不足,可能是因为void无法区分(在接口中){ {1}}。

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

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

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

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

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

(3b)await

上的另一个角度

由于基本q(为了async void)的目的是我们应该如何调用一个包装方法,我们可以围绕Polly.Contrib.Decorator方法运行另一种思想实验。如果我们能够(以某种方式)确定接口后面的async void方法已被声明void怎么办?我们会以不同的方式称呼它吗?

回到我们的示例async void。我们有什么选择?我们可以直接async void Foo()Foo()

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

所以:即使我们知道界面中await背后的方法是void Foo()async void Foo()仍应该只调用Polly.Contrib.Decorator。如果用户想立即将工作卸载到另一个线程上(例如,他们想要将其从GUI线程卸载),他们会(在引入Foo()之前)以Polly.Contrib.Decorator调用该接口方法。我们不需要额外添加一个。

这符合Polly遵循的原则,我建议其他代表性的图书馆应该遵循:它应该对用户如何影响最小(除了声明的预期效果)。代表们都在运行。

以上所有内容的关键是Task.Run(() => ...)关键字(在其自身中)使方法异步运行甚至同时运行,所以不是硬道理。 async关键字只允许编译器在async语句中将方法切换为一系列块;这个方法的块2..n在前面的await ed调用完成后作为延续运行(异步;在不同的时间)。调用者(使用await方法除外)返回async void这是一个承诺&#39;这将在Task ed方法完成时完成。

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

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