.Net中关于相等的问题

等于的疑惑

  因为存在以下四种原因,会阻碍我们理解相等比较是如何执行:

  1. 引用相等与值相等
  2. 判断值相等的多种方式
  3. 浮点数的准确性
  4. 与OOP存在的冲突

引用相等与值相等

  众所周知,在.Net框架中,引用类型在存储时不包含实际的值,它们包含一个指向内存中保存实际值位置的指针,这意味着对于引用类型,有两种方式来衡量相等性;两个变量都是指向内存中相同的位置,我们称为引用相等,也可以说是同一个对象;两个变量指定的位置包括相同的值, 即使它们指向内存中不同的位置,我们称其之为值相等。

   我们可以使用如下示例来说明上述几点:

 1 class Program 2 {  3     static void Main(String[] args) 4     { 5         Person p1 = new Person(); 6         p1.Name = "Sweet"; 7   8         Person p2 = new Person(); 9         p2.Name = "Sweet";10  11         Console.WriteLine(p1 == p2);12     }  13 }

  我们实例化了两个Person对象,并且都包含相同的Name属性;显然,上述两个Person类的实例是相同的,它们包含相同的值, 但是运行示例代码时,控制台打印输出的是False,这意味着它们不相等。

  这是因为在.Net框架中,对于引用类型默认判断方式是引用相等,换句话说,"=="运算符会判断这两个变量是否指向内存中相同的位置,因此在本示例中,尽管Person类的两个实例包含的值相同,但它们是单独的实例,变量p1p2两者分别指内存不同的位置。

  引用相等执行速度非常快,因为只需检查两个变量是否指向内存中相同的地址,而对于值相等要慢一些。例如,如果Person类不是只有一个字段和属性,而是具有很多,想检查Person类的两个实例是否具有相同的值,您必须检查每个字段或属性。C#中并没有提供运算符用于检查两个类型实例的值是否相等,如果由于某种原因想要实现这种功能,您需要自己编写代码来做到这一点。

  现在来看另一个例子:

 1 class Program 2 {  3     static void Main(String[] args) 4     { 5         string s1 = "Sweet"; 6   7         string s2 = string.Copy(s1); 8   9         Console.WriteLine(s1 == s2);10      } 11  }

  上面的代码与前一个示例代码非常相拟,但是在这个示例中,我们使用"=="运算符判断两个相同的String类型的变量。我们先给变量s1付值后,然后将变量s1的值复制并付给另一个变量s2,运行这段代码,在控制台打印输出为True,我们可以说两个String类型的变量是相等的。

  如果"=="运算符判断的方式使用的是引用相等, 程序运行时控制台打印输出的应该是False,但是用于String类型时"==" 运算符判断方式是值相等。

引用相等与值类型

  引用相等和值相等的问题仅适用于引用类型,对于未装箱的值类型,如整数,浮点型等,变量存储时已经包含了实际的值,这里没有引用的概念,意味着相等就是比较值。

  以下代码比较两个整数,两者是相等的,因为"=="运算符将比较变量实际的值。

 1 class Program 2 { 3      static void Main(String[] args) 4     { 5         int num1 = 2; 6   7         int num2 = 2; 8   9         Console.WriteLine(num1 == num2);10     } 11 }

  在上面的代码中,"=="运算符是将变量num1存储的值与变量num2存储的值进行比较。但是,如果我们修改此代码并将这两个变量转换为Object类型,代码如下:

1 int num1 = 2;2  3 int num2 = 2;4  5 Console.WriteLine((object)num1 == (object)num2);

  运行示例代码,您看到结果将是False,与上一次代码的结果相反。这是因为Object类型是引用类型,所以当我们将整数转换为Object类型,实际是两个整数被装箱后两个不同的引用实例,"=="运行符比较的是两个对象的引用,而不是值。

  好像上面的例子很少见,因为通常情况下我们不会将值类型转换为引用类型,但是存在另一种常见的情况,我们需要将值类型转换为接口。

