前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【.NET8】访问私有成员新姿势UnsafeAccessor(上)

【.NET8】访问私有成员新姿势UnsafeAccessor(上)

作者头像
InCerry
发布2023-09-21 16:14:42
2590
发布2023-09-21 16:14:42
举报
文章被收录于专栏:InCerryInCerry

前言

前几天在.NET性能优化群里面,有群友聊到了.NET8新增的一个特性,这个类叫 UnsafeAccessor,有很多群友都不知道这个特性是干嘛的,所以我就想写一篇文章来带大家了解一下这个特性。

其实在很早之前我就有关注到这个特殊的特性,但是当时.NET8还没有正式发布,所以我也没有写文章,现在.NET8已经RC了,很快就会发布正式版,而且刚好有了一些时间,所以也可以带大家了解一下这个新的特性。

由于篇幅原因,这篇文章会分为上下两篇,其中上篇会带大家了解 UnsafeAccessor是干嘛的,有哪些用法,下篇会带大家了解 UnsafeAccessor的性能比较以及它的实现原理。

首先在我们了解这个类之前要假设一个场景,在很多时候我们都会遇到这样的场景,就是我们需要在一个类中访问另外一个类的私有成员,比如有如下代码:

代码语言:javascript
复制

var a = new A();
 
Console.WriteLine(a._value);
 

 
public class A
 
{
 
 private int _value = 10;
 
}
 

在上面的代码中,我们在类 B中访问了类 A的私有成员 _value,这种情况在我们的实际开发中是很常见的,但是在.NET中是不允许的,因为私有成员是不允许被外部访问的,所以我们在类 B中是不能访问类 A的私有成员 _value的,但是在实际的开发中,我们有时候确实需要访问类 A的私有成员 _value,这个时候我们该怎么办呢?下面我们来看一下如何访问私有成员。

.NET8以前的解决方案

在.NET8之前,我们可以通过如下的几种方法来访问私有成员,分别是反射、Emit、Expression,下面我们分别来看一下这几种方法。

反射

在.NET中,有一种叫反射的技术,这个对于任何一个.NET开发工程师应该都不陌生,我们可以通过反射来访问程序集的元数据信息,调用方法,访问字段等等,所以可以通过反射来访问私有成员,比如下面的代码我们可以通过反射来访问私有成员,如下所示:

代码语言:javascript
复制

var a = new A();
 
// 反射访问私有成员
 
