【问题标题】:Find non-awaited async method calls查找未等待的异步方法调用
【发布时间】:2017-01-18 13:55:42
【问题描述】:

我在将 ASP.NET 应用程序迁移到 async/await 模型时偶然发现了一个相当危险的场景。

情况是我做了一个方法async:async Task DoWhateverAsync(),把接口中的声明改成Task DoWhateverAsync(),希望编译器能告诉我现在代码哪里错了,通过that Warning。好吧,运气不好。无论该对象通过接口注入到何处,都不会发生警告。 :-(

这很危险。有没有办法自动检查返回任务的非等待方法?我不介意警告太多,但我不想错过任何一个。

这是一个例子:

using System.Threading.Tasks;
namespace AsyncAwaitGames
{
    // In my real case, that method just returns Task.
    public interface ICallee { Task<int> DoSomethingAsync(); }

    public class Callee: ICallee
    {
        public async Task<int> DoSomethingAsync() => await Task.FromResult(0);
    }
    public class Caller
    {
        public void DoCall()
        {
            ICallee xxx = new Callee();

            // In my real case, the method just returns Task,
            // so there is no type mismatch when assigning a result 
            // either.
            xxx.DoSomethingAsync(); // This is where I had hoped for a warning.
        }
    }
}

【问题讨论】:

  • 它可能不像编译器警告那样通用,但我通常依赖于返回类型。如果我希望某个东西是 string 并尝试将其用作 string,但实际上它是 Task&lt;string&gt;,编译器告诉我这一点。 (我猜对void/Task 没有帮助。)
  • "之前的声明是什么?"不幸的是,它是无效的,并改为任务。
  • 我不了解您的情况。你说“希望编译器能告诉我现在的代码哪里错了”。 await 是程序中的一个点,等待的继续逻辑上需要完成任务。更改之前的代码在逻辑上不需要完成任务,那么为什么要在更改之后现在呢?引入了什么错误?
  • 我更改之前的代码/假设/完成了任务,因为 DoSomething() 是一个返回 void 的同步方法。然后我将方法更改为异步并返回任务。如果我直接使用该类,Visual Studio 会给我一个警告。由于我无法在上面的 ICallee 中将“异步”添加到方法签名中,因此代码现在看起来像返回 Task 的普通方法。显然这不足以触发 a) 使 DoCall() 异步和 b) 等待 xxx.DoSomethingAsync() 的警告。这是可以理解的。对我来说,根本原因是我无法在 ICallee 中指定“异步”。
  • "引入了什么错误?"之前:public void DoCall() { ICallee xxx = new Callee(); xxx.DoSomething();}(好)。之后:public void DoCall() { ICallee xxx = new Callee(); xxx.DoSomethingAsync();}(坏)。好:public void DoCall() { ICallee xxx = new Callee(); await xxx.DoSomethingAsync();}

标签: c# async-await


【解决方案1】:

在解决这个问题相当困难后,我决定创建一个带有代码修复的分析器来解决它。

代码可在此处获得: https://github.com/ykoksen/unused-task-warning

它也是一个 NuGet 包,可用作项目的分析器(在构建时): https://www.nuget.org/packages/Lindhart.Analyser.MissingAwaitWarning/#

此外,它还可以作为 Visual Studio 扩展(适用于 2017 年)提供。但是,这只分析当前打开的文件,所以我建议使用 NuGet 包。该扩展可在此处获得(或在 Visual Studio 中搜索): https://marketplace.visualstudio.com/items?itemName=Lindhart.missingAwaitWarning#overview

