装箱/拆箱,值类型/引用类型 和 Object类,这些都是.NET程序员人人皆知且人人都应该掌握的概念。大多数人都对他们非常了解,可是和一些同行们交流时我发现一些细节其实很多人并不了解,尤其是它们结合讨论的情景,本文通过一些代码来阐述一些我知道的概念。

返回目录

代码1:Object.Equals

考虑下面代码的结果:

Console.WriteLine(Object.Equals(1, 1));

Console.WriteLine(Object.Equals(1, (byte)1));

答案:True, False

首先Object.Equals参数是两个object,所以1(值类型)会被装箱成引用类型,这会使CLR在托管堆中创建两个全新的Object对象,然后Object.Equals先判断两个object是否有null,没有则调用Object的对象方法Equals,而由于1是值类型,值类型改写Object.Equals并进行比特比较,最终由于object1的比特值完全等于object2的比特值第一句True,第二句很显然Int和Byte内存大小不一样,比特比较不会成功。

返回目录

代码2:Object.ReferenceEquals

代码2:Object.ReferenceEquals

Console.WriteLine(Object.ReferenceEquals(1, 1));

答案:False

同样,Object.ReferenceEquals参数是两个object,所以CLR会在托管堆中建立两个object来分别装Int值,但Object.ReferenceEquals的函数就是判断两个引用的是否指向同一个在托管堆的空间对象,这里当然是False了。

返回目录

代码3:再强化一下理解

下面代码,如果MyType是class或struct时,分别会输出什么?

struct/class MyType

{

    public int Data;

}

class Program

{

    static void Main(string[] args)

    {

        MyType s1 = new MyType();

        MyType s2 = new MyType();

        s1.Data = s2.Data = 1990;

        Console.WriteLine(Object.Equals(s1, s2));

    }

}

答案:

struct输出:True

class输出:False

这个为了强化下理解,原理和上面的一样,值类型和引用类型针对Object.Equals的执行是不一样的 

返回目录

代码4:问候了Equals,我们再看看==

下面代码输出什么?

struct MyType

{

    public int Data;

}

class Program

{

    static void Main(string[] args)

    {

        MyType s1 = new MyType();

        MyType s2 = new MyType();

        s1.Data = s2.Data = 1990;

        Console.WriteLine(s1 == s2);

    }

}

如果MyType是class,那么结果所有人会知道是False,那如果MyType是struct,结果是?……结果是编译错误,是的,值类型中的用户自定义结构体默认==运算符是不被预先重载的,但是引用类型,枚举,原始值类型的==有。

返回目录

代码5:神奇的String

下面输出结果?

string a = "aaa";

string b = "aaa";

Console.WriteLine(Object.Equals(a, b));

Console.WriteLine(Object.ReferenceEquals(a, b));

Console.WriteLine(a == b);

Console.WriteLine((object)a == b);

//这句VS会提示警告:

//Possible unintended reference comparison; to get a value comparison,

//cast the left hand side to type 'string'

答案:都是True,但True的方式不一样,呵呵,我们一句一句分析

第一句: 调用a.Equals(b),String类的执行是字符串比较,true

第二句:注意这里不进行字符串比较,这里是判断两个引用是不是指向同一个对象,因为Object.ReferenceEquals参数是两个object,但是.NET中相同的字符串(编译器可预知判断的)CLR会确保它们只向同一个内存空间,这个又称字符串的Interning。

第三句:直接调用String的重载==,字符串比较。

第四句:调用引用类型(Object)的重载==,其实等于调用Object.ReferenceEquals。参考第二句,这里Visual Studio提示警告也验证了第二句的结论,这里不会进行字符串比较,而是判断两个引用是否指向同一片内存空间对象。  

返回目录

代码6:字段和属性

考虑如果Point是class或struct下面程序的结果?

struct/class Point

{

    public int X, Y;

}

class MyCls

{

    public Point PField;

    public Point PProperty { get; set; }

}

class Program

{

    static void Main()

    {

        MyCls cls = new MyCls();

        cls.PField.X = 3;

        cls.PProperty.X = 3;

    }

}

答案:如果是Point是struct(即值类型),cls.PField.X会赋值成功,而cls.FProperty.X不会赋值成功(其实根本无法编译成功),因为属性本质上就是函数调用,这里PProperty返回一个值类型的拷贝,编辑这个拷贝的内部字段是没有意义的。

如果Point是class(即引用类型),会抛出NullReference异常,因为类内的引用类型默认CLR不为他们分配空间的,所以他们保持默认值(null)。

返回目录

代码7:MemberwiseClone()

MemberwiseClone()是一个非常有用的函数,但很多人不会用它,它不是引用的直接拷贝,而是将成员字段进行复制,如果成员是值类型,那么将进行深层拷贝,如果是引用类型,那么只拷贝引用指针(前后两个引用指向托管堆中的同一份空间)。

考虑下面代码输出?

class a

{

    public object obj;

    public object ShadowCopy()

    {

        return MemberwiseClone();

    }

}

class Program

{

    static void Main(string[] args)

    {

        a oa = new a() { obj = new object() };

        a ob = oa;

        a oc = (a)oa.ShadowCopy();

        oa.obj = null;

        Console.WriteLine(ob.obj == null);

        Console.WriteLine(oc.obj == null);

    }

}

答案:True, False

ob和oa指向同一个对象,所以oa变了,ob也变,oc是oa的MemberwiseClone的结果,oa的改变仅将自己的引用改成null。而oc没变,oc的成员引用还指向原来的位置。