1 Console.WriteLine((IComparable<int>)num1 == (IComparable<int>)num2); 

  为了说明这种情况,我们修改示例代码,将int类型的变量转换为接口ICompareable<int>;这是.Net框架提供的一个接口,int类型实现这个接口(关于这个接口我们将其它的博客中讨论)。

  在.Net框架中,接口实际上是引用类型,如果我们运行这段代码,返回的结果是False因此,在将值类型转换为接口时,您需要特别小心,如果您进行相等检查,返回的结果比较的是引用相等。

"=="运算符

  如果C#对值类型和引用类型分别提供不同的运算符来判断相等,也许这些代码都不是问题,可惜C#只提供一个"=="运算符,也没有显示的方式来告诉运算符实际判断的类型是什么。例如,下面这一行代码:

1 Console.WriteLine(var1 == var2)

  我们不知道上述的"=="运算符采用的是引用相等还是值相等,因为需要知道"=="运行算判断的是什么类型,事实上C#也是这样设计的。

  在上述内容中,我们详细介绍了"=="运算符的作用及判断方式,在阅读完这篇博客之后,我希望您能比其他开发者更多的了解当使用"=="判断条件的时候到底发生了什么,您也能够更进一步了解两个对象之间的是如何判断相等的。

判断值相等的多种方式

  复杂的值相等的还存在另一个问题,通常存在多种方式来比较指定类型的值,String类型是一个最好的例子。

  经常存在这样一种情况,字符串比较时,可能需要忽略字母的大小写;例如:在一个电商平台中搜索一个英文名称的商品,此时比较商品名称时,我们需要忽略大小写,幸运的是在Sql Server数据库中,默认使用的是这种比较方式,在.Net框架中有没有办法满足我们的要求?幸运的是在String类型中提供了一个Equals方法的重载,看下面的示例:

1 string s1 = "SWEET";2 3 string s2 = "sweet";4 5 Console.WriteLine(s1.Equals(s2,StringComparison.OrdinalIgnoreCase));

  在程序中运行上面的示例,在控制台打印输出的是True

  当然.Net框架也提供了多种方式来判断类型的值相等。最常见方法,类型可以通过实现IEquatable<T>接口定义类型默认值相等的判断方式。如果您不想重新定义自己的类型,.Net框架也提供了其另一种机制来实现一点,通过实现IEqualityComparer<T>接口来自定义一个比较器,用于判断同一种类型的两个实例是否相等。例如:如果您想忽略String类型中的空格进行比较,可以自己定义一个比较器,来实现这一功能。
  .Net还提供了一个接口ICompareable<T>,用于判断当前类型大于或小于的比较,也可以通过IComparer<T>接口来实现一个比对器,一般在对象排序时,会用到这些接口。

浮点数的准确性

  在.Net框架中,您如果使用到浮点数,可以带来一些意想不到的问题,让我们来看一个例子:

1 float num1 = 2.000000f;2 float num2 = 2.000001f;3 4 Console.WriteLine(num1 == num2);

  我们有两个几乎相等的浮点数,但是很明显,它们不一样,因为它们在末尾的数字是不同的,我们运行程序,控制台打印输出的结果是True

  从程序来角度来讲,它们是相等的,这与我们预期结果矛盾。不过您可能已经猜测到问题出在哪里了,数字类型存在一个精度问题,float类型不能存储足够的有效数来区分这两个特定的数字,并且它还存在其它运算的问题。看这个例子:

