C# 类型基础
对象判等
因为我们要提到对象克隆(复制),那么,我们应该有办法知道复制前后的两个对象是否相等。所以,在进行下面的章节前,我们有必要先了解如何进行对象判等。
NOTE:有机会较深入地研究这部分内容,需要感谢 微软的开源 以及 VS2008 的FCL调试功能。关于如何调试 FCL 代码,请参考 Configuring Visual Studio to Debug .NET Framework Source Code。
我们先定义用作范例的两个类型,它们代表直线上的一点,唯一区别是一个是引用类型class,一个是值类型struct:
public class RefPoint { // 定义一个引用类型
public int x;
public RefPoint(int x) {
this.x = x;
}
}
public struct ValPoint { // 定义一个值类型
public int x;
public ValPoint(int x) {
this.x = x;
}
}
1.引用类型判等
我们先进行引用类型对象的判等,我们知道在System.Object基类型中,定义了实例方法Equals(object obj),静态方法 Equals(object objA, object objB),静态方法 ReferenceEquals(object objA, object objB) 来进行对象的判等。
我们先看看这三个方法,注意我在代码中用 #number 标识的地方,后文中我会直接引用:
public static bool ReferenceEquals (Object objA, Object objB)
{
return objA == objB; // #1
}
public virtual bool Equals(Object obj)
{
return InternalEquals(this, obj); // #2
}
public static bool Equals(Object objA, Object objB) {
if (objA==objB) { // #3
return true;
}
if (objA==null || objB==null) {
return false;
}
return objA.Equals(objB); // #4
}
我们先看ReferenceEquals(object objA, object objB)方法,它实际上简单地返回 objA == objB,所以,在后文中,除非必要,我们统一使用 objA == objB(省去了 ReferenceEquals 方法)。另外,为了范例简单,我们不考虑对象为null的情况。
我们来看第一段代码:
// 复制对象引用
bool result;
RefPoint rPoint1 = new RefPoint(1);
RefPoint rPoint2 = rPoint1;
result = (rPoint1 == rPoint2); // 返回 true;
Console.WriteLine(result);
result = rPoint1.Equals(rPoint2); // #2 返回true;
Console.WriteLine(result);
在阅读本文中,应该时刻在脑子里构思一个堆栈,一个堆,并思考着每条语句会在这两种结构上产生怎么样的效果。在这段代码中,产生的效果是:在堆上创建了一个新的RefPoint类型的实例(对象),并将它的x字段初始化为1;在堆栈上创建变量rPoint1,rPoint1保存堆上这个对象的地址;将rPoint1 赋值给 rPoint2时,此时并没有在堆上创建一个新的对象,而是将之前创建的对象的地址复制到了rPoint2。此时,rPoint1和rPoint2指向了堆上同一个对象。
从 ReferenceEquals()这个方法名就可以看出,它判断两个引用变量是不是指向了同一个变量,如果是,那么就返回true。这种相等叫做 引用相等(rPoint1 == rPoint2 等效于 ReferenceEquals)。因为它们指向的是同一个对象,所以对rPoint1的操作将会影响rPoint2:
注意System.Object静态的Equals(Object objA, Object objB)方法,在 #3 处,如果两个变量引用相等,那么将直接返回true。所以,可以预见我们上面的代码rPoint1.Equals(rPoint2); 在 #3 就会返回true。但是我们没有调用静态Equals(),直接调用了实体方法,最后调用了#2 的 InternalEquals(),返回true。(InternalEquals()无资料可查,仅通过调试测得)。
我们再看引用类型的第二种情况:
//创建新引用类型的对象,其成员的值相等
RefPoint rPoint1 = new RefPoint(1);
RefPoint rPoint2 = new RefPoint(1);
result = (rPoint1 == rPoint2);
Console.WriteLine(result); // 返回 false;
result = rPoint1.Equals(rPoint2);
Console.WriteLine(result); // #2 返回false
上面的代码在堆上创建了两个类型实例,并用同样的值初始化它们;然后将它们的地址分别赋值给堆上的变量 rPoint1和rPoint2。此时 #2 返回了false,可以看到,对于引用类型,即使类型的实例(对象)包含的值相等,如果变量指向的是不同的对象,那么也不相等。
2.简单值类型判等
注意本节的标题:简单值类型判等,这个简单是如何定义的呢?如果值类型的成员仅包含值类型,那么我们暂且管它叫 简单值类型,如果值类型的成员包含引用类型,我们管它叫复杂值类型。(注意,这只是本文中为了说明我个人作的定义。)
应该还记得我们之前提过,值类型都会隐式地继承自 System.ValueType类型,而ValueType类型覆盖了基类System.Object类型的Equals()方法,在值类型上调用Equals()方法,会调用ValueType的Equals()。所以,我们看看这个方法是什么样的,依然用 #number 标识后面会引用的地方。
public override bool Equals (Object obj) {
if (null==obj) {
return false;
}
RuntimeType thisType = (RuntimeType)this.GetType();
RuntimeType thatType = (RuntimeType)obj.GetType();
if (thatType!=thisType) { // 如果两个对象不是一个类型,直接返回false
return false;
}
Object thisObj = (Object)this;
Object thisResult, thatResult;
if (CanCompareBits(this)) // #5
return FastEqualsCheck(thisObj, obj); // #6
// 利用反射获取值类型所有字段
FieldInfo[] thisFields = thisType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
// 遍历字段,进行字段对字段比较
for (int i=0; i
thisResult = ((RtFieldInfo)thisFields[i]).InternalGetValue(thisObj,false);
thatResult = ((RtFieldInfo)thisFields[i]).InternalGetValue(obj, false);
if (thisResult == null) {
if (thatResult != null)
return false;
}
else
if (!thisResult.Equals(thatResult)) { // #7
return false;
}
}
return true;
}
我们先来看看第一段代码:
// 复制结构变量
ValPoint vPoint1 = new ValPoint(1);
ValPoint vPoint2 = vPoint1;
result = (vPoint1 == vPoint2); //编译错误:不能在ValPoint上应用 "==" 操作符
Console.WriteLine(result);
result = Object.ReferenceEquals(vPoint1, vPoint2); // 隐式装箱,指向了堆上的不同对象
Console.WriteLine(result); // 返回false
我们先在堆栈上创建了一个变量vPoint1,变量本身已经包含了所有字段和数据。然后在堆栈上复制了vPoint1的一份拷贝给了vPoint2,从常理思维上来讲,我们认为它应该是相等的。接下来我们就试着去比较它们,可以看到,我们不能用“==”直接去判断,这样会返回一个编译错误。如果我们调用System.Object基类的静态方法ReferenceEquals(),有意思的事情发生了:它返回了false。为什么呢?我们看下ReferenceEquals()方法的签名就可以了,它接受的是Object类型,也就是引用类型,而当我们传递vPoint1和vPoint2这两个值类型的时候,会进行一个隐式的装箱,效果相当于下面的语句:
Object boxPoint1 = vPoint1;
Object boxPoint2 = vPoint2;
result = (boxPoint1 == boxPoint2); // 返回false
Console.WriteLine(result);
而装箱的过程,我们在前面已经讲述过,上面的操作等于是在堆上创建了两个对象,对象包含的内容相同(地址不同),然后将对象地址分别返回给堆栈上的 boxPoint1和boxPoint2,再去比较boxPoint1和boxPoint2是否指向同一个对象,显然不是,所以返回false。
我们继续,添加下面这段代码:
result = vPoint1.Equals(vPoint2); // #5 返回true; #6 返回true;
Console.WriteLine(result); // 输出true
因为它们均继承自ValueType类型,所以此时会调用ValueType上的Equals()方法,在方法体内部,#5 CanCompareBits(this) 返回了true,CanCompareBits(this)这个方法,按微软的注释,意识是说:如果对象的成员中存在对于堆上的引用,那么返回false,如果不存在,返回true。按照ValPoint的定义,它仅包含一个int类型的字段x,自然不存在对堆上其他对象的引用,所以返回了true。从#5 的名字CanCompareBits,可以看出是判断是否可以进行按位比较,那么返回了true以后,#6 自然是进行按位比较了。
接下来,我们对vPoint2做点改动,看看会发生什么:
vPoint2.x = 2;
result = vPoint1.Equals(vPoint2); // #5 返回true; #6 返回false;
Console.WriteLine(result);
3. 复杂值类型判等
到现在,上面的这些方法,我们还没有走到的位置,就是CanCompareBits返回false以后的部分了。前面我们已经推测出了CanCompareBits返回false的条件(值类型的成员包含引用类型),现在只要实现下就可以了。我们定义一个新的结构Line,它代表直线上的线段,我们让它的一个成员为值类型ValPoint,一个成员为引用类型RefPoint,然后去作比较。
/* 结构类型 ValLine 的定义,
public struct ValLine {
public RefPoint rPoint; // 引用类型成员
public ValPoint vPoint; // 值类型成员
public Line(RefPoint rPoint, ValPoint vPoint) {
this.rPoint = rPoint;
this.vPoint = vPoint;
}
}
*/
RefPoint rPoint = new RefPoint(1);
ValPoint vPoint = new ValPoint(1);
ValLine line1 = new ValLine (rPoint, vPoint);
ValLine line2 = line1;
result = line1.Equals(line2); // 此时已经存在一个装箱操作,调用ValueType.Equals()
Console.WriteLine(result); // 返回True
这个例子的过程要复杂得多。在开始前,我们先思考一下,当我们写下 line1.Equals(line2)时,已经进行了一个装箱的操作。如果要进一步判等,显然不能去判断变量是否引用的堆上同一个对象,这样的话就没有意义了,因为总是会返回false(装箱后堆上创建了两个对象)。那么应该如何判断呢?对 堆上对象 的成员(字段)进行一对一的比较,而成员又分为两种类型,一种是值类型,一种是引用类型。对于引用类型,去判断是否引用相等;对于值类型,如果是简单值类型,那么如同前一节讲述的去判断;如果是复杂类型,那么当然是递归调用了;最终直到要么是引用类型要么是简单值类型。
NOTE:进行字段对字段的一对一比较,需要用到反射,如果不了解反射,可以参看 .Net 中的反射 系列文章。
好了,我们现在看看实际的过程,是不是如同我们料想的那样,为了避免频繁的拖动滚动条查看ValueType的Equals()方法,我拷贝了部分下来:
public override bool Equals (Object obj) {
if (CanCompareBits(this)) // #5
return FastEqualsCheck(thisObj, obj); // #6
// 利用反射获取类型的所有字段(或者叫类型成员)
FieldInfo[] thisFields = thisType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
// 遍历字段进行比较
for (int i=0; i
thisResult = ((RtFieldInfo)thisFields[i]).InternalGetValue(thisObj,false);
thatResult = ((RtFieldInfo)thisFields[i]).InternalGetValue(obj, false);
if (thisResult == null) {
if (thatResult != null)
return false;
}
else
if (!thisResult.Equals(thatResult)) { #7
return false;
}
}
return true;
}
- 进入 ValueType 上的 Equals() 方法,#5 处返回了 false;
- 进入 for 循环,遍历字段。
- 第一个字段是RefPoint引用类型,#7 处,调用 System.Object 的Equals()方法,到达#2,返回true。
- 第二个字段是ValPoint值类型,#7 处,调用 System.ValType的Equals()方法,也就是当前方法本身。此处递归调用。
- 再次进入 ValueType 的 Equals() 方法,因为 ValPoint 为简单值类型,所以 #5 CanCompareBits 返回了true,接着 #6 FastEqualsCheck 返回了 true。
- 里层 Equals()方法返回 true。
- 退出 for 循环。
- 外层 Equals() 方法返回 true。