var value = typeof(A).GetField("_value", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(a);
 
Console.WriteLine(value);
 

 
public class A
 
{
 
 private int _value = 10;
 
}
 

在上面的代码中,我们通过反射来访问了类 A的私有成员 _value,这样就可以访问到了,但是这种方式有一个缺点,就是性能比较差,因为反射是通过查找元数据和临时调用来实现的,所以性能比较差。在实际的开发中,我们一般不会使用反射来访问私有成员。

Emit

Emit 是 .NET 提供的一种动态生成和编译代码的技术。通过 Emit,我们可以动态生成一个新的方法,这个方法可以直接访问私有成员,这样就避免了反射的性能问题。以下是一个使用 Emit 访问私有成员的例子:

代码语言:javascript
复制

var a = new A();
 

 
// 创建一个动态方法,签名为 int GetValue(A a)
 
var method = new DynamicMethod("GetValue", typeof(int), new Type[] { typeof(A) }, typeof(A));
 

 
// 获取方法的 ILGenerator,通过 Emit 生成方法体
 
var il = method.GetILGenerator();
 

 
// 将参数 a 的私有成员 _value 压入堆栈
 
il.Emit(OpCodes.Ldarg_0);
 

 
// 将私有成员 _value 压入堆栈
 
il.Emit(OpCodes.Ldfld, typeof(A).GetField("_value", BindingFlags.NonPublic | BindingFlags.Instance));
 

 
// 返回栈顶的值
 
il.Emit(OpCodes.Ret);
 

 
// 通过 Emit 创建的方法,可以直接访问私有成员 _value
 
var func = (Func<A, int>)method.CreateDelegate(typeof(Func<A, int>));
 

 
// 调用方法
 
var value = func(a);
 
Console.WriteLine(value);
 

 
public class A
 
{
 
 private int _value = 10;
 
}
 

在上面的代码中,我们通过 Emit 创建了一个新的方法,这个方法可以直接访问类 A 的私有成员 _value。这种方法的性能比反射好很多,但是代码比较复杂,不易于维护。

Expression

Expression 是 .NET 提供的一种表达式树的技术。通过 Expression,我们可以创建一个表达式树,然后编译这个表达式树,生成一个可以访问私有成员的方法。以下是一个使用 Expression 访问私有成员的例子:

代码语言:javascript
复制

var a = new A();
 

 
// 创建一个表达式树,访问私有成员 _value
 
var parameter = Expression.Parameter(typeof(A), "x");
 

 
// 访问私有成员 _value
 
var field = Expression.Field(parameter, typeof(A).GetField("_value", BindingFlags.NonPublic | BindingFlags.Instance));
 

 
// 编译表达式树,生成一个可以访问私有成员 _value 的方法
 
var lambda = Expression.Lambda<Func<A, int>>(field, parameter);
 

 
// 调用方法
 
var func = lambda.Compile();
 
var value = func(a);
 
Console.WriteLine(value);
 

 
public class A
 
{
 
 private int _value = 10;
 
}
 

在上面的代码中,我们通过 Expression 创建了一个表达式树,然后编译这个表达式树,生成了一个可以访问类 A 的私有成员 _value 的方法。这种方法的性能比反射好,代码也相对简单,但是仍然比直接访问复杂。

.NET8的解决方案

我想很多聪明的小伙伴都已经猜到了,在.NET8中新增的 UnsafeAccessor就是用来访问私有成员的,我们可以通过 UnsafeAccessor来访问私有成员,下面我们来看一下如何使用 UnsafeAccessor来访问私有成员。

私有字段

代码语言:javascript
复制

using System.Runtime.CompilerServices;
 

 
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_value")]
 
static extern ref int GetValue(A a);
 

 
var a = new A();
 
var value = GetValue(a);
 
Console.WriteLine(value);
 

 
public class A
 
{
 
 private int _value = 10;
 
}
 

首先我们需要引入 System.Runtime.CompilerServices这个命名空间,然后定义一个 staicexternref方法,这个方法的返回值类型是字段的类型,然后它的参数就是对应实例的类型。在方法上面我们需要添加一个 UnsafeAccessor特性,这个特性有一个参数 UnsafeAccessorKind,这个参数表示我们要访问的是什么类型的私有成员,比如字段、属性、方法等等,这里我们要访问的是字段,所以我们传入的是 UnsafeAccessorKind.Field,然后我们还需要指定要访问的字段的名称,这里我们要访问的是 _value字段,所以我们传入的是 Name="_value",这样我们就可以通过 UnsafeAccessor来访问私有成员了。

来看一下运行的结果,可以看到和我们预期的一样输出了 10

因为它是返回了 ref的引用,所以我们可以通过这个引用来修改私有成员的值,比如我们修改一下 _value的值,如下所示:

代码语言:javascript
复制

using System.Runtime.CompilerServices;
 

 
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_value")]
 
static extern ref int ValueAccessor(A a);
 

 
var a = new A();
 
ref var value = ref ValueAccessor(a);
 
Console.WriteLine(value);
 

 
value = 20;
 
Console.WriteLine(ValueAccessor(a));
 

 
public class A
 
{
 
 private int _value = 10;
 
}
 

来看一下运行的结果,可以看到我们修改了 _value的值,第二次输出的时候就变成了 20

私有构造方法

同样的,使用 UnsafeAccessor我们也可以访问类中的私有构造方法和私有的方法,我们可以看到 UnsafeAccessor有一个参数 UnsafeAccessorKind,这个参数表示我们要访问的是什么类型的私有成员,比如字段、属性、方法等等,下方是它的定义:

代码语言:javascript
复制

private enum UnsafeAccessorKind
 
{
 
 Constructor,
 
 Method,
 
 StaticMethod,
 
 Field,
 
 StaticField
 
};
 

先来看一下如何访问私有构造方法,如下所示:

代码语言:javascript
复制
using System.Runtime.CompilerServices;
 

 
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
 
static extern A CreateA(int value);
 

 
var a = CreateA(10);
 
Console.WriteLine(a.Value);
 

 
public class A
 
{
 
 public readonly int Value;
 

 
 private A(int value)
 
 {
 
 Value = value;
 
 }
 
}
 

在上面的代码中,我们通过 UnsafeAccessor访问了类 A的私有构造方法,这个私有构造方法的参数是 int类型的,我们可以看到我们通过 UnsafeAccessor访问了私有构造方法,然后创建了一个 A的实例,然后输出了 Value的值,可以看到输出的结果是 10,这样我们就可以通过 UnsafeAccessor来访问私有构造方法了。

私有属性

由于属性是语法糖,编译器会自动为我们生成一个 getset方法,比如 publicintValue{get;set;},就会自动生成一个 get_Valueset_Value方法,我这里就不单独对访问私有方法进行演示,直接演示访问私有属性,它和访问私有方法是一样的,如下所示:

代码语言:javascript
复制

using System.Runtime.CompilerServices;
 

 
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_Value")]
 
static extern int GetValue(A a);
 

 
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_Value")]
 
static extern void SetValue(A a, int value);
 

 
var a = new A();
 
SetValue(a, 10);
 
Console.WriteLine(GetValue(a));
 

 
public class A
 
{
 
 public int Value { get; set; }
 
}
 

在上面的代码中,我们通过 UnsafeAccessor访问了类 A的私有属性 Value,这个私有属性有 getset方法,我们通过 UnsafeAccessor访问了 getset方法,然后我们就可以访问私有属性了,这样我们就可以通过 UnsafeAccessor来访问私有属性了。

局限性

当然,现在使用 UnsafeAccessor还有一些局限性,大家在使用的过程中需要注意一下。

通用泛型

比如现阶段它不支持通用泛型,像下面这样的代码是不支持的:

代码语言:javascript
复制

[UnsafeAccessor(UnsafeAccessorKind.Field, Name="_myList")]
 
static extern ref List<T> Field<T>(MyClass<T> _this);
 

但是现在可以写成下方这样的代码:

代码语言:javascript
复制

[UnsafeAccessor(UnsafeAccessorKind.Field, Name="_myList")]
 
static extern ref List<string> StringField(MyClass<string> _this);
 

 
[UnsafeAccessor(UnsafeAccessorKind.Field, Name="_myList")]
 
static extern ref List<double> DoubleField(MyClass<double> _this);
 

不过这会在.NET9中解决,有兴趣的小伙伴可以关注下方的链接: https://github.com/dotnet/runtime/issues/89439

私有类型

比如有下面这样的一个示例代码:

代码语言:javascript
复制

// Assembly A
 
private class C
 
{
 
 private static int Method(int a) { ... }
 
}
 

 
// Assembly B
 
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name="Method")]
 
