C# struct灵魂拷问
以下内容灵感来源于一次技术交流,基于CLR运行时。
struct也继承自System.Object,与class的区别到底在哪里?
struct继承自ValueType(所有值类型的基类),class属于引用类型,而值类型与引用类型的本质区别在于前者在拷贝时是按值拷贝,后者拷贝的是地址。其他区别还有:struct对象一般在线程栈上(除非嵌入了引用类型中作为字段),class总是在托管堆上由GC管理;struct对象不可能为null,总是初始化为0(不能自定义无参构造器);struct不支持继承;等。
struct为什么不能被继承(从语法上和设计上回答)
从语法上来说,struct继承ValueType的时候是隐式密封的,观察相应的IL代码即可发现sealed关键字。 从设计上来说,struct多用于纯数据的集合(也可以有逻辑),内存布局比较简单,没有类型对象指针和同步块索引。因此不能直接支持多态。
struct 可以实现接口吗
可以,系统里很多就是这么做的,比如 System.Int32 实际上CLR中的接口只是对一组方法签名进行了统一命名,这些方法不提供任何实现。
struct 可以自定义无参构造器吗
c#中不可以,会报错CS0568: Struct cannot contain explicit parameterless constructors。但CLR中其实没有这个限制。 如下代码中,CLR为了高效,会直接将内存空间置0,并不会自动调用1000次MyStruct的构造函数。如果c#允许定义无参构造器,那么在这里创建出来的MyStruct对象大概率是不合格的,会让使用者更加困惑。
MyStruct[] foo = new MyStruct[1000];多说一句,c#有一个设计理念是:类型的默认值不应当依赖于初始化。
struct何时被装箱
其实就是问值类型何时被装箱。
将值类型对象赋值/转换给一个接口(接口也是引用类型)
将值类型对象赋值/转换为一个
object(转换大多发生在函数参数传递)调用
Object中的非虚方法GetType()调用
ValueType或Object中的虚方法(例如ToString()),但自身没有实现ToString()。
其他情形,例如struct作为字典的key,可以归属到上述的4中。下面通过代码解释最难理解的第4种情况,其余三种可以仿效下面的方法自行检验,或者见 .NET 装箱/拆箱机制
在调用ToString()方法时,IL代码并无不同,都是constrained.跟着callvirt。callvirt并不代表只有引用类型可以用,值类型也可以,在前面加上constrained.就行。而且callvirt的出现并不意味着装箱。这个IL指令会另起一篇文章详细阐述。
虽然上面的IL中看不到box,展开进一步的分析。
m0没有Override,因此会调用父类
ValueType中的方法,为此一定要box的;m1在Override中调用了
base.ToString(),因此从IL代码中直接可以看到box这条指令;m2在Override中直接返回一个字符串,因此不需要
box;
同一个struct对象分别经历两次装箱,装箱后的对象地址相同吗
由装箱的过程可知,每次都是在堆内存上新申请一块空间并将值拷贝过去,因此两次装箱的对象地址不可能相同。
两个引用类型的对象默认是怎么比较相同的
通过比较二者的内存地址。参见Object.Equals。
struct作为字典的key产生了装箱后,而且装箱后地址不同,但字典似乎依然正常执行逻辑。它是如何判定key相等的?
既然讨论的是发生了装箱,也就是该struct并没有override自己的Equals,因此装箱后的对象调用的是ValueType.Equals。该方法正确处理了box后的对象的相等性判断,避免了使用Object.Equals带来的问题。具体来说,其过程是:
如果obj为
null,直接返回false;如果二者的类型不同,直接返回
false;尝试进行内存逐bit快速比较;
逐一比较每个字段,调用其对应类型的
Equals方法。
ValueType.Equals反编译后的源码如下:
灵魂拷问结束。
如果对.NET CLR没有系统性的了解,看到这些问题可能已经满头大汗。推荐看《CLR via C#》第二部分(尤其第5章),相信阅读后会豁然开朗。
同时极力推荐使用dnSpy作为IL查看工具,在实践中反思书本上的知识有没有过时,亦或是自己的理解有误。例如在观察是否有装箱时,笔者之前有一个不正确的理解:看不到box指令,就没有装箱。因此在分析一个没有实现自己的ToString()的struct对象调用ToString()时,因为没有观察到box,对书本和ECMA335标准上的说法产生了困惑,以为是编译器做了某种神奇优化。正确的理解是:实际会调用到ValueType.ToString(),为此struct一定会装箱。
Last updated