C#类型基础Part2-对象判等
- 参考资料
- 引用类型判等
- 简单值类型判等
- 复杂值类型判等
参考资料
- 《.NET之美-.NET关键技术深入解析》
引用类型判等
先定义两个类型,它们代表直线上的一个点,一个是引用类型class,一个是值类型struct
public class RefPoint{public int x:public RefPoint(int x){this.x=x;}
}public class ValPoint{public int x:public ValPoint(int x){this.x=x;}
}
在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) { // #3return true;}if (objA==null || objB==null) {return false;}return objA.Equals(objB); // #4
}
先看ReferenceEquals(Object objA,Object objB)方法,它实际上简单地返回
objA==objB。再观察一下Object.Equals()静态方法,如果任何一个对象引用为null,则总是
返回false。当对象不为null时,最后调用了实例上的Equals()方法(#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);
在这段代码中,在堆上创建了一个新的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,可以看到,对于引用类型,即使类型的实例(对象)包含的值相等,如果变量指向的是不同的对象,那么也不相等。
简单值类型判等
注意本节的标题:简单值类型判等,这个简单是如何定义的呢?如果值类型的成员仅包含值类型,那么暂且管它叫简单值类型;如果值类型的成员包含引用类型,则管它叫复杂值类型。
值类型都会隐式地继承自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) { // 如果两个对象不是一个类型,直接返回falsereturn false;}Object thisObj = (Object)this;Object thisResult, thatResult;if (CanCompareBits(this)) // #5return 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)) { // #7return 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,由于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自然是进行按位比较了。
接下来,对vPoint2做点改动,看看会发生什么:
vPoint2.x = 2;
result = vPoint1.Equals(vPoint2); // #5 返回true; #6 返回false;
Console.WriteLine(result);
此时,因为vPoint2中的int值发生了变化,所以在#6处按位比较时,就会返回false。
复杂值类型判等
到现在为止,上面的System.ValueType.Equals()方法,还没有执行到的位置,就是CanCompareBits返回false以后的部分了。前面已经推算出了CanCompareBits返回false的条件(值类型的成员包含引用类型),现在只要实现一下就可以了。重新定义一个新的结构ValLine,它代表直线上的线段,让它的一个成员为值类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(装箱后堆上创建了两个对象)。那么应该如何判断呢?对堆上对象的成员(字段)进行一对一的比较,而成员又分为两种类型,一种是值类型,一种是引用类型。对于引用类型,去判断是否引用相等;对于值类型,如果是简单值类型,那么同前一节讲述的一样去判断;如果是复杂类型,那么当然是递归调用了;
最终确定要么是引用类型要么是简单值类型。
好了,现在看看实际的过程,是不是如同我们所料想的那样,为了避免频繁地拖动滚动条查看ValueType的Equals()方法,这里将代码复制了部分:
public override bool Equals (Object obj) {//前面略if (CanCompareBits(this)) // #5return 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)) { #7return 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