鷲ノ巣

C# とか PowerShell とか。当ブログの記事は(特に公開直後は)頻繁に改定される場合があることをご了承ください。

UnsafeAccessorAttribute 完全理解

.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 とかいう場合には、InternalVisibleToAttributeUnsafeAccessorAttribute の合わせ技が使えるでしょう(型とメンバーのどちらも internal の場合は InternalVisibleToAttribute だけでよいですね)。

対象の型が private の場合はどうしようもなく、リフレクションを使うしかありません。
また、対象の型が static である場合も、引数の型にできないので使うことができません。
ですから、主なユースケースは「テストのために渋々可視性を internal にしているような場面」だとしたわけです。「元々の設計上 internal が適切な場面」等では、引き続き InternalVisibleToAttribute を使うことになるでしょう。

おまけの話

昔の Visual Studio には、テスト用にプライベート アクセッサとかいうものを生成する機能がありました。
また、MSTest なら、今でも PrivateObject クラスとかいうものがあるようです。
内部は素朴なリフレクションによる実装になっています。

便利ツールの話

こんなのがあるらしいです。
neue.cc