1 float num1 = 0.7f;2 float num2 = 0.6f + 0.1f;3 4 Console.WriteLine(num2);5 Console.WriteLine(num1 == num2);

  这是一个简单的计算,我们将0.6与0.1相加,非常明显,相加后的结果是0.7,但是我们运行程序,控制台打印输出的结果是False,注意结果是False,这说明计算结果不等于0.7。其原因是,浮点数在运算的过程中出现了舍入误差导致了存储一个非常接近的数字,虽然num2转换成String类型后,在控制台打印输出的结果是0.7,但是num2的值并不等于0.7。

  

  舍入误差意味着判断相等通常会给您一个错误的结果,.Net框架没有提供解决方案。给您的建议是,不要尝试比较浮点数是否相等,因为可能不是预期结果。这个问题只会影响等于比较,通常不会影响小于和大于比较,在大多数情况下,比较一个浮点数是大于还是小于另一个浮点数不会出该问题。

  在stackoverflow上提供这样一个解决办法,供大家参考:https://stackoverflow.com/questions/6598179/the-right-way-to-compare-a-system-double-to-0-a-number-int。

值相等与面向对象之间的矛盾

  这个问题对经验丰富的开发人员来说可能会感到很诧异,实际上这是等于比较、类型安全和良好的面向对象实践之间的冲突。这三个问题如果没有处理好,将会带来其它的Bug。

  现在我们来举这样一个例子,假设我们有基类Animal表示动物,派生类Dog来表示狗。

1 public class Animal2 {3 4 }5 6 public class Dog : Animal7 {8 9 }

  如果我们希望在Animal类实现当前实例是否等于其它Animal实例,则可能需要实现接口IEquatable<Animal>。这要求它定义一个Equals()方法并以Animal类型的实例作为参数。

1 public class Animal : IEquatable<animal>2 {3     public virtual bool Equals(Animal other)4     {5         throw new NotImplementedException();6     }7 }

  如果我们希望Dog类也实现当前实例是否等于其它Dog实例,那么可能需要实现接口IEquatable<Dog>,这意味着它也定义一个Equals()方法并以Dog类型的实例作为参数。

1 public class Dog : Animal, IEquatable<Dog>2 {3     public virtual bool Equals(Dog other)4     {5         throw new NotImplementedException();6     }7 }

  现在问题出现了,在这个一个精心设计的OOP代码中,您可能会认为Dog类会覆盖Animal类的Equals()方法,但是麻烦的是DogEquals()方法与Animal类的Equals()方法使用的是不同的参数类型,实际是重写不了Animal类的Equals()方法。如果您不够仔细,可能会调用错误的Equals方法,最终返回错误的结果。

  通常的解决办法是重写Object类型Equals方法;该方法采用一个Object类型为参数类型,这意味着它不是类型安全的,但它能够正常重写基类的方法,并且这也是最简单的解决办法。

时间: 2024-07-30 02:32:01

.Net中关于相等的问题的相关文章

win10周年版eNSP中启动AR提示错误代码40问题

win 10操作系统中安装eNSP 1.2.00.380,一直运行正常,但在2016年11月升级win 周年版之后,启动AR时启动失败,提示错误代码40. 卸载eNSP及VirtualBox之后重装问题依旧.按照论坛和网上各种说法更新virtualbox修改虚拟网卡设置,或者重新注册都无法解决,最终多方查找终于找到解决方案. 环境:win10 周年版,eNSP 1.2.00.380,VirtualBox 4.2.8 eNSP注册后virtualbox管理器中会出现AR_Base,WLAN_AC_

css中的px、em、rem 详解

概念介绍: 1.px (pixel,像素):是一个虚拟长度单位,是计算机系统的数字化图像长度单位,如果px要换算成物理长度,需要指定精度DPI(Dots Per Inch,每英寸像素数),在扫描打印时一般都有DPI可选.Windows系统默认是96dpi,Apple系统默认是72dpi. 2.em(相对长度单位,相对于当前对象内文本的字体尺寸):是一个相对长度单位,最初是指字母M的宽度,故名em.现指的是字符宽度的倍数,用法类似百分比,如:0.8em, 1.2em,2em等.通常1em=16px

angularJs中关于ng-class的三种使用方式说明

