【译】.NET 8 拦截器(interceptor)

  通常情况下,出于多种原因,我不会说我喜欢写关于预览功能的文章。我的大多数帖子旨在帮助人们解决他们可能遇到的问题,而不是找个肥皂盒或打广告。但是我认为我应该介绍这个 .NET 预览特性,因为它是我在 .NET 生态系统中渴望已久的东西(猴子补丁,monkey patching,在运行时动态修改模块、类或函数,通常是添加功能或修正缺陷,猴子补丁在代码运行时内存中发挥作用,不会修改源码,因此只对当前运行的程序实例有效;因为猴子补丁破坏了封装,而且容易导致程序与补丁代码的实现细节紧密耦合,所以被视为临时的变通方案,不是集成代码的推荐方式)的姊妹主题。如果你不熟悉这个话题,我建议你阅读我关于猴子打补丁的帖子。一般来说,猴子补丁允许你用一个实现代替另一个实现,你知道吗,. NET 8引入了拦截器的概念。

  顾名思义,拦截器允许开发人员针对特定的方法调用,用新的实现拦截它们。拦截器有几个目的和重要的区别,我们将在这篇文章中讨论。让我们开始吧。

拦截器是什么?

  在 .NET 8预览版6中,SDK 引入了额外的功能来“拦截”代码库中的任何方法调用。“interceptor(拦截器)”这个词很清楚地说明了这个新功能的目的。它只是有意地替换方法,而不是全局地替换方法实现。这种方法意味着,作为开发人员,您必须系统地使用拦截器。

  . NET 团队使用拦截器将以前依赖于反射的基础架构代码重写为特定于应用程序的编译时版本。拦截器有望减少程序的启动时间和提高效率。. NET 团队设计了拦截器来与源代码生成器(source generator)一起工作,因为源代码生成器可以处理抽象语法树和代码文件以实现目标方法调用。虽然您可以手动编写拦截器调用,但这在实际应用程序中是不切实际的。

  让我们开始设置您的项目以使用拦截器。

入门

  拦截器是 .NET 8预览版6的一个特性,所以你需要匹配其 SDK 版本或更高版本才能使用它。首先创建一个新的控制台应用程序,或者任何 .NET 应用程序。

  接下来,在 .csproj 中,必须添加以下 PropertyGroup 元素。

<PropertyGroup>
    <Features>InterceptorsPreview</Features>
</PropertyGroup>

  还要确保将 LangVersion 元素设置为预览以访问该特性。

<PropertyGroup>
    <LangVersion>preview</LangVersion>
</PropertyGroup>

  接下来,将以下属性定义添加到项目中。

namespace System.Runtime.CompilerServices;

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public sealed class InterceptsLocationAttribute : Attribute
{
    public InterceptsLocationAttribute(string filePath, int line, int character)
    {
    }
}

  是的,这个属性不是 BCL 的一部分是很奇怪的,但由于这是一个预览特性,我想 .NET 团队不想在以后的 API 更改中污染 .NET 框架。

  您将注意到该属性有三个参数:filePath、line 和 character。您还会注意到,这些值没有在任何地方赋值,您是正确的。该属性是编译器将在编译时读取的标记,因此设置运行时使用的值是没有意义的。

  现在,让我们拦截一些代码。将以下内容添加到 Program.cs 文件中。注意,行号和间距非常重要。如果重新格式化代码,这个解决方案可能会失效。还要确保将文件路径更改为 Program.cs 文件的绝对路径。

using System.Runtime.CompilerServices;

C.M(); // What the Fudge?!
C.M(); // Original

class C
{
    public static void M() => Console.WriteLine("Original");
}

// generated
class D
{
    [InterceptsLocation("/Users/khalidabuhakmeh/RiderProjects/ConsoleApp12/ConsoleApp12/Program.cs", 
        line: 3, character: 3)]
    public static void M() => Console.WriteLine("What the Fudge?!");
}

  运行上面的应用程序,您将看到最奇怪的事情。同一个方法调用的两个不同输出!搞什么鬼?

  如何做到的?编译后的代码是什么样子的?我们可以使用 JetBrains Rider 的 IL Viewer 看到发生了什么。

// Decompiled with JetBrains decompiler
// Type: Program
// Assembly: ConsoleApp12, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: 09D7E1E0-5709-4A62-884A-AB84DAA1E08C
// Assembly location: /Users/khalidabuhakmeh/RiderProjects/ConsoleApp12/ConsoleApp12/bin/Debug/net8.0/ConsoleApp12.dll
// Local variable names from /users/khalidabuhakmeh/riderprojects/consoleapp12/consoleapp12/bin/debug/net8.0/consoleapp12.pdb
// Compiler-generated code is shown

using System.Runtime.CompilerServices;

[CompilerGenerated]
internal class Program
{
  private static void <Main>$(string[] args)
  {
    D.M();
    C.M();
  }

  public Program()
  {
    base..ctor();
  }
}

  现在可以看到,编译器用我们的拦截实现替换了第一个方法调用。哇!

  在这种令人眼花缭乱的感觉褪去之后,你可能会认为这是不切实际的。谁有时间硬编码文件的完整路径、计算行数和列数呢?正如前面提到的,这就是源代码生成器的用武之地。

  虽然在处理语法树时我不会在这里演示它,但是您确实可以访问如 FilePath 之类的信息,并且每个 CSharpSyntaxNode 都有一个 GetLocation 方法,该方法使您可以访问代码文件中的行号和位置。如果您已经精通编写源代码生成器,那么您已经可以获得这些信息。

结论

  这个特性是针对 .NET 社区中特定的一群人,特别是那些编写和维护源代码生成器的人。在这个小群体中,您可能会有框架作者希望从 .NET 中挤出最后一点性能。正如您所看到的,拦截器只能更改特定的实现,而不能全局地针对方法。如果使用源代码生成器对所有方法进行拦截,则必须为每个位置生成一个拦截调用。生成大量自定义代码可能会对编译资产的大小产生不利影响,因此要注意使用此特性。另外,您可以考虑完全避免这个功能。拦截器仍处于预览阶段,其主要目的是帮助 .NET 作者改进 ASP .NET Core 和 .NET SDK 中的其他框架。不管怎样,在下次调试 .NET 8应用程序时,了解这个特性是有好处的,因为你认为你调用的方法可能不是你实际调用的方法。

  我希望你喜欢这篇博文,并一如既往地感谢你阅读并与朋友和同事分享我的博文。

 

原文链接:https://khalidabuhakmeh.com/dotnet-8-interceptors

 

posted @ 2023-09-05 07:59  MeteorSeed  阅读(1884)  评论(2编辑  收藏  举报