c# readonly的"奥秘"
本文将探索c# readonly关键字在编译以及运行时的一些关系,通过讨论类中的值类型(即结构)字段的可修改性入手。
我们先编写一个极其简单的结构类型:
public struct StEntity { public int Val { get { _val++; return _val; } } private int _val; }
它只有一个int类型字段,以及访问该字段的属性,该属性将在访问时,将其值修改(+1),并返回。
随后我们编写一个具备该类型的一个字段及随同的一个属性的简单引用类型。
public class StEntityWrapper { private readonly StEntity _stEntity; public int Val { get => _stEntity.Val; } }
最后,我们使用如下的控制台代码以使用该引用类型的这个Val属性(程序入口的主方法签名被省略):
var wrapper = new StEntityWrapper(); var s1 = wrapper.Val; Console.WriteLine($"{nameof(s1)}:{s1}"); var s2 = wrapper.Val; Console.WriteLine($"{nameof(s2)}:{s2}");
结果输出如下:
s1:1
s2:2
可以看到两次访问的值是不一样的,说明在两次操作中,我们在StEntityWrapper的Val属性中所使用的StEntity是同一个实例,现在,我们为StEntity类型的字段_val加上readonly修饰,StEntityWrapper的代码将被修改成如下的样子:
public class StEntityWrapper { private readonly StEntity _stEntity; public int Val { get => _stEntity.Val; } }
再次运行程序,结果输出如下:
s1:1
s2:1
看到两次访问的值是相同的,说明在两次操作中,但是StEntityWrapper->Val的属性代码是一致的,结果却和前者有所区别,编译器和CLR究竟做了什么样的PY交易,使得StEntityWrapper性♂情Dark♂变.
通过c#代码无论是编译前还是编译后,两个StEntityWrapper实现的Val属性是一致的, 只能调查一下幕后煮屎者------MSIL,我们使用反编译工具由浅探♂讨一下:
这是未使用readonly的StEntityWrapper的MSIL代码:
// Token: 0x17000002 RID: 2 .property instance int32 Val() { // Token: 0x06000002 RID: 2 RVA: 0x00002058 File Offset: 0x00000258 .get instance int32 FrameworkDemo.StEntityWrapper::get_Val() } // Token: 0x06000002 RID: 2 RVA: 0x00002058 File Offset: 0x00000258 .method public hidebysig specialname instance int32 get_Val () cil managed { // Header Size: 12 bytes // Code Size: 12 (0xC) bytes // LocalVarSig Token: 0x11000001 RID: 1 .maxstack 1 /* 0x00000264 02 */ IL_0000: ldarg.0 /* 0x00000265 7C01000004 */ IL_0001: ldflda valuetype FrameworkDemo.StEntity FrameworkDemo.StEntityWrapper::_stEntity /* 0x0000026A 2804000006 */ IL_0006: call instance int32 FrameworkDemo.StEntity::get_Val() /* 0x0000026F 2A */ IL_000B: ret } // end of method StEntityWrapper::get_Val
比♂跤一下加入了readonly修shit♂的MSIL属性代码:
.property instance int32 Val() { // Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250 .get instance int32 FrameworkDemo.StEntityWrapper::get_Val() } // Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250 .method public hidebysig specialname instance int32 get_Val () cil managed { // Header Size: 12 bytes // Code Size: 15 (0xF) bytes // LocalVarSig Token: 0x11000001 RID: 1 .maxstack 1 .locals init ( [0] valuetype FrameworkDemo.StEntity ) /* (14,20)-(14,33) E:\CilDemoSolution\FrameworkDemo\StEntityWrapper.cs */ /* 0x0000025C 02 */ IL_0000: ldarg.0 /* 0x0000025D 7B01000004 */ IL_0001: ldfld valuetype FrameworkDemo.StEntity FrameworkDemo.StEntityWrapper::_stEntity /* 0x00000262 0A */ IL_0006: stloc.0 /* 0x00000263 1200 */ IL_0007: ldloca.s V_0 /* 0x00000265 2803000006 */ IL_0009: call instance int32 FrameworkDemo.StEntity::get_Val() /* 0x0000026A 2A */ IL_000E: ret } // end of method StEntityWrapper::get_Val
可以看到,两者的搞基代码虽然是一致的,但是生成的MSIL代码确实是云泥之别,未使用readonly字段修饰的属性方法直接将字段的地址通过ldflda载入到栈中,并调用其Val方法,而具备readonly修shit♂符的属性方法使用了一个局部变量,江字段的内存使用ldfld尻贝到这个局部变量的内存中,并使用局部变量的地址调用Val方法,为什么编译器在这里会出现这种差异呢?
一个原因可能是:被标记为readonly的字段内存块不能在运行时通过访问字段的方式修改其内容,引用类型在非空时,字段中存储的是对应实例的地址,而值类型则存储的是其整个内存块,那么问题来♂了,这种行为约束发生在编译时,在运行时,它是否仍然具备这种约束并抛出一个运行时异常呢?带着学♂习的精♂神,我们直接修改了附加了readonly的属性中msil的实现与其未附加readonly修饰的版本一致,并运行测试代码,它的结果如下:
s1:1
s2:2
:):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):)
两次结果发生了变化,由此我们可以下腚论,readonly修饰符的约束将只在编译时发生,并影响到编译结果,而运行时,在对readonly修饰的字段进行修改时,CLR并不会检查其可修改性。
That is ♂ it.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用