分析器的代码:

    public override void Initialize(AnalysisContext context)
    {
        context.RegisterSyntaxNodeAction(AnalyseSymbolNode, SyntaxKind.InvocationExpression);
    }

    private void AnalyseSymbolNode(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext)
    {
        if (syntaxNodeAnalysisContext.Node is InvocationExpressionSyntax node)
        {
            if (syntaxNodeAnalysisContext
                    .SemanticModel
                    .GetSymbolInfo(node.Expression, syntaxNodeAnalysisContext.CancellationToken)
                    .Symbol is IMethodSymbol methodSymbol)
            {
                if (node.Parent is ExpressionStatementSyntax)
                {
                    // Only checks for the two most common awaitable types. In principle this should instead check all types that are awaitable
                    if (EqualsType(methodSymbol.ReturnType, typeof(Task), typeof(ConfiguredTaskAwaitable)))
                    {
                        var diagnostic = Diagnostic.Create(Rule, node.GetLocation(), methodSymbol.ToDisplayString());

                        syntaxNodeAnalysisContext.ReportDiagnostic(diagnostic);
                    }
                }
            }
        }
    }

    /// <summary>
    /// Checks if the <paramref name="typeSymbol"/> is one of the types specified
    /// </summary>
    /// <param name="typeSymbol"></param>
    /// <param name="type"></param>
    /// <returns></returns>
    /// <remarks>This method should probably be rewritten so it doesn't merely compare the names, but instead the actual type.</remarks>
    private static bool EqualsType(ITypeSymbol typeSymbol, params Type[] type)
    {
        var fullSymbolNameWithoutGeneric = $"{typeSymbol.ContainingNamespace.ToDisplayString()}.{typeSymbol.Name}";
        return type.Any(x => fullSymbolNameWithoutGeneric.Equals(x.FullName));
    }

【讨论】:

  • 这是否会捕获我放置 Task t = GetSomethingAsync()var t = GetSometingAsync() 的情况,这会掩盖编译器警告。
  • 我在哪里看到错误。我添加了包,但我对任务的调用很糟糕,但除了编译器的标准消息之外,我没有看到任何其他错误。
  • @Simon_Weaver 查看您必须使用 Visual Studio 2017 的警告(我相信,它可能在 2015 年有效)。此外,要将其作为编译器警告,您必须将 NuGet 包添加到您的项目中。如果您使用 Visual Studio 扩展,您只会在打开文件时将其视为警告。
  • 感谢您的信息。问题是我一定是在某个地方做了这个,假设它不会抛出异常——现在它会抛出异常——我不知道在哪里。非常非常棘手。每次它碰巧寻找模式时,我都会给我发一条短信。我可能已经完成了 var T 或 Task t,但我搜索并找不到任何内容。
  • 感谢您提供这个插件!正是我需要的。我们在未检测到的 void 方法中有太多未等待的调用。大多数空白应该是任务
【解决方案2】:

你有几个选择:

  • 这是最简单的“穴居人”解决方案,在整个解决方案中使用内置的 VS 搜索功能(CTRL + SHIFT + F)搜索,也在查找选项下单击复选框 使用正则表达式并使用此正则表达式:(?&lt;!await|task(.*))\s([_a-zA-Z0-9\.])*Async\( 它假定您使用 Async 关键字发布修复了所有异步方法,并且 方法调用在一行中。如果不正确,则不要使用它(或将缺少的验证添加到表达式中)。
  • 使用一些第 3 方代码分析工具,Nuget 包。 ReSharper 非常受欢迎,我相信它能够检测到这个问题,或者您可以创建自己的规则。
  • 我的选择是使用 Roslyn(@Volker 提供了一种解决方案)。您可以使用代码修复解决方案创建自己的规则集(灯泡图标将显示您的代码修复),所以这是最好的。
  • 更新: VS 2019 默认检查这个问题并给出警告。

如何使用罗斯林:

  • 您必须安装 .NET 编译器平台 SDK:来自 here
  • 使用 VS 2017 版本 15.2(或更高版本)
  • 创建一个新项目 File -> New -> Project,在 Extensibility 组下选择:Analyzer with Code Fix (Nuget + VSIX) 你必须以 .NET Framework 4.6.2 为目标来创建它项目。

您可以复制粘贴以前的解决方案。创建

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AsyncAwaitAnalyzer : DiagnosticAnalyzer
{ ...
}

类与逻辑,以检测问题。并创建

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AsyncAwaitCodeFixProvider)), Shared]
public class AsyncAwaitCodeFixProvider : CodeFixProvider
{ ...
}

类为问题提供修复建议(添加等待)。

成功构建后,您将获得自己的 .wsix 包,您可以将其安装到您的 VS 实例中,并且在 VS 重新启动后应该开始解决问题。

【讨论】:

  • 抱歉,以前从未创建过这些...您明确指的是“以前的解决方案”中的哪些代码段?
【解决方案3】:

最后,我们使用 roslyn 来查找所有 Task 或 Task 的返回值被忽略的实例:

if (methodSymbol.ReturnType.Equals(syntaxNodeAnalysisContext.SemanticModel.Compilation.GetTypeByMetadataName(typeof(Task).FullName)))
{
    // For all such symbols, produce a diagnostic.
    var diagnostic = Diagnostic.Create(Rule, node.GetLocation(), methodSymbol.ToDisplayString());

    syntaxNodeAnalysisContext.ReportDiagnostic(diagnostic);
}
if (((INamedTypeSymbol) methodSymbol.ReturnType).IsGenericType && ((INamedTypeSymbol) methodSymbol.ReturnType).BaseType.Equals(syntaxNodeAnalysisContext.SemanticModel.Compilation.GetTypeByMetadataName(typeof(Task).FullName)))
{
    // For all such symbols, produce a diagnostic.
    var diagnostic = Diagnostic.Create(Rule, node.GetLocation(), methodSymbol.ToDisplayString());

    syntaxNodeAnalysisContext.ReportDiagnostic(diagnostic);
}

【讨论】:

  • 我请求您围绕您分享的建议添加一些上下文/评论。它将帮助提问者和其他未来的读者更好地理解您的帖子。
  • 能否请您添加您对 Roslyn 扩展所做的完整实现?我试图让它工作,但对这些扩展很陌生。
  • 这就是我要告诉你的。感谢罗斯林 :)
  • 我尝试实现这一点,但它只是给出了太多警告。我在实现自己的解决方案之前使用了它(请参阅我的答案),我记得它只是找到了方法返回 Task 的所有地方。
  • @Volker 让你的答案更完整,详细说明在哪里以及如何添加以上内容
【解决方案4】:

编译器会发出warning CS4014,但只有在调用方法是async时才会发出。

没有警告:

Task CallingMethod() {
    DoWhateverAsync();
    // More code that eventually returns a task.
}

警告 CS4014:由于未等待此调用,因此在调用完成之前继续执行当前方法。考虑将“等待”运算符应用于调用结果。

async Task CallingMethod() {
    DoWhateverAsync();
}

这在您的特定情况下并不是非常有用,因为您必须找到调用 DoWhateverAsync 的所有位置并更改它们以获取警告,然后修复代码。但是您想首先使用编译器警告来查找这些调用。

我建议你使用 Visual Studio 来查找 DoWhateverAsync 的所有用法。无论如何,您都必须通过编译器警告或通过使用列表来修改周围的代码。

【讨论】:

  • @David 不,在 OP 的情况下调用方法不是async,因为他正在从同步模型过渡到异步模型。
  • 如果您执行Task t = DoWhateverAsync() 也不会发出警告,这可能是故意用来隐藏编译器警告的。哎呀
【解决方案5】:

您可以在 VS 项目属性中添加特定警告作为引发编译错误的警告,如 here 所述

您可以添加以分号分隔的警告代码列表,例如CS4014,如果您不等待 async 方法,编译器将无法编译。

这是 VS2017 的屏幕截图: VS2017 configuration to throw compiler errors for warning CS4014

【讨论】:

  • 问题是在所描述的场景中绝对不会产生任何警告。
  • 实际上,正如@Martin Liversage 所提到的,调用函数(在本例中为DoCall)必须是async 函数才能引发编译器警告。所以我的立场是正确的,因为这不适用于你的情况。我认为处理这个问题的方法是通过一些代码分析器,无论是 Roslyn 还是 ReSharper 等。
【解决方案6】:

我为此找到了一个很棒的 Visual Studio 扩展/NuGet 包。让您不必像其他答案建议的那样创建自己的。

https://github.com/ykoksen/unused-task-warning

一旦我安装了 Visual Studio 扩展,我就去分析/运行代码分析/打开解决方案,它发现了我遇到这个问题的几个地方。

【讨论】:

  • 接受答案的解决方案,其中包括您发布的相同链接以及更多信息。
  • @Ergwun,接受的答案不引用扩展名。但我看到评分最高的答案确实如此。不知怎的,我错过了。
  • 是的,你是对的——它是票数最高的,而不是被接受的。我认为他们最近对其进行了更改,因此接受的答案不一定是最高的,因此当前投票最多的答案以前不会那么突出。
猜你喜欢
  • 1970-01-01
  • 2015-11-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-08
  • 2013-04-15
相关资源
最近更新 更多