.NET 10中使用[UnsafeAccessorType]实现更轻松的反射
这是“探索.NET 10预览版”系列文章的第九篇。
在本文中,我将描述对.NET 8中引入的[UnsafeAccessor]机制的一些改进。[UnsafeAccessor]允许您轻松访问类型的私有字段和调用私有方法,而无需使用反射API。在.NET 9中,此机制可用的方法和类型存在一些限制。在.NET 10中,通过引入[UnsafeAccessorType],其中一些限制已被填补,本文将探讨这些改进。
在.NET 8和9中使用[UnsafeAccessor]调用私有成员
[UnsafeAccessor]机制在.NET 8中引入,并在.NET 9中增加了对泛型的支持。
我将忽略您为何希望以这种方式访问私有成员这个问题,因为这是题外话。您始终可以通过反射API实现此目的,[UnsafeAccessor]只是让它变得更容易、更快一些。
例如,假设您想检索List中的私有_items字段:
1
2
3
4
5
|
public class List<T>
{
T[]? _items;
// .. 其他成员
}
|
您可以使用如下反射代码实现:
1
2
3
4
5
6
7
8
9
10
11
|
// 获取用于访问值的FieldInfo对象
var itemsFieldInfo = typeof(List<int>)
.GetField("_items", BindingFlags.NonPublic | BindingFlags.Instance);
// 创建列表实例
var list = new List<int>(16);
// 使用反射检索列表
var items = (int[])itemsFieldInfo.GetValue(list);
Console.WriteLine($"{items.Length} items"); // 输出 "16 items"
|
要使用[UnsafeAccessor],您必须创建一个特殊的extern方法,并用该属性修饰,该方法具有访问目标成员的正确签名。对于字段,这意味着一个方法,它接受目标类型的单个参数,并返回字段目标类型的实例作为ref。方法本身的名称并不重要。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 创建列表实例
var list = new List<int>(16);
// 调用方法以检索列表
int[] items = Accessors<int>.GetItems(list);
Console.WriteLine($"{items.Length} items"); // 输出 "16 items"
static class Accessors<T>
{
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_items")]
public static extern ref T[] GetItems(List<T> list);
}
|
请注意,由于List是泛型类型,我们必须使用“容器”类型来声明具有正确签名的访问器方法。使方法本身为泛型(GetItems<T>(List<T>))将不起作用,使用封闭类型GetItems(List<int>)也不会起作用。
本文后面还有更多关于调用泛型方法以及泛型类型成员的示例。
还值得注意的是,GetItems()签名返回一个ref(当您访问字段时必须如此),这意味着您也可以更改该字段。下面的示例使用相同的访问器方法,但这次替换了字段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// 创建列表实例
var list = new List<int>(16);
Console.WriteLine($"Capacity: {list.Capacity}"); // 输出 "Capacity: 16"
// 调用方法以检索字段ref并将字段值设置为空数组
Accessors<int>.GetItems(list) = Array.Empty<int>();
Console.WriteLine($"Capacity: {list.Capacity}"); // 输出 "Capacity: 0"
// 与之前相同的访问器:
static class Accessors<T>
{
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_items")]
public static extern ref T[] GetItems(List<T> list);
}
|
当然,您不仅可以调用字段,还可以调用方法(以及属性)和构造函数;任何在UnsafeAccesorType上定义的内容:
1
2
3
4
5
6
7
8
|
public enum UnsafeAccessorKind
{
Constructor,
Method,
StaticMethod,
Field,
StaticField,
}
|
例如,以下展示了调用定义在List上的静态方法的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// 调用私有静态方法
// 我们可以将`null`作为实例参数传递,因为这些都是静态方法
bool isCompat1 = Accessors<int?>.IsCompatibleObject(null, 123); // true
bool isCompat2 = Accessors<int?>.IsCompatibleObject(null, null); // true
bool isCompat3 = Accessors<int?>.IsCompatibleObject(null, 1.23); // false
static class Accessors<T>
{
// 我们正在调用的方法具有以下签名:
// private static bool IsCompatibleObject(object? value)
//
// 我们的extern签名必须将目标类型作为第一个方法参数包含在内
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "IsCompatibleObject")]
public static extern bool CheckObject(List<T> instance, object? value);
}
|
如上所示,对于实例方法和静态方法,您都将“目标”实例作为方法的第一个参数(在调用静态方法时传递null作为参数)。这样运行时就知道它在处理哪种Type:即第一个参数的类型。但是,如果您无法指定此类型,因为类型本身是私有的,该怎么办?
.NET 9中[UnsafeAccessor]的限制
在.NET 9中使用[UnsafeAccessor]的一大限制是,您必须能够直接引用目标签名中涉及的所有类型。举个具体例子,假设您正在使用的库中有一个类似这样的类型。
1
2
3
4
5
6
7
8
9
10
11
|
public class PublicClass
{
private readonly PrivateClass _private = new("Hello world!");
internal PrivateClass GetPrivate() => _private;
}
internal class PrivateClass(string someValue)
{
internal string SomeValue { get; } = someValue;
}
|
这非常刻意,但足以满足我们的目的。假设您有一个PublicClass的实例,但您真正需要的是_private字段上持有的SomeValue。然而,PrivateClass被标记为internal,因此您无法直接引用它。这意味着这些访问器都无法工作,因为无法在签名中使用PrivateClass:
1
2
3
4
5
6
7
8
9
10
11
|
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_private")]
static extern ref readonly PrivateClass GetByField(PublicClass instance);
// 👆 ❌ 无法引用PrivateClass
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetPrivate")]
static extern PrivateClass GetByMethod(PublicClass instance);
// 👆 ❌ 无法引用PrivateClass
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_SomeValue")]
static extern string GetSomeValue(PrivateClass instance);
// 👆 ❌ 无法引用PrivateClass
|
上面的例子是由于类型的可见性导致无法在签名中引用该类型。.NET 9中还有另一种情况不起作用:在编译时无法访问您正在处理的类型。第二种情况可能比较少见,但我想到了几个例子:
- .NET运行时由于不同库之间的循环依赖而遇到这种情况,例如HTTP和加密库。
- Datadog检测库需要访问我们正在检测的库的内部属性,但由于版本兼容性限制,我们无法直接引用这些库。
在.NET 9中,解决这些问题的唯一方法是回退到“普通”反射、使用System.Reflection.Emit或使用System.Linq.Expressions,所有这些都比[UnsafeAccessor]慢得多,并且更加繁琐。幸运的是,在.NET 10中,我们有一种两全其美的方法:即使对于无法引用的类型,我们也可以使用[UnsafeAccessor]!
在.NET 10中使用[UnsafeAccessorType]访问未引用的类型
在.NET 9中,您必须能够直接引用[UnsafeAccessor]方法签名中使用的所有类型。.NET 10引入了[UnsafeAccessorType]属性,它使我们即使对于无法引用的类型也能使用[UnsafeAccessor]。
将[UnsafeAccessorType]与[UnsafeAccessor]结合使用
.NET 10引入了一个新属性[UnsafeAccessorType],它允许您将[UnsafeAccessor]参数的预期类型指定为字符串,这解决了上一节中描述的两个场景。最好通过实际操作来理解这一点,因此让我们看一个与之前相同的例子。假设我们有这个层次结构。
1
2
3
4
5
6
7
8
9
10
11
|
public class PublicClass
{
private readonly PrivateClass _private = new("Hello world!");
internal PrivateClass GetPrivate() => _private;
}
internal class PrivateClass(string someValue)
{
internal string SomeValue { get; } = someValue;
}
|
我们将创建一些不安全访问器方法来检索那个棘手的SomeValue:
1
2
3
4
5
6
7
8
|
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetPrivate")]
[return: UnsafeAccessorType("PrivateClass")] // 👈 将目标返回类型指定为字符串
static extern object GetByMethod(PublicClass instance);
// 👆 使用object作为返回类型
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_SomeValue")]
static extern string GetSomeValue([UnsafeAccessorType("PrivateClass")] object instance);
// 在属性中指定目标类型 👆 并使用object作为实例类型 👆
|
如上面的示例所示,您使用object代替直接引用类型,并添加一个带有“真实”类型的[UnsafeAccessorType](稍后会详细说明)。一旦我们有了上述定义,我们就可以链式调用这两个方法来检索PrivateClass实例中存储的值,即使我们无法直接引用它:
1
2
3
4
5
6
7
8
9
|
// 创建目标实例
var publicClass = new PublicClass();
// 调用GetPrivate(),并将结果作为object返回
object privateClass = GetByMethod(publicClass);
// 传递object并调用SomeValue的getter方法
string value = GetSomeValue(privateClass);
Console.WriteLine(value); // Hello world!
|
就是这样,您可以使用[UnsafeAccessor]处理无法引用的类型。
使用[UnsafeAccessorType]指定类型名称
我在上一个示例中使用的类型名称非常简单,只是PrivateClass,但这隐藏了一个事实:这实际上是一个完全限定的类型名称,就像您可能在Type.GetType(name)中使用的那样。这需要完全限定,尽管不一定需要程序集限定,但这样做通常更健壮。另请注意,对于泛型类型和方法,这些字符串必须采用开放或封闭的泛型格式,具体取决于它们的用法,例如List``1[[!0]]。同样,您需要使用+来处理嵌套类。
为了使这一切更具体,我包含了一些来自运行时[UnsafeAccessor]单元测试的示例,这些示例演示了在访问器方法中使用[UnsafeAccessorType]引用类型。请注意,所有类型都定义在一个名为PrivateLib的程序集中,并且所有类和成员都是内部的,因此无法直接引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
namespace PrivateLib;
internal class Class1
{
static int StaticField = 123;
int InstanceField = 456;
Class1() { }
static Class1 GetClass() => new Class1();
private Class1[] GetArray(ref Class1 a) => new[] { a };
}
internal class GenericClass<T>
{
List<Class1> ClosedGeneric() => new List<Class1>();
List<U> GenericMethod<U>() => new List<U>();
bool GenericWithConstraints<V, W>(List<T> a, List<V> b, List<W> c, List<Class1> d)
where W : T
=> true;
}
|
以下是一组具有代表性的访问器示例,按复杂度递增排列。每个示例都展示了[UnsafeAccessorType]的不同用法。这也演示了不同类型的访问器,例如构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
// 1. 调用Class1的构造函数,返回的类型名称是程序集限定的
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
[return: UnsafeAccessorType("PrivateLib.Class1, PrivateLib")]
extern static object CreateClass();
// 2. 调用静态方法。返回类型和“目标”参数都是程序集限定的
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "GetClass")]
[return: UnsafeAccessorType("PrivateLib.Class1, PrivateLib")]
extern static object CallGetClass([UnsafeAccessorType("PrivateLib.Class1, PrivateLib")] object a);
// 3. 返回静态字段的引用
[UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "StaticField")]
extern static ref int GetStaticField([UnsafeAccessorType("PrivateLib.Class1, PrivateLib")] object a);
// 4. 返回实例字段的引用
// 注意,我们不能在返回类型上使用[UnsafeAccessorType]。稍后详述。
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "InstanceField")]
extern static ref int GetInstanceField([UnsafeAccessorType("PrivateLib.Class1, PrivateLib")] object a);
// 5. 通过引用传递对象并返回数组
// 注意通过引用传递时签名中的`&`。
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetArray")]
[return: UnsafeAccessorType("PrivateLib.Class1[], PrivateLib")]
extern static object CallM_RC1(TargetClass tgt, [UnsafeAccessorType("PrivateLib.Class1&, PrivateLib")] ref object a);
// 6. 调用泛型类型上的方法,并返回封闭泛型类型
// 返回类型混合使用了完全限定(用于BCL类型)和程序集限定的类型
// 注意开放泛型定义使用!0表示未指定的类型参数
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ClosedGeneric")]
[return: UnsafeAccessorType("System.Collections.Generic.List`1[[PrivateLib.Class1, PrivateLib]]")]
extern static object CallGenericClassClosedGeneric([UnsafeAccessorType("PrivateLib.GenericClass`1[[!0]], PrivateLib")] object a);
// 7. 调用泛型类型上的泛型方法。
// 这与上面类似,但我们使用!!0来表示未指定的泛型方法类型参数。
// 注意访问器方法本身必须是泛型的。
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GenericMethod")]
[return: UnsafeAccessorType("System.Collections.Generic.List`1[[!!0]]")]
extern static object CallGenericClassGenericMethod<U>([UnsafeAccessorType("PrivateLib.GenericClass`1[[!0]], PrivateLib")] object a);
// 8. 调用具有类型约束的泛型类型上的泛型方法
// 这是上述情况的更复杂版本,但额外指定了与目标方法约束匹配的类型约束。
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GenericWithConstraints")]
public extern static bool CallGenericClassGenericWithConstraints<V, W>(
[UnsafeAccessorType("PrivateLib.GenericClass`1[[!0]], PrivateLib")] object tgt,
[UnsafeAccessorType("System.Collections.Generic.List`1[[!0]]")]
object a,
[UnsafeAccessorType("System.Collections.Generic.List`1[[!!0]]")]
object b,
List<W> c,
[UnsafeAccessorType("System.Collections.Generic.List`1[[PrivateLib.Class1, PrivateLib]]")]
object d) where W : T;
|
这些示例展示了使用[UnsafeAccessorType]可以进行的各种调用,这使得这种方法非常强大。更重要的是,它很快。通常使用[UnsafeAccessor]比传统反射快得多。不幸的是,即使在.NET 10中,我们仍然有一些无法做到的事情。
[UnsafeAccessorType]的限制
如上一节所示,您可以将[UnsafeAccessorType]与[UnsafeAccessor]结合使用,以访问运行时无法引用的类型的私有成员,但仍然存在一些无法替代传统反射使用的空白。
总结如下:
- 如果您无法表示泛型类型参数,则无法在泛型类型上调用访问器。
- 您无法调用需要标记[UnsafeAccessorType]的字段。
- 您无法调用需要标记[UnsafeAccessorType]的返回ref的方法。
为了澄清这些情况,我将提供一些不起作用的类型和访问器的示例。
1. 无法表示类型参数
第一种情况很简单。假设我们有Generic类型,以及一个辅助类Class1:
1
2
|
internal class Generic<T> { }
internal class Class1 { }
|
我们需要创建Generic类型的实例,即使它被标记为internal,因此我们为其创建一个访问器:
1
2
3
4
5
6
|
static class Accessors<T>
{
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
[return: UnsafeAccessorType("Generic`1[[!0]]")]
public static extern object Create();
}
|
当我们需要引用的T可以被引用时,这可以正常工作:
1
2
|
object instance = Accessors<int>.Create();
Console.WriteLine(generic.GetType()); // Generic`1[System.Int32]
|
但如果我们需要创建Generic的实例会发生什么?简单的答案是我们不能。我们需要调用Accessors<Class1>.Create(),但这不会编译,因为我们无法引用Class1。因此,如果我们有这种模式,则必须回退到传统反射。更重要的是,即使我们使用“传统”反射创建了Generic的实例,我们也将无法使用[UnsafeAccessor]方法与对象交互,因为我们总是需要通过定义在Accessors<Class1>上的方法来这样做,这又是不可能的,因为我们无法引用Class1。
2. 无法表示字段返回类型
我们将扩展前面的示例,添加一个新类型Class2,它有几个引用Class1类型的字段:
1
2
3
4
5
6
7
|
internal class Class1 { }
internal class Class2
{
private Class1 _field1 = new();
private readonly Class1 _field2 = new();
}
|
如果_field1和_field2引用已知类型,那么我们可以为它们创建[UnsafeAccessor]而不会出现问题。您可能认为像下面这样的访问器会起作用:
1
2
3
4
5
6
7
8
9
10
11
12
|
// 用于创建C2实例的辅助函数
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
[return: UnsafeAccessorType("Class2")]
static extern object Create();
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_field1")]
[return: UnsafeAccessorType("Class1")]
static extern ref object CallField1([UnsafeAccessorType("Class2")] object instance);
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_field2")]
[return: UnsafeAccessorType("Class1")]
static extern ref readonly object CallField2([UnsafeAccessorType("Class2")] object instance);
|
但是,如果您尝试使用CallField1()或CallField2(),将在运行时收到错误:
1
2
3
|
object class2 = Create();
object field1 = CallField1(class2); // 抛出 System.NotSupportedException: Invalid usage of UnsafeAccessorTypeAttribute
object field2 = CallField2(class2); // 抛出 System.NotSupportedException: Invalid usage of UnsafeAccessorTypeAttribute
|
在这两种情况下,您都会收到一个System.NotSupportedException,说明Invalid usage of UnsafeAccessorTypeAttribute:您根本无法访问字段,除非您可以表示这些类型。
顺便说一下,当我第一次尝试这个功能时,这真的让我很困惑。访问字段是我尝试的第一件事,我以为是哪里做错了。😅
再重复一遍,如果您访问的字段没有使用[UnsafeAccessorType],例如如果_field1是int,那么这是可以正常工作的。只有在尝试将[UnsafeAccessorType]与字段一起使用时才会失败。
3. 无法表示ref方法返回
与之前的问题类似,如果您有一个返回ref的方法,并且您无法表示该类型,那么您就不能使用[UnsafeAccessor]。例如,让我们添加一个GetField1()方法,它返回_field1的引用:
1
2
3
4
5
6
7
|
internal class Class1 { }
internal class Class2
{
private Class1 _field1 = new();
private ref Class1 GetField1(Class2 a) => ref _field1;
}
|
这个访问器可能如下所示:
1
2
3
|
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetField1")]
[return: UnsafeAccessorType("Class1&")] // ref 返回
static extern ref object CallGetField1([UnsafeAccessorType("Class2")] object instance);
|
但尝试调用此访问器会在运行时失败,与尝试直接访问字段的方式相同:
1
2
|
object class2 = Create();
object field1 = CallGetField1(class2); // 抛出 System.NotSupportedException: Invalid usage of UnsafeAccessorTypeAttribute
|
这些是我发现的所有限制,因此只要您尝试做的事情不属于这些类别之一,那么您应该没问题!
总结
在本文中,我描述了对.NET 8中引入的[UnsafeAccessor]机制的一些改进。我展示了[UnsafeAccessor]如何让您在.NET 8和.NET 9中轻松访问私有字段和调用类型的私有成员,而无需使用反射API。然后,我描述了一些限制,即您需要能够引用您正在访问的成员所使用的类型。
接下来,我介绍了[UnsafeAccessorType]属性,并展示了如何使用它在编译时无法引用的类型上调用方法。我展示了如何使用它来调用方法、构造函数和字段,以及如何处理泛型类型和泛型方法。
最后,我描述了[UnsafeAccessorType]的限制,即您不能将其用于处理无法引用类型参数的泛型类型的实例,并且不能在字段或返回ref的方法上使用[UnsafeAccessorType]。
本系列
探索.NET 10预览版