C#类型基础Part2-对象判等

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/874455.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL定时备份数据,并上传到oss

1.环境准备 1.安装阿里云的ossutil 2.安装mysql 2.编写脚本 脚本内容如下 #!/bin/bash # 数据库的配置信息&#xff0c;根据自己的情况进行填写 db_hostlocalhost db_usernameroot db_passwordroot db_namedb_root # oss 存贮数据的bucket地址 bucket_namerbsy-backup-buck…

软件更新的双刃剑:从”微软蓝屏”事件看网络安全的挑战与对策

引言 原文链接 近日&#xff0c;一场由微软视窗系统软件更新引发的全球性"微软蓝屏"事件震惊了整个科技界。这次事件源于美国电脑安全技术公司"众击"提供的一个带有"缺陷"的软件更新&#xff0c;如同一颗隐形炸弹在全球范围内引爆&#xff0c;…

/var/log/nginx/error.log

日志 : *1979 recv() failed (104: Connection reset by peer) while proxying and reading from upstream, client: 127.0.0.1, server: 0.0.0.0:60443, upstream: “10.x.x.x:6443”, bytes from/to client:64534240/731320118, bytes from/to upstream:731320118/64534240 …

Qt 实战(7)元对象系统 | 7.5、QMetaProperty详解

文章目录 一、QMetaProperty详解1、QMetaProperty的作用2、使用QMetaProperty2.1、声明属性2.2、访问属性 3、QMetaProperty成员方法4、示例4.1、通过名称获取指定属性4.2、遍历全部属性&#xff08;包含从基类继承下来的&#xff09;4.3、遍历当前类的全部属性&#xff08;不包…

Python面试宝典第17题:Z字形变换

题目 将一个给定字符串 s 根据给定的行数numRows &#xff0c;以从上往下、从左到右进行Z字形排列。比如&#xff1a;输入字符串为"PAYPALISHIRING"&#xff0c;行数为3时&#xff0c;排列如下。最后&#xff0c;你的输出需要从左往右逐行读取&#xff0c;产生出一个…

unity 实现图片的放大与缩小(根据鼠标位置拉伸放缩)

