试想一下,你自定义了一个Point结构:

1
2
3
4
5
6
7
public struct Point
{
private readonly float m_x;
private readonly float m_y;
private readonly float m_z;
//没有重写Equals方法
}

下面这样一段代码,共发生了几次装箱?

1
2
3
4
5
6
var p1 = new Point();
var p2 = new Point();
if (p1.Equals(p2))
{
Console.WriteLine("-------");
}

再看看IL代码:

il_point

你觉得共发生了几次装箱呢?1次?2次?还是其他?

我太天真了

天真的我,天真地觉得是1次。因为我就看到了一个box指令。如果你和我一样因为只看到了一个box指令而觉得是1次,那么恭喜,我俩都中招了。

其实如果不看IL,按照一般的思路去想的话,我会觉得是2次。因为自定义的Point类型继承自System.ValueType,Equals方法是System.ValueType重写自System.Object的虚方法。Point结构没有重写Equals方法的话,想要调用父类ValueType的Equals方法,就需要装箱,因为需要判断对象的类型来定位类型的方法表,才能找到该方法。所以,p1调用Equals的时候,p1要装箱。p2作为参数传入的时候,p2要装箱(因为Equals的参数要求是Object)。

但是看到IL,我只看到一个box指令,我会觉得可能是我想错了,可能就是1次吧。。。

其实还是2次。因为box底下跟着一个constrained. 被我忽略了。。。后来还是一位同学发了关于constrained. 的定义我才知道,它也可能会做装箱的操作。

constrained. 指令

constrained. 指令有什么作用呢?

点击查看文档

根据文档中的定义:

constrained.

关键部分是:

  • 如果类型是引用类型,那么托管指针ptr解引用并且作为this指针传入callvirt方法。
  • 如果类型是值类型,并且实现(重写)了虚方法,那么ptr不会修改,作为this指针 传入call方法,直接调用该类型实现的这个方法。
  • 如果类型是值类型,但是没有实现(重写)虚方法,那么ptr解引用,装箱,作为this指针传入callvirt方法。

“Point调用Equals发生几次装箱”的正确结果

对于我们的问题,Point结构没有重写ValueType的Equals方法,p1调用Equals方法的时候,处于第三种情况。很明显可以知道,发生了值类型装箱。加上p2作为参数传入Equals(Object)发生的一次装箱,一共是2次装箱。

如果Point重写了Equals方法

如果Point重写了Equals方法,则处于上面的第二种情况,p1调用Equals方法时,是直接调用Point实现的Equals实例方法。但是p2作为参数传入Equals(Object)会发生一次装箱。这样的情况,一共1次装箱。

反思

我太天真了。这次我踩坑了,我还是不细心,没有仔细思考。不应该通过所谓的表象去直接得出结论,还是要从问题本身去思考。有些时候看问题确实不能只看表面,而且看表面也不能看个一知半解。constrained. 就是这样一个东西,你要是放着没管,你就和我一样犯错了,你如果搞懂了它的作用,答案也就很明显了。

踩坑,没关系,不踩坑反而可能出问题。以后《我大意了啊》会作为一个系列出现,专门讲述踩坑+解决方案/正确答案。

⬆︎TOP