.Net中关于相等的问题
在.Net框架中,如果您查看所有类型的的基类:System.Object类,将找到如下4个与相等判断的方法:
除此之外,Microsoft已经提供了9个不同的接口,用于比较类型:
- IEquatable<T>
- IComparable
- IComparable<T>
- IComparer
- IComparer<T>
- IEqualityComparer
- IEqualityComparer<T>
- IStructuralEquatable
- IStructuralComparable
您是否真的理解方法这些方法和接口?如果使用不当,可能会产生致命的错误,并且还会破坏依赖于这些接口的集合。
接下来我们几篇博客来讨论这些方法和接口,重点关注的是如何正确使用这些方法和接口。
等于的疑惑
因为存在以下四种原因,会阻碍我们理解相等比较是如何执行:
- 引用相等与值相等
- 判断值相等的多种方式
- 浮点数的准确性
- 与OOP存在的冲突
引用相等与值相等
众所周知,在.Net框架中,引用类型在存储时不包含实际的值,它们包含一个指向内存中保存实际值位置的指针,这意味着对于引用类型,有两种方式来衡量相等性;两个变量都是指向内存中相同的位置,我们称为引用相等,也可以说是同一个对象;两个变量指定的位置包括相同的值, 即使它们指向内存中不同的位置,我们称其之为值相等。
我们可以使用如下示例来说明上述几点:
class Program
{
static void Main(String[] args)
{
Person p1 = new Person();
p1.Name = "Sweet"; Person p2 = new Person();
p2.Name = "Sweet"; Console.WriteLine(p1 == p2);
}
}
我们实例化了两个Person对象,并且都包含相同的Name属性;显然,上述两个Person类的实例是相同的,它们包含相同的值, 但是运行示例代码时,控制台打印输出的是False,这意味着它们不相等。
这是因为在.Net框架中,对于引用类型默认判断方式是引用相等,换句话说,"=="运算符会判断这两个变量是否指向内存中相同的位置,因此在本示例中,尽管Person类的两个实例包含的值相同,但它们是单独的实例,变量p1和p2两者分别指内存不同的位置。
引用相等执行速度非常快,因为只需检查两个变量是否指向内存中相同的地址,而对于值相等要慢一些。例如,如果Person类不是只有一个字段和属性,而是具有很多,想检查Person类的两个实例是否具有相同的值,您必须检查每个字段或属性。C#中并没有提供运算符用于检查两个类型实例的值是否相等,如果由于某种原因想要实现这种功能,您需要自己编写代码来做到这一点。
现在来看另一个例子:
class Program
{
static void Main(String[] args)
{
string s1 = "Sweet"; string s2 = string.Copy(s1); Console.WriteLine(s1 == s2);
}
}
上面的代码与前一个示例代码非常相拟,但是在这个示例中,我们使用"=="运算符判断两个相同的String类型的变量。我们先给变量s1付值后,然后将变量s1的值复制并付给另一个变量s2,运行这段代码,在控制台打印输出为True,我们可以说两个String类型的变量是相等的。
如果"=="运算符判断的方式使用的是引用相等, 程序运行时控制台打印输出的应该是False,但是用于String类型时"==" 运算符判断方式是值相等。
引用相等与值类型
引用相等和值相等的问题仅适用于引用类型,对于未装箱的值类型,如整数,浮点型等,变量存储时已经包含了实际的值,这里没有引用的概念,意味着相等就是比较值。
以下代码比较两个整数,两者是相等的,因为"=="运算符将比较变量实际的值。
class Program
{
static void Main(String[] args)
{
int num1 = ; int num2 = ; Console.WriteLine(num1 == num2);
}
}
在上面的代码中,"=="运算符是将变量num1存储的值与变量num2存储的值进行比较。但是,如果我们修改此代码并将这两个变量转换为Object类型,代码如下:
int num1 = ; int num2 = ; Console.WriteLine((object)num1 == (object)num2);
运行示例代码,您看到结果将是False,与上一次代码的结果相反。这是因为Object类型是引用类型,所以当我们将整数转换为Object类型,实际是两个整数被装箱后两个不同的引用实例,"=="运行符比较的是两个对象的引用,而不是值。
好像上面的例子很少见,因为通常情况下我们不会将值类型转换为引用类型,但是存在另一种常见的情况,我们需要将值类型转换为接口。
Console.WriteLine((IComparable<int>)num1 == (IComparable<int>)num2);
为了说明这种情况,我们修改示例代码,将int类型的变量转换为接口ICompareable<int>;这是.Net框架提供的一个接口,int类型实现这个接口(关于这个接口我们将其它的博客中讨论)。
在.Net框架中,接口实际上是引用类型,如果我们运行这段代码,返回的结果是False。因此,在将值类型转换为接口时,您需要特别小心,如果您进行相等检查,返回的结果比较的是引用相等。
"=="运算符
如果C#对值类型和引用类型分别提供不同的运算符来判断相等,也许这些代码都不是问题,可惜C#只提供一个"=="运算符,也没有显示的方式来告诉运算符实际判断的类型是什么。例如,下面这一行代码:
Console.WriteLine(var1 == var2)
我们不知道上述的"=="运算符采用的是引用相等还是值相等,因为需要知道"=="运行算判断的是什么类型,事实上C#也是这样设计的。
在上述内容中,我们详细介绍了"=="运算符的作用及判断方式,在阅读完这篇博客之后,我希望您能比其他开发者更多的了解当使用"=="判断条件的时候到底发生了什么,您也能够更进一步了解两个对象之间的是如何判断相等的。
判断值相等的多种方式
复杂的值相等的还存在另一个问题,通常存在多种方式来比较指定类型的值,String类型是一个最好的例子。
经常存在这样一种情况,字符串比较时,可能需要忽略字母的大小写;例如:在一个电商平台中搜索一个英文名称的商品,此时比较商品名称时,我们需要忽略大小写,幸运的是在Sql Server数据库中,默认使用的是这种比较方式,在.Net框架中有没有办法满足我们的要求?幸运的是在String类型中提供了一个Equals方法的重载,看下面的示例:
string s1 = "SWEET"; string s2 = "sweet"; Console.WriteLine(s1.Equals(s2,StringComparison.OrdinalIgnoreCase));
在程序中运行上面的示例,在控制台打印输出的是True。
当然.Net框架也提供了多种方式来判断类型的值相等。最常见方法,类型可以通过实现IEquatable<T>接口定义类型默认值相等的判断方式。如果您不想重新定义自己的类型,.Net框架也提供了其另一种机制来实现一点,通过实现IEqualityComparer<T>接口来自定义一个比较器,用于判断同一种类型的两个实例是否相等。例如:如果您想忽略String类型中的空格进行比较,可以自己定义一个比较器,来实现这一功能。
.Net还提供了一个接口ICompareable<T>,用于判断当前类型大于或小于的比较,也可以通过IComparer<T>接口来实现一个比对器,一般在对象排序时,会用到这些接口。
浮点数的准确性
在.Net框架中,您如果使用到浮点数,可以带来一些意想不到的问题,让我们来看一个例子:
float num1 = 2.000000f;
float num2 = 2.000001f; Console.WriteLine(num1 == num2);
我们有两个几乎相等的浮点数,但是很明显,它们不一样,因为它们在末尾的数字是不同的,我们运行程序,控制台打印输出的结果是True。
从程序来角度来讲,它们是相等的,这与我们预期结果矛盾。不过您可能已经猜测到问题出在哪里了,数字类型存在一个精度问题,float类型不能存储足够的有效数来区分这两个特定的数字,并且它还存在其它运算的问题。看这个例子:
float num1 = 0.7f;
float num2 = 0.6f + 0.1f; Console.WriteLine(num2);
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来表示狗。
public class Animal
{ } public class Dog : Animal
{ }
如果我们希望在Animal类实现当前实例是否等于其它Animal实例,则可能需要实现接口IEquatable<Animal>。这要求它定义一个Equals()方法并以Animal类型的实例作为参数。
public class Animal : IEquatable<animal>
{
public virtual bool Equals(Animal other)
{
throw new NotImplementedException();
}
}
如果我们希望Dog类也实现当前实例是否等于其它Dog实例,那么可能需要实现接口IEquatable<Dog>,这意味着它也定义一个Equals()方法并以Dog类型的实例作为参数。
public class Dog : Animal, IEquatable<Dog>
{
public virtual bool Equals(Dog other)
{
throw new NotImplementedException();
}
}
现在问题出现了,在这个一个精心设计的OOP代码中,您可能会认为Dog类会覆盖Animal类的Equals()方法,但是麻烦的是Dog的Equals()方法与Animal类的Equals()方法使用的是不同的参数类型,实际是重写不了Animal类的Equals()方法。如果您不够仔细,可能会调用错误的Equals方法,最终返回错误的结果。
通常的解决办法是重写Object类型Equals方法;该方法采用一个Object类型为参数类型,这意味着它不是类型安全的,但它能够正常重写基类的方法,并且这也是最简单的解决办法。
总结
- C#在语法上不区分值相等和引用相等,这意味着有时候很难预测在特定情况下"
=="运算符是如何执行; - 存在多种方式实现值相等判断,.Net框架允许类型定义默认的值比较方式,同时提供自己编写比较器的机制来实现每种类型的值比较;
- 不建议使用浮点数进行值相等比较,因为舍入误差可能导致结果超出预期;
- 值相等、类型安全和良好的面向对象之间存在冲突。
转载请注明出自,原文链接:http://www.cnblogs.com/tdfblog/p/About-Equality-in-NET.html
.Net中关于相等的问题的更多相关文章
- Python开源框架
info:更多Django信息url:https://www.oschina.net/p/djangodetail: Django 是 Python 编程语言驱动的一个开源模型-视图-控制器(MVC) ...
- mapreduce中一个map多个输入路径
package duogemap; import java.io.IOException; import java.util.ArrayList; import java.util.List; imp ...
- Hadoop 中利用 mapreduce 读写 mysql 数据
Hadoop 中利用 mapreduce 读写 mysql 数据 有时候我们在项目中会遇到输入结果集很大,但是输出结果很小,比如一些 pv.uv 数据,然后为了实时查询的需求,或者一些 OLAP ...
- Python中的多进程与多线程(一)
一.背景 最近在Azkaban的测试工作中,需要在测试环境下模拟线上的调度场景进行稳定性测试.故而重操python旧业,通过python编写脚本来构造类似线上的调度场景.在脚本编写过程中,碰到这样一个 ...
- .NET Core中的认证管理解析
.NET Core中的认证管理解析 0x00 问题来源 在新建.NET Core的Web项目时选择“使用个人用户账户”就可以创建一个带有用户和权限管理的项目,已经准备好了用户注册.登录等很多页面,也可 ...
- Angular杂谈系列1-如何在Angular2中使用jQuery及其插件
jQuery,让我们对dom的操作更加便捷.由于其易用性和可扩展性,jQuer也迅速风靡全球,各种插件也是目不暇接. 我相信很多人并不能直接远离jQuery去做前端,因为它太好用了,我们以前做的东西大 ...
- 关于CryptoJS中md5加密以及aes加密的随笔
最近项目中用到了各种加密,其中就包括从没有接触过得aes加密,因此从网上各种查,官方的一种说法: 高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学 ...
- In-Memory:在内存中创建临时表和表变量
在Disk-Base数据库中,由于临时表和表变量的数据存储在tempdb中,如果系统频繁地创建和更新临时表和表变量,大量的IO操作集中在tempdb中,tempdb很可能成为系统性能的瓶颈.在SQL ...
- SQL Server中的高可用性(2)----文件与文件组
在谈到SQL Server的高可用性之前,我们首先要谈一谈单实例的高可用性.在单实例的高可用性中,不可忽略的就是文件和文件组的高可用性.SQL Server允许在某些文件损坏或离线的情况下,允 ...
- 【.net 深呼吸】序列化中的“引用保留”
假设 K 类中有两个属性/字段的类型相同,并且它们引用的是同一个对象实例,在序列化的默认处理中,会为每个引用单独生成数据. 看看下面两个类. [DataContract] public class 帅 ...
随机推荐
- MyBatis7:MyBatis插件及示例----打印每条SQL语句及其执行时间
Plugins 摘一段来自MyBatis官方文档的文字. MyBatis允许你在某一点拦截已映射语句执行的调用.默认情况下,MyBatis允许使用插件来拦截方法调用 Executor(update.q ...
- sql拼接,String和Stringbuffer的问题
首先提出来一个问题: 下边两种拼字符串的方式,哪种更好一些,或者还有更好的方式? StringBuffer hql=new StringBuffer(); hql.append("from ...
- 借用mysql 或者其他数据库 处理MSSQL 2016前处理导入特殊字符
MSSQL 2016支持了utf8编码的文件,之前处理比较麻烦的bcp 方式导入特殊字符一下子就方便了. 但是之前的版本,处理起来还是有一点麻烦.这次处理使用的数据库版本是sql server 201 ...
- 安装并配置Apache
从今天开始,我将开始Web开发的学习.本系列博客将陆续记录我学习过程中的收获和困惑,从前端到后端,一探Web开发的流程和内容.我目前掌握的有C/C++,有一些使用C进行嵌入式开发的经验,C++就马马虎 ...
- Linux下串口通信工具minicom的用法
一.查看串口设备 例如,将USB转串口线插入交换机Console口后,执行命令:$ll /dev/ttyUSB* 二.连接串口设备 $sudo minicom -D /dev/ttyUSB0 三.设置 ...
- java入门学习笔记之2(Java中的字符串操作)
因为对Python很熟悉,看着Java的各种字符串操作就不自觉的代入Python的实现方法上,于是就将Java实现方式与Python实现方式都写下来了. 先说一下总结,Java的字符串类String本 ...
- Java中的事件监听机制
鼠标事件监听机制的三个方面: 1.事件源对象: 事件源对象就是能够产生动作的对象.在Java语言中所有的容器组件和元素组件都是事件监听中的事件源对象.Java中根据事件的动作来区分不同的事件源对象,动 ...
- 让textarea和附近的文字居中对齐
textarea {display:inline-block;vertical-align:middle;}
- 数据的ID名生成新的引用索引树
<?php $arr= [ '0'=>[ "id"=>2, "name"=>"建材", "pid" ...
- jquery判断对象是否显示或隐藏
if($('a.specail2').is(":visible")){ /**jquery判断对象是否显示或隐藏**/ $('a.one').hide(); $('a.specai ...