1创建UnityHelper.cs using UnityEngine.Events; using UnityEngine.EventSystems;public class UnityHelper {/// <summary>/// 简化向EventTrigger组件添加事件的操作。/// </summary>/// <param name"_eventTrigger">要添加事件监听的UI元素上…

DevExpress中文教程 - 如何在.NET MAUI应用中实现Material Design 3?

DevExpress .NET MAUI多平台应用UI组件库提供了用于Android和iOS移动开发的高性能UI组件&#xff0c;该组件库包括数据网格、图表、调度程序、数据编辑器、CollectionView和选项卡组件等。 获取DevExpress v24.1正式版下载 Material Design是一个由Google开发的跨平台指南系统…

HydraRPC: RPC in the CXL Era——论文阅读

ATC 2024 Paper CXL论文阅读笔记整理 问题 远程过程调用&#xff08;RPC&#xff09;是分布式系统中的一项基本技术&#xff0c;它允许函数在远程服务器上通过本地调用执行来促进网络通信&#xff0c;隐藏底层通信过程的复杂性简化了客户端/服务器交互[15]。RPC已成为数据中心…

【Hot100】LeetCode—279. 完全平方数

目录 题目1- 思路2- 实现⭐完全平方数——题解思路 3- ACM 实现 题目 原题连接&#xff1a;279. 完全平方数 1- 思路 思路 动规五部曲 2- 实现 ⭐完全平方数——题解思路 class Solution {public int numSquares(int n) {// 1. 定义 dpint[] dp new int[n1];//2. 递推公式…

Mojo编程语言

Mojo编程语言作为一种新兴的、专为AI开发者设计的编程语言&#xff0c;近年来在AI领域引起了广泛关注&#xff0c;并逐渐成为AI开发者的新宠儿。以下是对Mojo编程语言的详细解析&#xff1a; 设计目的与特点 Mojo编程语言由Modular公司开发&#xff0c;旨在结合Python的易用性…

算法学习5——图算法

图算法在计算机科学中占有重要地位&#xff0c;广泛应用于网络连接、路径查找、社会网络分析等领域。本文将介绍几种常见的图算法&#xff0c;包括Dijkstra算法、Bellman-Ford算法、Floyd-Warshall算法、Kruskal算法和Prim算法&#xff0c;并提供相应的Python代码示例。 图的基…

在 WSL2 中频繁切换 PHP 版本,可以使用更简便的方法

在 WSL2 中频繁切换 PHP 版本&#xff0c;可以使用更简便的方法&#xff0c;例如使用 update-alternatives 工具。这是一种更系统化的方法&#xff0c;允许你更方便地管理和切换不同的 PHP 版本。 以下是使用 update-alternatives 工具切换 PHP 版本的步骤&#xff1a; 添加 P…

论文学习记录之一种具有边缘增强特点的医学图像分割网络

标题&#xff1a;一种具有边缘增强特点的医学图像分割网络 期刊&#xff1a;电子与信息学报-&#xff08;2022年5月出刊&#xff09; 摘要&#xff1a;针对传统医学图像分割网络存在边缘分割不清晰、缺失值大等问题&#xff0c;该文提出一种具有边缘增强特点的医学图像分割网…

社交圈子小程序搭建-源码部署-服务公司

消息通知:当有新的消息、评论或回复时&#xff0c;用户需要收到系统的推送通知&#xff0c;以便及时查看和回复 活动发布与参加:用户可以在社交圈子中发布各种类型的活动&#xff0c;如聚餐、旅游、运动等。其他用户可以参加这些活动&#xff0c;并与组织者进行交流和沟通 社交…

C#初级——输出语句和转义字符

输出语句 在C#中&#xff0c;C#的输出语句是通过Console类进行输出&#xff0c;该类是一个在控制台下的一个标准输入流、输出流和错误流。使用该类下的Write()函数&#xff0c;即可打印要输出的内容。 Console.Write("Hello World!"); //在控制台应用中打印Hell…

通过QT进行服务器和客户端之间的网络通信

客户端 client.pro #------------------------------------------------- # # Project created by QtCreator 2024-07-02T14:11:20 # #-------------------------------------------------QT core gui network #网络通信greaterThan(QT_MAJOR_VERSION, 4): QT widg…

Docker安装nacos(详细教程)

Nacos 是一个开源的动态服务发现、配置管理和服务管理平台&#xff0c;广泛用于微服务架构中。在本文章中&#xff0c;博主将详细介绍如何使用 Docker 来安装 Nacos&#xff0c;以便快速启动并运行这个强大的服务管理工具。 前置条件 在开始安装 Nacos 之前&#xff0c;请确保…

解决union all之后字段返回非该字段类型的值

首先明确一个概念&#xff0c;union all的两部分的结果表的字段必须名称&#xff0c;类型&#xff0c;位置的先后都完全一样才可以 我的错误&#xff1a;一个datetime类型的字段&#xff0c;单独查询没问题&#xff0c;union all之后却返回了0 原因&#xff1a;字段顺序问题 …

DP(7) | 打家劫舍① | Java | LeetCode 198, 213, 337 做题总结

打家劫舍问题 来源于代码随想录&#xff1a;https://programmercarl.com/0198.%E6%89%93%E5%AE%B6%E5%8A%AB%E8%88%8D.html#%E6%80%9D%E8%B7%AF ① 确定dp数组&#xff08;dp table&#xff09;以及下标的含义 dp[i]&#xff1a;考虑下标i&#xff08;包括i&#xff09;以内的房…

pytorch 笔记:torch.optim.Adam

torch.optim.Adam 是一个实现 Adam 优化算法的类。Adam 是一个常用的梯度下降优化方法&#xff0c;特别适合处理大规模数据集和参数的深度学习模型 torch.optim.Adam(params, lr0.001, betas(0.9, 0.999), eps1e-08, weight_decay0, amsgradFalse, *, foreachNone, maximizeFa…