【譯】.NET 8 攔截器(interceptor)

2023-09-05 09:00:41

  通常情況下,出於多種原因,我不會說我喜歡寫關於預覽功能的文章。我的大多數貼文旨在幫助人們解決他們可能遇到的問題,而不是找個肥皂盒或打廣告。但是我認為我應該介紹這個 .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