在开发中我们通常会遇到一种需求:一个元素在不同的状态需要展现不同的样子. 而在这所谓的样子当然就是改变其css的属性,而实现能动态的改变其属性值,必然只能是更换其class属性 这里有三种方法: 第一种:通过数据的双向绑定(不推荐) 第二种:通过对象数组 第三种:通过key/value 下面简单说下这三种: 第一种:通过数据的双向绑定 实现方式: function changeClass(){   $scope.className = "change2"; } <div clas

Uploadify/uploadifive上传(中文文档)

Uploadify是一款基于JQuery的优秀的文件/图片上传的插件,有基于Flash和HTML5两种版本. Uploadify/uploadifive主要特点有: 1. 多文件上传 2. 个性化设置 3. 上传进度条显示 4. 拖拽上传(HTML5版本) 官网:http://www.uploadify.com 部署 在部署一个Uploadify实例前,请确保满足最低要求: 1.jQuery 1.4.x 或更高版本 2.Flash Player 9.0.24 或更高版本 3.支持PHP, ASP

XShell 连接虚拟机中的服务器 失败 、连接中断(Connection closed by foreign host.)

在使用XShell连接虚拟机中的服务器时,报以下错误并断开连接,之前连接还是挺稳定的,忽然就这样了 Last login: Thu Aug 10 21:28:38 2017 from 192.168.1.102 [[email protected] ~]# Socket error Event: 32 Error: 10053. Connection closing...Socket close. Connection closed by foreign host. Disconnected f

微信浏览器中调用支付宝支付

众所周知,在微信浏览器中是无法唤起支付宝的,会提示请在浏览器中打开,如果非要在微信浏览器中调起支付宝的话,只能是跳出微信浏览器,关于这一点,在支付宝官网给出了一个例子.但是,话说回去,后来我仔细想想,其实真的没有必要非要在微信浏览器中调起支付宝支付(当时真是一根筋啊啊啊...) 支付宝手机网站支付的官方文档: https://doc.open.alipay.com/docs/doc.htm?treeId=203&articleId=105288&docType=1 快速接入: https:

C#中Dictionary的介绍

关键字:C# Dictionary 字典 作者:txw1958原文:http://www.cnblogs.com/txw1958/archive/2012/11/07/csharp-dictionary.html 说明    必须包含名空间System.Collection.Generic     Dictionary里面的每一个元素都是一个键值对(由二个元素组成:键和值)     键必须是唯一的,而值不需要唯一的     键和值都可以是任何类型(比如:string, int, 自定义类型,等等

C#中使用ffmpeg合并视频

首先将最新的ffmpeg.exe放到debug路径下,下载地址 http://www.ffmpeg.org/download.html 然后调用此方法 public void CombineMp4WithoutTxt(string StrMP4A, string StrMP4B, string StrOutMp4Path) { Process p = new Process();//建立外部调用线程 p.StartInfo.FileName = System.Windows.Forms.Appl

Javascript中call的使用

call 方法应用于:Function 对象调用一个对象的一个方法,以另一个对象替换当前对象.call([thisObj[,arg1[, arg2[,   [,.argN]]]]])参数:thisObj 可选项.将被用作当前对象的对象. arg1, arg2, , argN 可选项.将被传递方法参数序列. 说明:call 方法可以用来代替另一个对象调用一个方法.call 方法可将一个函数的对象上下文从初始的上下文改变为由 thisObj 指定的新对象.如果没有提供 thisObj 参数,那么 G

微信公众号中添加外部链接地址的图文教程

2017-9-18,长沙,有点闷,有点热. 本教程教大家如何在微信公众号中,添加外部的链接,网络有很多教程,但由于表述不太清楚,出个教程吧.最终实现在微信后台管理平台"原文链接"处插入外部链接,用户点击发布好的图文文章底部左下角的"阅读原文",就可以跳转到您添加的外部链接页面中去. 第1步. 使用微信公众管理帐号登陆微信管理后台 > 素材管理 > 图文消息 >  新建图文消息或者编辑文章都可以,如下图所示: 第2步. 之后,将页面向下拉,看到底部&