C# 12のプレビューモードで利用可能、インターセプタ(Interceptors)とは?
C# 12では、インターセプタによりコンパイル時に置き換え可能なメソッドを定義可能になりました。
インターセプタとは、その名の通りあるメソッドを実行時に別メソッドへリダイレクトする機能です。この機能により、開発時にのみ特化版のメソッドを実行することが可能になります。このインターセプタは、C# 12のプレビューモードでのみ利用可能です。以降のリリースで仕様が変更、あるいは削除される可能性がある旨を、Microsoftはアナウンスしています。よって、本番環境では利用しないことが推奨されます。
以降、簡単なコンソールアプリケーションを作りながら、インターセプタの機能を検証してみます。まず、コンソールアプリケーションを作成します。
% dotnet new console -o interceptors
プロジェクト設定ファイルに、以下のリストのようにインターセプタを有効にする名前空間の設定を追加します。C# 12 PreviewではInterceptorsPreviewを有効にする<Features>要素が必要でしたが、リリース版では不要になっています。
<PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);CSharp</InterceptorsPreviewNamespaces> (1) </PropertyGroup>
(1)の追加行において指定されているCSharpは、以降のリストでインターセプタのコードを記述する名前空間になります。
次に、以下のリストのような、メソッド呼び出しを行うコードをProgram.csファイルに記述します。この時点で、動作に問題のないことを確認しておきます。
Console.WriteLine("オリジナルのWriteLine呼び出し"); (1) MyClass c = new() { Prop = "プロパティ" }; c.Method("メソッド引数"); (2) class MyClass { public string Prop { get; set; } public void Method(string param) => Console.WriteLine($"オリジナルのメソッド呼び出し: prop = '{Prop}', param = '{param}'"); (3) }
(1)(2)は置き換え対象のメソッド呼び出しですが、異なるのは(2)の方はクラスのプロパティも表示している点です。(3)のメソッド定義では、クラスのプロパティと引数の双方を表示する仕様になっています。
この時点でプロジェクトをビルド、実行し、以下のように出力されることを確認してください。
オリジナルのWriteLine呼び出し オリジナルのメソッド呼び出し: prop = 'プロパティ', param = 'メソッド引数'
ここで、インターセプタを追加します。同じくProgram.csファイルに、インターセプタのコードを以下のリストのように追記します(filePath引数には、各自のソースファイルのパスを指定してください)。
namespace CSharp { (1) static class Interceptor { (2) [System.Runtime.CompilerServices.InterceptsLocation( (3) filePath: @"/Users/…/interceptors/Program.cs", line: 4, character: 9)] public static void InterceptWriteLine(string? message) (4) { Console.WriteLine($"インターセプトされたWriteLine呼び出し: message = '{message}'"); } [System.Runtime.CompilerServices.InterceptsLocation( (3) filePath: @"/Users/…/interceptors/Program.cs", line: 6, character: 3)] public static void InterceptorMethod(this MyClass c, string param) => Console.WriteLine($"インターセプトされたメソッドの呼び出し: prop = '{c.Prop}', param = '{param}'"); (5) } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public sealed class InterceptsLocationAttribute(string filePath, int line, int character) : Attribute (6) { } }
(1)では、interceptors.csprojファイルで指定した名前空間をここにも指定します。
(2)は、インターセプタの静的メソッドを格納するための静的クラスの宣言です。名前は何でも構いません。
(3)は、インターセプトするメソッド呼び出しの位置を正確に指定するための属性です。この属性は(6)にて定義されるクラスです。
(4)(5)は、インターセプトで置き換える側のメソッド定義です。(4)は引数のみを処理すればいいので改めてWriteLineメソッドを呼び出す処理になっていますが、(5)はクラスのインスタンスも受け取る必要があるので、thisパラメータを第1引数に指定しています。これにより、静的メソッドでありながらインスタンスメソッドのインターセプトも可能になっています。
改めて(6)ですが、これは(3)で指定している属性の定義に相当します。
改めてビルド、実行してみると、インターセプトしたメソッドによって実行結果が変化していることが分かります。
インターセプトされたWriteLine呼び出し: message = 'オリジナルのWriteLine呼び出し' インターセプトされたメソッドの呼び出し: prop = 'プロパティ', param = 'メソッド引数'
System.Runtime.CompilerServices名前空間
上記(6)のInterceptsLocation属性は、System.Runtime.CompilerServices名前空間内で定義する必要がありますが、このような名前空間に独自の定義を追加することは、通常は行いません。公式ドキュメントにも明確な記述はありませんが、インターセプタは実験的な機能なので、このようなルールになっていると思われます。
まとめ
今回は、任意の型エイリアス、インライン配列、Experimental属性、インターセプタについて紹介しました。
C#では、本連載で紹介したように、より使い勝手を良くする改変が新バージョンで施されています。よりモダンで使い勝手のよい言語として成長し続けるC#を、今後も見守っていきたいものです。