C# 相等比较

有两种类型的相等:

  • 值相等:即两个值是一样的
  • 引用相等:即引用是一样的,也就是同一个对象

默认地,对于值类型来讲,相等指的就是值相等;对于引用类型,相等就是指的引用相等。

int a = 5;
int b = 5;
Console.WriteLine(a == b);

class Foo { public int x; }
Foo f1 = new Foo { x = 5 };
Foo f2 = new Foo { x = 5 };
Console.WriteLine(f1 == f2);

标准相等协议

有三种标准相等协议:

  • ==!=运算符
  • object的虚函数Equals
  • `IEquatablee接口

另外还有pluggable协议,IStructuralEquatable接口。

==和!=

当使用==!=,C#在编译的过程就确定哪个类型来进行比较,不需要调用虚方法。

下面例子,编译器用int类型的==:

int x =5;
int y=5;
Console.WriteLine(x==y);//return True;

下面例子,编译器用object类的==:

object x=5;
object y=5;
Console.WriteLine(x==y);//return false;
Foo f1 = new Foo { x = 5 };
Foo f2 = null;
Console.WriteLine(f2==f2);//true
Console.WriteLine();

虚方法 Object.Equals

object x = 5;
object y = 5;
Console.WriteLine(x.Equals(y));

虚方法默认为这样的:

public virtual bool Equals(object obj)

{

  if(obj==null) return false;

  if(GetType() != obj.GetType()) return false;
if(obj==this)   Return true; }

由此可以看出,默认的实现其实比较的是两个对象的内存地址。值类型和string类型除外,因为所有值类型继承于System.ValueType()(System.ValueType()同样继承于Object,但是System.ValueType()本身却是引用类型),而System.ValueType()对Equals()和==操作符进行了重写,是逐字节比较的。而string类型是比较特殊的引用类型,所以strIng在很多地方都是特殊处理的,此处就不做深究了。

int a = 5;
int b = 5;
Console.WriteLine(a.Equals(b));//true
string a = "abc";
string b = "abc";
Console.WriteLine(a.Equals(b));//true
string a = "abc";
string b = "abc";
Console.WriteLine(a==b);//true
int a = 5;
int b = 5;
Console.WriteLine(a == b);//true

Equals方法在运行时根据object的实际类型来调用,在上例中,它调用了Int32的Equals方法,所以是true.

int x = 5;
double y = 5;
Console.WriteLine(x.Equals(y));//return false
			object x = 3, y = 3;
Console.WriteLine(x.Equals(y));
x = null;
Console.WriteLine(x.Equals(y));
y = null;
Console.WriteLine(x.Equals(y));

虚函数,如果调用者本身就是null,那么将抛出异常

调用Int32的Equals方法,然而x,y类型不一样,所以返回false,只有y也是Int类型,并且值与x一样的时候,才返回true

那么,为什么C#的设计者不通过让==也变成虚方法,从而让其与Equals等价,从而避免了复杂性?

其实它主要考虑了三个原因:

  • 如果第一个操作数是null,Equals方法失效,会抛出NullReferenceException;而==不会。
  • ==是静态的调用,所以它执行起来也相当快。
  • 有时候,==Equals可能对“相等”有不同的含义。

下面方法比较了任何类型是否相等:

public static bool AreEqual(object obj1,object obj2)
=> obj1==null?obj2==null:obj1.Equals(obj2);

静态方法object.Equals

object类提供了一个静态方法,其作用与上面的AreEqual作用一样,即可以比较任何类型是否相等,但需要装箱,包括是否是null,它就是Equals,接受2个参数:

public static bool Equals(object obj1,object obj2)

这就提供了一个null-safe的相等比较算法,当类型在编译时未确定,比如:

object x=3,y=3;
Console.WriteLine(object.Equals(x,y));//return true;
x=null;
Console.WriteLine(object.Equals(x,y));//return false;
y=null;
Console.WriteLine(object.Equals(x,y));//return true;

而如果上面Equals全部用==代替:

object x = 3, y = 3;
Console.WriteLine(x==y);
x = null;
Console.WriteLine(x == y);
y = null;
Console.WriteLine(x == y);

一个重要的应用就是当在写泛型类型的时候,就不能用==

所以必须这样写:

public class Test<T>{
T _value;
public void SetValue(T newValue)
{
if (!object.Equals(newValue,_value))
{
_value=newValue;
OnValueChanged();
}
}
protected virtual void OnValueChanged(){...}
}

==运算符在这里是不允许的,因为它要在编译的时候就确定是哪个类型。

另一种方法是用EqualityComparer<T>泛类,这避免了使用object.Equals而必需的装箱。

静态方法 object.ReferenceEquals

有时候,需要强行执行引用对比,这时候就要用到了object.ReferenceEquals.

object x = 3, y = 3;
Console.WriteLine(object.ReferenceEquals(x,y));
x = null;
Console.WriteLine(object.ReferenceEquals(x, y));
y = null;
Console.WriteLine(object.ReferenceEquals(x, y));

class Widget{...}
class Test
{
static void Main()
{
Widget w1=new Widget();
Widget w2=new Widget();
Console.WriteLine(object.ReferenceEquals(w1,w2));//return false
}
}

对于Widget,有可能虚函数Equals已经被覆盖了,以致w1.Equals(w2)返回true,也有可能重载了==运算符,以致w1==w2返回true

在这种情况下,object.ReferenceEquals保证了一般的引用相等的语义。

另一种强制引用相等的办法是先把value转换为object类型,然后用==运算符

IEquatable<T>

object.Equals静态方法必须要装箱,这对于高性能敏感的程序是不利的,因为装箱是相对昂贵的,与实际的比较相比。解决办法就是IEquatable<T>

public interface IEquatable<T>
{
bool Equals(T other);
}

它给出调用objectEquals虚方法相同效果的结果,但更快,你也可以用IEquatable<T>作为泛型的约束:

class Test<T> where T:IEquatable<T>
{
public bool IsEqual(T a,T b)
{
reurn a.Equals(b);//不用装箱
}
}

IsEqual被调用时,它调用了a.Equals,给出object的虚函数Equals的相同的效果,如果去掉约束,会发现仍然可以编译,但此时a.Equals调用的是object.Equals静态方法,也就是实际进行装箱操作了,所以就相对慢了些。

当Equals和==是不同的含义

有时候对于==Equals赋予不同的“相等”含义是非常有用的,比如:

double x=double.NaN;
Console.WriteLine(x==x);//false
Console.WriteLine(x.Equals(x));//true

double类型的==运算符强制任何一个NaN和任何一个数都不相等,对于另一个NaN也不相等,从数学的角度,这是非常自然的。而Equals,需要遵守一些规定,比如:

x.Equals(x) must always return true.

集合和字典就是依赖Equals的这种行为,否则,就找不到之前储存的Item了。

对于值类型来讲,==Equals有不同的涵义实际上是比较少的,更多的场景是引用类型,用==来进行引用相等的判断,用Equals来进行值相等的判断。StringBuilder就是这样做的:

var sb1 = new StringBuilder("foo");
var sb2 = new StringBuilder("foo");
Console.WriteLine(sb1 == sb2);//false,reference equal
Console.WriteLine(sb1.Equals(sb2));//true

Equality and Custom type

值类型用值相等,引用类型用引用相等,结构的Equals方法默认用structural value equality(即比较结构中每个字段的值)。

当写一个类型的时候,有两种情况可能要重写相等:

  • 改变相等的涵义

当默认的==Equals对于所写的类型的涵义不是那么自然的时候,这时候,就有必要重新了。

  • 加速结构的相等的比较

结构默认的structural equality比较算法是相对慢的,通过重写Equals可以提升它的速度,重载==IEquatable<T>允许非装箱相等比较,又可以再提速。

重载引用类型的相等语义,意义不大,因为默认的引用相等已经非常快了。

如果定义的类型重写了Equals方法,还应该重写GetHashCode方法,事实上,如果类型重写Equals的同时,没有重写GetHashCode,C#编译器就会生成一条警告。

之所以还要定义GetHashCode,是由于在System.Collections.Hashtable类型,System.Collections.Generic.Dictionary类型及其他一些集合的实现中,要求两个对象必须具有相同哈希码才视为相等,所以重写Equals就必须重写GetHashCode,确保相等性算法和对象哈希码算法一致,否则就是哈希码就是类型实例默认的地址。

IEqualityComparer,IEqualityComparer<T>接口则强行要求要同时实现Equals,GetHashCode方法,EqualityComparer抽象类则同时继承了这两个接口,只需要重新Equals(T x,T,y),GetHashCode(T obj)即可(https://www.cnblogs.com/johnyang/p/15417804.html),方便在需要判断是否两个对象相等的场景下,作为参数,或者调用者本身来使用。

重载相等语义的步骤:

  • 重载GetHashCode()和Equals()
  • (可选)重载!=,==,应实现这些操作符的用法,在内部调用类型安全的Equals
  • (可选)运用IEquatable<T>,这个泛型接口允许定义类型安全的Equals方法,通常重载的Equals接受一个Object参数,以便于在内部调用类型安全的Equals方法。

为什么在重写Equals()时,必须重写GetHashCode()的例子:

    class Foo:IEquatable<Foo>
{ public int x;
public override bool Equals(object obj)//重写Equals算法,注意这里参数是object
{
if (obj == null)
return base.Equals(obj);//base.Equal是
return Equals(obj as Foo);
}
public bool Equals(Foo other) //实现IEquatable接口,注意这里参数是Foo类型
{
if (other == null)
return base.Equals(other);
return this.x == other.x;
}
//public override int GetHashCode()
//{
// return this.x.GetHashCode();
//} } public void Main()
{
var f1 = new Foo { x = 5 };
var f2 = new Foo { x = 3 };
var f3 = new Foo { x = 5 };
var flist = new List<Foo>();
flist.Add(f1);
flist.Add(f3);
flist.Add(f2);
Console.WriteLine(f1.Equals(f3));
Console.WriteLine(flist.Contains(f3));
Console.WriteLine(flist.Distinct().Count());
var dic = new Dictionary<Foo, string>();
dic.Add(f1,"f1");
dic.Add(f2, "f2");
Console.WriteLine(dic[f3]);
}

在注释了GetHashCode重写代码后,我们运行上面的程序,就会发现,虽然Equals可以正常工作,但对于list的distinct的数量,显然错误,f1既然是和f2相等,那么数量应该是2,还有最后发现字典访问键f3也访问不了了,这当然是没有重写GetHashCode的后果,因为根据key取值的时候也是把key转换成HashCode而且验证Equals后再取值,也就是说,只要GetHashCode和Equlas中有一个方法没有重写,在验证时没有重写的那个方法会调用基类的默认实现,而这两个方法的默认实现都是根据内存地址判断的,也就是说,其实一个方法的返回值永远会是false。其结果就是,存储的时候你可能任性的存,在取值的时候就找不到北了!

如果一个对象在被作为字典的键后,它的哈希码改变了,那么在字典中,将永远找不到这个值了,为了解决这个问题,所以可以基于不变的字段来进行哈希计算。

现在,我们再去掉注释看看:

正常工作了!

而对GetHashCode重载的要求如下:

  • 对于Equals返回为true的两个值,GetHashCode也必须返回一样的值。

  • 不能抛出异常

  • 对于同一个对象,必须返回同一个值,除非对象改变

    对于class的GetHashCode默认返回的是internal object token,这对于每个实例来讲都是唯一的。


    假如因为某些原因要实现自己的哈希表集合,或者要在实现的代码中调用GetHashCode,记住千万不能对哈希码进行持久化,因为它很容易改变,一个类型的未来版本可能使用不同的算法计算哈希码。

    有公司不注意,在他们的网站上,用户选择用户名和密码进行注册,然后网站获取密码String,调用GetHashCode,将哈希码持久性存储到数据库,用户重新登陆网站,输入密码,网站再次调用GetHashCode,将哈希码与数据库中存储值对比,匹配就允许访问,不幸的是,升级到新CLR后,String的GetHashCode算法发生改变,结果就是所有用户无法登录


    重载Equals

    自己定义的重载Equals必须具备如下特征:

    (1)自反性,即x.Equals(x)是true

    (2)对称性,即x.Equals(y)y.Equals(x)返回值相同

    (3)可传递性,即x.Equals(y)返回true,y.Equals(z)也返回true,那么x.Equals(z)肯定也应该是true

    (4)可靠性,不抛出异常

    满足这几点,应用程序才会正常工作。

随机推荐

  1. swoole(5)信号监听、热重启

    一:信号监听 信号:由用户.系统或者进程发给目标进程的信息,以通知目标进程某个状态的改变或系统异常 信号查看:kill -l SIGHUP     终止进程     终端线路挂断 SIGINT     ...

  2. C# 私钥加密,公钥解密

    /// <summary> /// RSA加密的密匙结构 公钥和私匙 /// </summary> public struct RSAKey { public string P ...

  3. 【Unit2】电梯调度(多线程设计)-作业总结

    第一次作业 1.1 题目概述 5座楼,每座楼单电梯,类型相同,请求不跨楼层 1.2 个人处理思路 红色加粗为线程类,绿色块为临界区(共享对象) /...鄙人还在加班加点的赶制中.qwq./ 1.3 B ...

  4. linux tmux 使用教程

    前言 Tmux 是一个终端复用器(terminal multiplexer),非常有用,属于常用的开发工具. 本文介绍如何使用 Tmux. 一.Tmux 是什么? 1.1 会话与进程 命令行的典型使用 ...

  5. 【VMware VCF】解决 VCF 环境中组件用户密码过期问题。

    由于长时间没有启动 VCF 环境,现在在启动 SDDC Manager 组件后,UI 一直处于如下图所示的"初始化"状态.当时第一直觉就认为肯定是 VCF 环境组件的用户密码过期了 ...

  6. CAD通过XCLIP命令插入DWG参照裁剪图形,引用局部图像效果(CAD裁剪任意区域)

    CAD通过XCLIP命令插入DWG参照裁剪图形,实现引用局部图像效果,裁剪任意区域! 1.首先在你要引用局部图的文件内,插入参照! 2. 然后再空白区域指定插入点,输入比例因子,默认输入1,然后缩小视 ...

  7. PLSQL定时任务创建 Oracle数据库dbms_job

    创建一个job job创建 begin sys.dbms_job.submit(job => 1, --代表的是号码,第几个定时任务 what => 'sys_mailing_list_j ...

  8. 项目实战 TS

    项目实战 TS 通用技巧 新手先 any 再填坑,老手先定义数据结构写逻辑 遇到新场景,没把握快速,先用 any 再填坑,填坑的过程也是 TS 技能满满提升的过程. TS 发现潜在问题 1)复杂逻辑, ...

  9. Ubuntu给Appimage创建快捷方式

    下载 AppImageLauncher 2.安装 3.选择要运行的Appimage 双击运行即可.他会在home目录下创建一个applications文件夹,并且帮你自动创建快捷方式.

  10. Hack The Box-Chemistry靶机渗透

    通过信息收集访问5000端口,cif历史cve漏洞反弹shell,获取数据库,利用低权限用户登录,监听端口,开放8080端口,aihttp服务漏洞文件包含,获取root密码hash值,ssh指定登录 ...