“世界上不会有两片完全相同的树叶”,这句话适用于现实世界。而在软件世界中,这句话变成了"世界上必须有两片完全相同的树叶",否则,很多事情无以为继。
当比较2个对象是否相等时,通常情况下:==操作符用来比较值类型,比较的是值;实例方法Equals和静态方法Object.ReferenceEquals比较引用类型,比较的是对象的地址。
在实际项目中,当比较2个引用类型对象时,我们的需求变为:通过依次比较2个对象的所有属性来判断是否相等。这时候,IEquatable接口就有了展示自己的机会。本篇主要包括:
使用==操作符比较值类型是否相等实例方法Equals比较引用类型地址是否相等实现IEquatable接口重写实例方法Equals()
使用==操作符比较值类型是否相等
classPRogram
{
staticvoidMain(string[] args)
{
ComplexNumber a =newComplexNumber(){Real = 4.5D, Imaginary = 8.4D};
ComplexNumber b =newComplexNumber() { Real = 4.5D, Imaginary = 8.4D };
Console.WriteLine("{0} 是否等于{1}:{2}",a, b, CompareTwoComplexNumbers(a, b));
Console.ReadKey();
}
staticboolCompareTwoComplexNumbers(ComplexNumber a, ComplexNumber b)
{
return((a.Real == b.Real) && (a.Imaginary == b.Imaginary));
}
}
publicclassComplexNumber
{
publicdoubleReal {get;set; }
publicdoubleImaginary {get;set; }
publicoverridestringToString()
{
returnString.Format("{0}{1}{2}i",
Real,
Imaginary >= 0 ? "+" : "-",
Math.Abs(Imaginary));
}
}
以上,比较诸如int,double,DateTime,struct等值类型的时候,==操作符当然是不二之选。
实例方法Equals比较引用类型地址是否相等
classProgram
{
staticvoidMain(string[] args)
{
Guy darren1 =newGuy("Darren", 37, 100);
Guy darren2 = darren1;
Console.WriteLine(Object.ReferenceEquals(darren1,darren2));
Console.WriteLine(darren1.Equals(darren2));
Console.WriteLine(Object.ReferenceEquals(null,null));
darren2 =newGuy("Darren", 37, 100);
Console.WriteLine(Object.ReferenceEquals(darren1, darren2));
Console.WriteLine(darren1.Equals(darren2));
Console.ReadKey();
}
}
publicclassGuy
{
privatereadonlystringname;
publicstringName{get{returnname; }}
privatereadonlyintage;
publicintAge{get{returnage; }}
publicintCash {get;privateset; }
publicGuy(stringname,intage,intcash)
{
this.name = name;
this.age = age;
Cash = cash;
}
publicoverridestringToString()
{
returnString.Format("{0} 今年 {1} 岁了,身价{2}", Name, Age, Cash);
}
}
以上,实例方法Equals()和静态方法Object.ReferenceEquals()适用于比较引用类型,而且比较的是对象的地址。
可是,如果我们想使用Equals()方法比较引用类型对象的各个属性,怎么办呢?
实现IEquatable接口重写实例方法Equals()
写一个派生于Guy的类EquatableGuy,并且实现IEquatable<Guy>接口,重写IEquatable<Guy>接口的Equals(Guy other)方法。
classProgram
{
staticvoidMain(string[] args)
{
Guy darren1 =newEquatableGuy("Darren", 37, 100);
Guy darren2 =newEquatableGuy("Darren", 37, 100);
Console.WriteLine(Object.ReferenceEquals(darren1, darren2));//False
Console.WriteLine(darren1.Equals(darren2));//True
Console.ReadKey();
}
}
publicclassGuy
{
privatereadonlystringname;
publicstringName{get{returnname; }}
privatereadonlyintage;
publicintAge{get{returnage; }}
publicintCash {get;privateset; }
publicGuy(stringname,intage,intcash)
{
this.name = name;
this.age = age;
Cash = cash;
}
publicoverridestringToString()
{
returnString.Format("{0} 今年 {1} 岁了,身价{2}", Name, Age, Cash);
}
}
publicclassEquatableGuy : Guy, IEquatable<Guy>
{
publicEquatableGuy(stringname,intage,intcash) :base(name, age, cash){}
publicboolEquals(Guy other)
{
if(ReferenceEquals(null, other))returnfalse;
if(ReferenceEquals(this, other))returntrue;
returnEquals(other.Name, Name) && other.Age == Age && other.Cash == Cash;
}
publicoverrideboolEquals(objectobj)
{
if(!(objisGuy))returnfalse;
returnEquals((Guy) obj);
}
publicoverrideintGetHashCode()
{
constintprime = 397;
intresult = Age;
result = (result * prime) ^ (Name !=null? Name.GetHashCode() : 0);
result = (result * prime) ^ Cash;
returnresult;
}
}
以上,值得注意的是:
当实现IEquatable<Guy>接口时,一定要重写Object基类的Equals方法,然后在Object基类的Equals方法内部调用我们自定义的IEquatable<Guy>接口方法。另外还必须重写Object基类的GetHashCode方法。这时MSDN规定的,在这里。
而且,使用IEquatable<Guy>泛型接口还有一个好处是避免装箱和拆箱,因为在JIT编译时才替代占位符。而如果我们通过重写Object基类方法Equals实现自定义比较的话,难免会出现装箱和拆箱,影响比较性能。
如果我们项使用==操作符比较引用对象是否相等呢?我们可以通过重写操作符来实现。
我们再写一个EquatableGuy的子类EquatableGuyWithOverload,并且重写==操作符。
classProgram
{
staticvoidMain(string[] args)
{
var darren1 =newEquatableGuyWithOverload("Darren", 37, 100);
var darren2 =newEquatableGuyWithOverload("Darren", 37, 100);
Console.WriteLine(Object.ReferenceEquals(darren1, darren2));//False
Console.WriteLine(darren1 == darren2);//True
Console.ReadKey();
}
}
publicclassEquatableGuyWithOverload : EquatableGuy
{
publicEquatableGuyWithOverload(stringname,intage,intcash) :base(name, age, cash){}
publicstaticboolOperator==(EquatableGuyWithOverload left, EquatableGuyWithOverload right)
{
if(Object.ReferenceEquals(left,null))returnfalse;
elsereturnleft.Equals(right);
}
publicstaticbooloperator!=(EquatableGuyWithOverload left, EquatableGuyWithOverload right)
{
return!(left == right);
}
publicoverrideboolEquals(objectobj)
{
returnbase.Equals(obj);
}
publicoverrideintGetHashCode()
{
returnbase.GetHashCode();
}
}
以上,子类EquatableGuyWithOverload重写了==操作符,由于其父类EquatableGuy已经重写了基类Object的Equals方法,所以在这里可以直接调用。
总结:通常情况下,使用==操作符比较值类型对象;使用实例方法Equals或静态方法Object.ReferenceEquals比较引用类型对象地址;如果想自定义比较逻辑,可以考虑实现IEquatable<>泛型接口,避免装箱、拆箱。
参考资料:
防止装箱落实到底,只做一半也是失败
Understanding C#: Equality, IEquatable, and Equals()