【譯】.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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章