在C#中,有一个特殊的类型Object类。这个类是C#中所有其它类的超类。
我们知道,C#只允许单继承,也即每一个类都可以有唯一的一个超类。本章我们重新定义:所有的C#类有且只有一个超类。如果一个类没有显式指定,则C#以Object类作为其超类。所以所有的类都会直接或者间接的继承Object类。
定义Object类的作用:
1、向所有的类引入一些关键方法;
2、可以让任意类的对象都引用到Object类型的变量上。
我们先看”作用1“
Object类包含了若干方法,如下图:
图1
- Object构造器,默认构造器而已,没啥特点;
- ~Object方法,析构器,这玩意儿我们讲到IO操作的时候讲,挺麻烦的,比C++的析构函数啰嗦多了;
- Equals方法,用于比较对象,该方法有一个静态重载,该方法很重要;
- GetHashCode方法,该方法用于获取一个对象的Hash值,讲到集合类的时候讲;
- GetType方法,获取一个对象的反射对象,讲反射的时候讲;
- MemberwiseClone方法,返回一个当前对象的拷贝(浅拷贝);
- ReferenceEquals方法,一个静态方法,用来比较两个对象的引用;
- ToString方法,返回对象的字符串表示。
上述的这一系列方法会被继承到所有类中,即所有的C#对象都具有这一系列方法。
对象比较:
我们研究过,在C#中,存在两种类型的对象——值类型对象和引用类型对象,分别对应两种类型的变量:值类型变量和引用类型变量,和C语言的值类型变量及指针类型变量对应。
我们可以简单的理解为:值类型变量保存着对象的“值”,而引用类型对象保存这对象的“引用”;所以在C#中,变量的比较就有两种含义。
- 对于值类型变量来说,比较两个变量是否保存着相同的值;
- 对于引用类型变量来所,比较两个变量是否引用到了同一个对象上。
当然,特殊情况下,我们也可能需要比较两个引用类型变量所引用的对象是否具有相同值的属性。我们看代码:
上述这段代码有如下几个要点需要注意:
- 对于引用类型,=运算符将引用从一个变量传递到另一个变量,所以=两边的变量引用同一个对象;
- 对于引用相同的两个变量,使用==运算符返回true;
- 对象的Clone方法产生一个新实例,返回该实例的引用,该实例的所有字段值和原对象相同,即Clone方法得到对象的一个副本。从Object类继承下来的保护方法MemberwiseClone返回当前对象的副本;
- 对于Clone方法返回的对象,其引用和原对象不同,==运算符返回false;
- 从Object继承下来的Equals方法结果和==运算符相同,只是比较当前引用(this)和参数保存的引用是否引用到了同一个对象上。
我们修改一下上面的代码,增加一个新方法:
这次我们覆盖了Equals方法,程序运行结果则不同了:Equals不再比较两个变量保存的对象引用是否相同,而是比较两个变量所引用的对象是否具有相同的属性值。
我们看一下覆盖后Equals方法的具体执行流程:
- 使用Object静态方法ReferenceEquals比较参数obj和当前对象引用(this)是否相同,如果引用相同,则必为相同对象;
- 如果变量obj和当前引用不相同,则使用as运算符,尝试将obj类型转换和当前对象相同的类型,转换成功表示obj变量引用着和当前对象类相同的对象,as运算符转换后对象的引用,否则返回null,只有同类型的对象才需要比较,不同类型的对象必然是不同的对象;
- 如果as运算符转换对象成功,则进一步比较当前对象引用(this)和参数obj引用对象的字段值是否相等。
好了,总结一下:==和Object类的Equals方法都是比较对象的引用是否相同,并不比较对象的字段是否相等;而Equals方法可以覆盖,以求让其对对象的字段进行等值比较。
所以,如果需要比较两个C#引用类型变量,除非确定需要比较的就是对象引用时才可以使用==运算符,否则应该使用对象的Equals方法,以防对象所属的类覆盖了这个方法。
说完引用类型,值类型是什么样呢?
C#令所有的值类型从一个特殊的ValueType类继承(这也直接解释了为什么值类型只能实现接口而无法继承其它类,值类型已经自动继承了一个类),我们看一下ValueType的特点:
图2
从图中可以看到,ValueType已经覆盖了Equals方法,即从ValueType继承的类型,不再遵守Object类定义的Equals方法。
从前面的代码中,我们得知,Object类的Equals方法仅仅是比较了当前引用和参数所引用对象的引用是否相同,从前面的课程我们也学习到,值类型对象不存在引用的。所以ValueType覆盖了Equals方法,令所有的值类型对象在调用Equals方法时,比较的是两个对象的所有字段值是否相等。
一般情况下,我们无需覆盖Equals方法。对于引用类型来说,等值比较就是比较对象的引用,引用不同的两个对象应该就是不同的对象;对于值类型来说,比较的就是所有字段的值,字段值不同的两个对象是不同的对象。
只有在特殊情况下,才需要覆盖Equals方法,已定义某个类特殊的对象比较方法。
注意:理论上可以用任意代码覆盖Equals方法,但要保证如下原则:
- 合理性:Equals只能用于对象比较,不能做其它任何用途;
- 稳定性:对于两个已存在的对象,在对象未作任何改变的情况下,无论任何时候调用Equals方法,返回的结果应该是一样的;
- 对称性:对于对象a和b,则a.Equalse(b)和b.Equals(a)返回的结果应该是一样的。
一旦覆盖了Equals方法,则C#推荐同时覆盖GetHashCode方法,具体原因讲到集合类是再做详细说明。
对象的字符串表示
可以覆盖ToString方法,来返回某个对象的字符串表示。
Object类的ToString方法返回一个对象所属类的类名。我们可以在任何类中覆盖ToString方法以返回需要的字符串。看代码:
和下面这段代码做一个比较,看看结果有何不同:
将ToString方法改为如下表示,再看看结果:
可以看到,ToString方法理论上也可以用任意代码覆盖,但应该返回符合对象含义的字符串。