装箱拆箱与对象的比较
看到这样的一道面试题:
int i = 10;
object obj = i;
int j = (int) obj;
分析一下程序执行中的内存处理。
首先,我们可以看到这段程序定义了三个局部变量,局部变量将被定义在栈中,第一个变量比较简单,由于 i 是整形变量,所以变量 i 的值直接被保存在堆栈中。
而第二行对 obj 的赋值要复杂一点点,由于 obj 的类型是 object 类型,这是引用类型,所以,在堆栈中保存的必须是一个对象的引用,而不能是一个值,此时,会发生著名的装箱,CLR 会在堆中创建一个对象,在这个对象中保存变量 i 的值,并且,还会同时保存这个值的类型,这里是整数类型。此时,在内存中将会存在两个 10,一个保存在栈中,一个保存在堆中。
有的人认为此时堆中对象中保存的值就是引用堆栈中 j 的值,这个问题我们可以验证一下,如果是这样的话,我们修改 i 的值,那么 obj 应该也会同时发生变化。我们可以在第二行之后增加两行检查一下i。
i = 99;
Console.WriteLine( obj );
你可以看一看 obj 是否发生了变化。
第三行执行的时候,将一个引用类型的对象赋予一个值类型的变量 j,这时,会发生另外一个著名的事件:拆箱,CLR 会检查拆箱的类型是否匹配,然后从堆的对象中读取保存在其中的值,将这个值保存到位于堆栈中的变量 j 中。至此,在内存中已经存在了三个 10,两个在堆栈中,一个在堆中。如图所示:
如果我们为上面的程序再增加一行:
object obj2 = i;
那么,又会发生什么情况呢?
有的人认为在堆中其实还是第二行中装箱的对象,此时的 obj2 引用的也是这个对象,内存中的结构如下所示:
验证这个问题稍微复杂了一点,因为我们不能为 obj 赋值,赋值的话就是另外一个对象。而这个装箱的对象也没有什么方法让我们在不改变对象的情况下仅仅修改其中的值。
不过,我们还是有办法来检查它们是否为同一个对象的。
首先,我们可以考虑对象的 HashCode,在 Object 基类上定义了 GetHashCode 方法,可以返回对象的 Hash 码,通常我们可以通过比较对象的 HashCode 来判断引用的是否为同一个对象。那么,我们可以使用下面的语句来检查一下。
Console.WriteLine( obj.GetHashCode() );
Console.WriteLine( obj2.GetHashCode() );
不过,很不幸的是,你会看到输出了两次 10,那么,它们是同一个对象吗?我们还不能这么说。
HashCode 的用途主要用在对象作为字典的键的时候,用来判断键是否相同,但是,有的时候,对于不同的对象我们也希望他们看作同样的键,因此这个 GetHashCode 方法实际上是虚方法,是可以被类型重写掉的。在整数类型中,这个方法已经被重写掉了,所以,你会看到具有同样值得整数返回的 HashCode 是相同的。
比如,你有两张 10 元的人民币,这两张人民币显然不是同一张,但是,他们的票面价格是相同的,无论你用那一张存到银行中,银行都会为你记下 10 元的存款,这称为值相等,也就是价值相同。Object 基类还定义了方法 Equals ,这个 Equals 方法就是用来判断值相等的。所以,在重写了 GetHashCode 方法之后,你应该也重写一下 Equals 方法,以保证值相等的比较。
回到我们的问题,我们显然需要判断的不是值相等,还有什么方法吗?有,Object 基类还有一个静态方法 ReferenceEquls,专门用来比较两个对象是否其实是同一个对象实例,这称为引用相等。因此,下面的代码可以轻易地用来完成这个任务。
Console.WriteLine( Object.ReferenceEquals( obj, obj2 ) );
问题进行到这里,已经基本完成了。不过,CLR 又是怎么知道这两个对象是否为同一个对象的呢?在系统内部,每个对象在应用程序域中一旦诞生,CLR 都会赋予一个唯一的 HashCode ,虽然我们可以重写 GetHashCode,但是,这个内部的 HashCode 还是存在的,定义在命名空间 System.Runtime.ComilerServices 命名空间中的类 RuntimeHelpers 助手类就可以帮助我们解决这个问题,它的静态方法 GetHashCode 可以接受一个对象的引用,返回这个对象底层的 HashCode。
使用下面的代码,我们可以看到两个对象的 HashCode 真的是不同的。
Console.WriteLine(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj));
Console.WriteLine(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj2));
实际的内存结构应该如下:
完整的代码如下所示:
int i = 10;
object obj = i;
int j = (int)obj;
object obj2 = i;
Console.WriteLine(obj.GetHashCode());
Console.WriteLine(obj2.GetHashCode());
Console.WriteLine(Object.ReferenceEquals(obj, obj2));
Console.WriteLine(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj));
Console.WriteLine(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj2));