在.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<T>中的私有_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方法,并用该属性修饰,该方法需具有访问目标成员的正确签名。对于字段,这意味着一个方法接受目标类型的单个参数,并返回字段目标类型的引用。方法本身的名称不重要。
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<T>是泛型类型,我们必须使用一个“容器”类型来声明具有正确签名的访问器方法。使方法本身为泛型(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"
// 调用方法以检索字段引用并将字段值设置为空数组
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<T>上的静态方法的示例:
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的程序集中,并且所有类和成员都是internal的,因此无法直接引用。
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
52
|
// 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<T>类型,加上一个辅助类Class1:
1
2
|
internal class Generic<T> { }
internal class Class1 { }
|
我们需要创建Generic<T>类型的实例,即使它被标记为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<Class1>的实例呢?简单的答案是我们不能。我们需要调用Accessors<Class1>.Create(),但这不会编译,因为我们无法引用Class1。所以如果我们有这种模式,那么我们必须回退到传统反射。更重要的是,即使我们使用“传统”反射创建了Generic<Class1>的实例,我们也无法使用[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的方法,并且你无法表示该类型,那么你就不能使用[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&")] // 引用返回
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预览版