logo

Chanrich

.NET 8 中的 UnsafeAccessor(转载)

2024-12-20 Views Native AOT | .NET 8 | CSharp887字4 min read

反射允许您访问类的私有成员。当您想要访问不属于自己的类的私有成员时,反射非常有用。然而反射的速度很慢,并且不能很好地与 Native AOT 配合使用。这篇文章中将会介绍 .NET 8 中新引入的 UnsafeAccessor,它可以允许您访问私有成员而不进行反射。

原文地址:.NET 8 中的 UnsafeAccessor

在 .NET 8 之前,可以使用反射或通过在运行时生成 IL 来访问私有成员。这两种方法都很慢。.NET 8 提供了一种新的零开销方法来访问私有成员。这是使用 UnsafeAccessorAttribute 该特性完成的。

UnsafeAccessor 基于 C# Source Generator 发生在编译期间,因此你可以将他与 Native AOT 一起使用。

若要访问私有成员,可以使用 UnsafeAccessor 特性创建一个 extern 方法来声明私有成员的访问器。
请注意,UnsafeAccessorAttribute 不如反射强大。例如,尚不完全支持泛型类型。

首先,让我们创建包含私有成员的类

class Sample
{
    // 构造函数
    private Sample() { }
    private Sample(int value) { }
    // 字段
    private int instanceField = 1;
    private readonly int instanceFieldRO = 2;
    // 静态字段
    private static int staticField = 3;
    private static readonly int staticFieldRO = 4;
    // 属性
    public int InstanceProperty { get; set; }
    public int StaticProperty { get; set; }
    // 方法
    private int InstanceMethod(int value) => value;
    private static int StaticMethod(int value) => value;
}

构造函数

// 调用私有构造函数
var sample1 = CallPrivateConstructorClass();
var sample2 = CallPrivateConstructorClassWithArg(1);
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
extern static Sample CallPrivateConstructorClass();
// 参数必须与构造函数相匹配
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
extern static Sample CallPrivateConstructorClassWithArg(int value);

实例方法

var sample = CallPrivateConstructorClass();
Console.WriteLine(InstanceMethod(sample, 1));
// 第一个参数是包含私有方法的实例对象,第二个参数是私有方法的参数
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "InstanceMethod")]
extern static int InstanceMethod(Sample @this, int value);

静态方法

// 静态方法不需要实例,传入 null 调用即可
Console.WriteLine(StaticMethod(null, 2));
// 第一个参数必须是包含私有方法的类型
// 静态方法不需要实例,但运行时需要知道类型
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "StaticMethod")]
extern static int StaticMethod(Sample @this, int value);

实例属性

可以通过 getter 和 setter 方法访问属性

var sample = CallPrivateConstructorClass();
InstanceSetter(sample, 42);
Console.WriteLine(InstanceGetter(sample));
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_InstanceProperty")]
extern static void InstanceSetter(Sample @this, int value);
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_InstanceProperty")]
extern static int InstanceGetter(Sample @this);

静态属性

StaticSetter(null, 42);
Console.WriteLine(StaticGetter(null));
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "set_StaticProperty")]
extern static void StaticSetter(Sample @this, int value);
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "get_StaticProperty")]
extern static int StaticGetter(Sample @this);

实例字段

// 使用 ref 获取该字段的引用,这样就可以对字段进行读写操作
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "instanceField")]
extern static ref int GetInstanceField(Sample @this);
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "instanceFieldRO")]
extern static ref int GetInstanceReadOnlyField(Sample @this);
var sample = CallPrivateConstructorClass();
// 读取字段的值
_ = GetInstanceField(sample);
// 将值写入字段
GetInstanceField(sample) = 42;
// readonly 关键字只是编译器检查,所以你可以修改实例的 readonly 字段
GetInstanceReadOnlyField(sample) = 42;

静态字段

特性名称以 Unsafe 开头,所以它可以允许不安全的东西,例如修改静态只读字段。

[UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "staticField")]
extern static ref int GetStaticField(Sample @this);
[UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "staticFieldRO")]
extern static ref int GetStaticReadOnlyField(Sample @this);
var sample = CallPrivateConstructorClass();
// 读取值
_ = GetStaticField(sample);
// 写入值
GetStaticField(sample) = 42;
// 您可以修改静态只读字段的值
// 静态只读字段与 JIT 的常量非常相似。
// 修改静态只读字段的值可能导致意外行为,你可能无法再次读取该值。
// 请注意,使用反射是无法做到这一点的
GetStaticReadOnlyField(sample) = 42; 
// This fails at runtime
typeof(Sample).GetField("staticFieldRO", BindingFlags.Static | BindingFlags.NonPublic)
    .SetValue(null, 44);
EOF