.NET 8 から UnsafeAccessorAttribute というのが登場しました。
learn.microsoft.com
これは、他のクラスの非公開メンバーにアクセスできてしまうという掟破りの機能です。
これまでもリフレクションを使えば出来たのですが、より簡便かつハイパフォーマンスに可能になりました。
基本の使い方
呼び出す側に static extern
なメソッドを定義し、それに UnsafeAccessorAttribute
をつけます。
属性のパラメーターには対象のメンバーのタイプを表す UnsafeAccessorKind 列挙型を指定します。
メソッドの第一引数は対象の型とし、対象がメソッドであれば、第二引数以降にその引数を並べます。
こんな感じです。
// 呼び出し側 using System.Runtime.CompilerServices; var target = new Target(); Console.WriteLine(Accessor.GetField(target)); // 10 internal static class Accessor { [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_field")] internal static extern ref int GetField(Target target); } // ターゲット側 public class Target { private int _field = 10; }
何に使うの?
基本的には単体テストかと思います。
他所のクラスの private
メソッドをテストしたいが、リフレクションは使いたくないという場合、これまでは、可視性を internal
にした上で、InternalsVisibleToAttribute を使うというのがよくある手法でした。
しかし、テストのために可視性を広げるというのは、できればやりたくないものです。そういうことが、private
のままでできるようになったわけです。
注意事項そのいち
もちろんなのですが、非公開メンバーに触るので、従来のリフレクションと同様の注意事項が存在します。
つまり、対象アセンブリがサードパーティのものである場合、バージョンアップに伴って実装が変更されると、前と同じようには呼び出せなくなる可能性があるということです。
ですからここでは、主な用途をテストとしています。対象アセンブリの内情を良く知っているからこその用途です。
他の用途にも使えますが、名前に Unsafe とついていることの意味をよく理解して自己責任で使いましょう。
なお、「private
メンバーの単体テストをすることの是非」や「対象の中身を良く知っていることを前提としたテストの是非」については、ここでは触れないものとします。
詳しい使い方
// メソッドを呼び出す [UnsafeAccessor(UnsafeAccessorKind.Method)] private static extern int Method(Target target, int i); // フィールドを取得する [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_field")] private static extern ref int GetField(Target target); // 静的メソッドを呼び出す // この場合、第一引数は対象の型の識別のみに使われ、実行時にはアクセスされない [UnsafeAccessor(UnsafeAccessorKind.StaticMethod)] private static extern int Method(Target? _, int i); // 静的フィールドを取得する // この場合、第一引数は対象の型の識別のみに使われ、実行時にはアクセスされない [UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "s_field")] private static extern ref int GetStaticField(Target? _); // プロパティの値を取得する [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_Property")] private static extern int GetProperty(Target target); // プロパティの値を設定する [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_Property")] private static extern void SetProperty(Target target, int value) // インスタンスを作成する [UnsafeAccessor(UnsafeAccessorKind.Constructor)] private static extern Target CreateInstance(); // コンストラクタを呼び出す [UnsafeAccessor(UnsafeAccessorKind.Method, Name = ".ctor")] private static extern void Reinitialize(Target target);
フィールドを触る場合は、戻り値に ref
を付ける必要があります。これによって、フィールドの値を設定することもできます。readonly
がついていてもお構いなしです。
ref int field = ref GetField(target); field = 10; // フィールド値の設定
プロパティの場合、実体はそれぞれ、名前の頭に get_
および set_
がついたメソッドですので、メソッドとして扱います。インデクサも同様です。
イベントの場合は add_
や remove_
がついたメソッドとして扱います。
対象が const
の場合はどうもうまく行かないようです。
対象の型が構造体の場合、第一引数に ref
を付ける必要があります。
また、種別を Method
、名前を ".ctor"
とすることで、コンストラクタをメソッドとして呼び出すことができます。
つまり、通常はできない、インスタンスの再初期化ができてしまうということです。
なお、メンバーの検索は、その型のみが対象になります。親クラスから継承したメンバーは(オーバーライドしていない限りは)呼び出せないので、親クラスを直接ターゲット型に指定する必要があります。
注意事項そのに
Name
プロパティを指定しない場合、UnsafeAccessorAttribute
を付けたメソッドの名前が使われます。
ただし、現代の C# では、様々な場面で、この名前が見た目通りのものにならないことがあります。
代表例は、C# 7 で導入されたローカル関数や、C# 10 で導入されたトップレベル ステートメントです。
こうした場合は、Name
を明示してやる必要があります。
// トップレベル ステートメントの場合、このメソッドのコンパイル後の名前は "Method" にはならず、"<<Main>$>g__Method|0_0" とかいう予測不能なものになる。 // その場合でも nameof(Method) は正しく "Method" になるので、Name プロパティの値として使える。 [UnsafeAccessor(UnsafeAccessorKind.Method, Name = nameof(Method))] static extern int Method(Target target, int i);
便利な使い方
拡張メソッドとして定義すると、自然な使用感が得られます。
var target = new Target(); var result = target.Method(i); internal static class Accessor { [UnsafeAccessor(UnsafeAccessorKind.Method)] public static extern int Method(this Target target, int i); }
カバーできないケース
対象の型を識別するために、UnsafeAccessorAttribute
を付けたメソッドの第一引数は対象の型にしなければなりません。
また、戻り値や引数の型も、対象のメンバーと同じにしなければいけません。
ということは、これらの型が不可視である場合には使えないということです。
それらの可視性が internal
で、対象のメンバーの可視性が private
とかいう場合には、InternalVisibleToAttribute
と UnsafeAccessorAttribute
の合わせ技が使えるでしょう(型とメンバーのどちらも internal
の場合は InternalVisibleToAttribute
だけでよいですね)。
対象の型が private
の場合はどうしようもなく、リフレクションを使うしかありません。
また、対象の型が static
である場合も、引数の型にできないので使うことができません。
ですから、主なユースケースは「テストのために渋々可視性を internal
にしているような場面」だとしたわけです。「元々の設計上 internal
が適切な場面」等では、引き続き InternalVisibleToAttribute
を使うことになるでしょう。
おまけの話
昔の Visual Studio には、テスト用にプライベート アクセッサとかいうものを生成する機能がありました。
また、MSTest なら、今でも PrivateObject クラスとかいうものがあるようです。
内部は素朴なリフレクションによる実装になっています。
便利ツールの話
こんなのがあるらしいです。
neue.cc