因为对象复制(克隆)的一个前提条件就是:要能够知道复制前后的两个对象是否相 等,所以,在展开对象复制的内容前,有必要先了解如何进行对象判等。
1.2.1 引用类型判等
首先看一下引用类型对象的判等,大家知道在System.Object基类型中,定义了实例方 法Equals(Object obj),静态方法Equals(Object objA,Object objB),静态方法 ReferenceEquals(Object objA,Object objB) 这三个方法来进行对象的判等。
先看看这三个方法是如何实现的,注意在代码中用“#+数字”标识的地方,后文会直接 引用:
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
}
实例一:
// 复制对象引用
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);
在阅读本节时,应该时刻在脑子里构思一个栈和一个堆,并思考着每条语句会在这两种 结构上产生怎么样的效果。在这段代码中,产生的效果如图1-5所示:在堆上创建了一个新 的RefPoint类型的对象实例,并将它的x字段初始化为1;在栈上创建RefPoint类型的变量 rPoint1,rPoint1保存了堆上这个对象的地址;而将rPoint1赋值给rPoint2时,此时并没有在 堆上创建一个新的对象,而是将之前创建的对象的地址复制到了rPoint2。此时,rPoint1和 rPoint2指向了堆上同一个对象。
从ReferenceEquals()这个方法名就可以看出,它判断两个引用变量是不是指向了同一个 变量,如果是,那么就返回true。这种相等叫做引用相等(rPoint1==rPoint2等效于 ReferenceEquals)。因为它们指向的是同一个对象,所以对rPoint1的操作将会影响 rPoint2。
实例二:
//创建新引用类型的对象,其成员的值相等
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,可以看到,对于引用类型,即 使类型的实例(对象)包含的值相等,如果变量指向的是不同的对象,那么也不相等。
1.2.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<thisFields.Length; 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);
上面的代码先在栈上创建了一个变量vPoint1,由于ValPoint是结构类型,因此变量本身 已经包含了所有字段和数据。然后在栈上复制了vPoint1的一份副本给了vPoint2。如果依照 前面的惯性思维去考虑,那么就会认为它们应该是相等的。然而,接下来试着去比较它们, 就会看到,不能用“==”直接去判断,这样会返回一个编译错误“不能在ValPoint上应用==操 作符”。
如果调用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自然是进行按位比较了。
网友评论