static extern int CallMethod(??? c, int a); 
 

这里的问题是,我们不知道如何定义 CallMethod的第一个参数,因为 C是私有的,我们无法在 CallMethod的入参中定义它。

静态类

比如有下面这样的一个示例代码:

代码语言:javascript
复制

// Assembly A
 
public static class C
 
{
 
 private static int Method(int a) { ... }
 
}
 

 
// Assembly B
 
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name="Method")]
 
static extern int CallMethod(??? c, int a); 
 

这里的问题是,我们无法在 B中定义 CallMethod的第一个参数,因为 C是静态的,我们无法在 CallMethod的入参中定义它。

私有类参数

比如有下面这样的一个示例代码:

代码语言:javascript
复制

// Assembly A
 
public class C
 
{
 
 private class D { }
 
 private static int Method(D d) { ... }
 
}
 

 
// Assembly B
 
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name="Method")]
 
static extern int CallMethod(??? d); // Unable express D type as a parameter.
 

这里的问题是,我们无法在 B中定义 CallMethod的入参,因为 D是私有的,我们无法在 CallMethod的入参中定义它。包括目前还有私有返回值参数也是无法定义的。

但是这些问题在.NET9中也会解决,有兴趣的小伙伴可以关注下方的链接:

https://github.com/dotnet/runtime/issues/90081

总结

在本文中,首先介绍了在.NET8之前访问私有成员的几种方法,包括反射、Emit、和Expression。这些方法虽然可以实现访问私有成员,但是各有其优缺点,例如反射性能较差,Emit和Expression代码复杂度较高。

随后,我们详细介绍了.NET8新增的特性 UnsafeAccessor,这是一种更方便、更高效的访问私有成员的方法。我们通过实例代码演示了如何使用 UnsafeAccessor访问私有成员,包括私有字段、私有构造方法和私有属性。并且, UnsafeAccessor还支持修改私有成员的值。

然而, UnsafeAccessor目前还存在一些局限性,例如不支持通用泛型,无法访问私有类型、静态类和私有类参数。但是,这些问题预计在.NET9中将得到解决。

总的来说, UnsafeAccessor为.NET开发者提供了一个新的工具,使我们能够更方便、更高效地访问私有成员。虽然当前还存在一些局限性,但随着.NET的不断发展和进步,我们有理由相信这些问题将会得到解决。同时,我们也期待.NET在未来能够提供更多的功能和特性,以满足我们日益增长的开发需求。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-09-17 15:58,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 InCerry 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • .NET8以前的解决方案
    • 反射
      • Emit
        • Expression
        • .NET8的解决方案
          • 私有字段
            • 私有构造方法
              • 私有属性
                • 局限性
                  • 通用泛型
                  • 私有类型
                • 静态类
                  • 私有类参数
                  • 总结
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档