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) { // #3
		return 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) { // 如果两个对象不是一个类型,直接返回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); // 返回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)) // #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;
}
  • 进入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

相关推荐

  1. C#类型基础Part2-对象

    2024-07-23 09:42:06       14 阅读
  2. C语言---指针part2

    2024-07-23 09:42:06       22 阅读
  3. C#中对象类型转换

    2024-07-23 09:42:06       29 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-23 09:42:06       52 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-23 09:42:06       54 阅读
  3. 在Django里面运行非项目文件

    2024-07-23 09:42:06       45 阅读
  4. Python语言-面向对象

    2024-07-23 09:42:06       55 阅读

热门阅读

  1. 量化机器人能否提高市场预测精度?

    2024-07-23 09:42:06       17 阅读
  2. ELK Stack入门之部署EFK架构

    2024-07-23 09:42:06       14 阅读
  3. uniapp刷新当前页面bug

    2024-07-23 09:42:06       17 阅读
  4. ArcGIS Pro SDK (九)几何 12 多面体

    2024-07-23 09:42:06       13 阅读
  5. 数据库连接池

    2024-07-23 09:42:06       15 阅读
  6. RKNN执行bash ./build-linux_RK3566_RK3568.sh 报错

    2024-07-23 09:42:06       14 阅读
  7. 数学建模--图论与最短路径

    2024-07-23 09:42:06       15 阅读
  8. mariadb安装在服务器(Linux)

    2024-07-23 09:42:06       15 阅读
  9. 银行卡二三四要素核验的多种应用场景

    2024-07-23 09:42:06       18 阅读
  10. 怎么在 Ubuntu 中卸载已经安装的软件

    2024-07-23 09:42:06       16 阅读