7.9 结构比较
数组和元组都实现接口IStructuralEquatable和IStructuralComparable。这两个接口不仅可以比较引用,还可以比较内容。这些接口都是显式实现的,所以在使用时需要把数组和元组强制转换为这个接口。IStructuralEquatable接口用于比较两个元组或数组是否有相同的内容,IStructuralComparable接口用于给元组或数组排序。
对于说明IStructuralEquatable接口的示例,使用实现IEquatable接口的Person类。IEquatable接口定义了一个强类型化的Equals()方法,以比较FirstName和LastName属性的值(代码文件StructuralComparison/Person.cs):
public class Person: IEquatable<Person> { public int Id { get; private set; } public string FirstName { get; set; } public string LastName { get; set; } public override string ToString() => $"{Id}, {FirstName} {LastName}"; public override bool Equals(object obj) { if (obj == null) { return base.Equals(obj); } return Equals(obj as Person); } public override int GetHashCode() => Id.GetHashCode(); public bool Equals(Person other) { if (other == null) return base.Equals(other); return Id == other.Id && FirstName == other.FirstName && LastName == other.LastName; } }
现在创建了两个包含Person项的数组。这两个数组通过变量名janet包含相同的Person对象,和两个内容相同的不同Person对象。比较运算符“! =”返回true,因为这其实是两个变量persons1和persons2引用的两个不同数组。因为Array类没有重写带一个参数的Equals()方法,所以用“==”运算符比较引用也会得到相同的结果,即这两个变量不相同(代码文件StructuralComparison/Program.cs):
var janet = new Person { FirstName = "Janet", LastName = "Jackson" }; Person[] persons1 = { new Person { FirstName = "Michael", LastName = "Jackson" }, janet }; Person[] persons2 = { new Person { FirstName = "Michael", LastName = "Jackson" }, janet }; if (persons1 ! = persons2) { WriteLine("not the same reference"); }
对于IStructuralEquatable接口定义的Equals()方法,它的第一个参数是object类型,第二个参数是IEqualityComparer类型。调用这个方法时,通过传递一个实现了IEqualityComparer<T>的对象,就可以定义如何进行比较。通过EqualityComparer<T>类完成IEqualityComparer的一个默认实现。这个实现检查该类型是否实现了IEquatable接口,并调用IEquatable.Equals()方法。如果该类型没有实现IEquatable,就调用Object基类中的Equals()方法进行比较。
Person实现IEquatable<Person>,在此过程中比较对象的内容,而数组的确包含相同的内容:
if ((persons1 as IStructuralEquatable).Equals(persons2, EqualityComparer<Person>.Default)) { WriteLine("the same content"); }
下面看看如何对元组执行相同的操作。这里创建了两个内容相同的元组实例。当然,因为引用t1和t2引用了两个不同的对象,所以比较运算符“! =”返回true:
var t1 = Tuple.Create(1, "Stephanie"); var t2 = Tuple.Create(1, "Stephanie"); if (t1 ! = t2) { WriteLine("not the same reference to the tuple"); }
Tuple<>类提供了两个Equals()方法:一个重写了Object基类中的Equals()方法,并把object作为参数,第二个由IStructuralEqualityComparer接口定义,并把object和IEqualityComparer作为参数。可以给第一个方法传送另一个元组,如下所示。这个方法使用EqualityComparer<object>.Default获取一个ObjectEqualityComparer<object>,以进行比较。这样,就会调用Object.Equals()方法比较元组的每一项。如果每一项都返回true, Equals()方法的最终结果就是true,这里因为int和string值都相同,所以返回true:
if (t1.Equals(t2)) { WriteLine("the same content"); }
还可以使用类TupleComparer创建一个自定义的IEqualityComparer,如下所示。这个类实现了IEqualityComparer接口的两个方法Equals()和GetHashCode():
class TupleComparer: IEqualityComparer { public new bool Equals(object x, object y) => x.Equals(y); public int GetHashCode(object obj) => obj.GetHashCode(); }
注意:实现IEqualityComparer接口的Equals()方法需要new修饰符或者隐式实现的接口,因为基类Object也定义了带两个参数的静态Equals()方法。
使用TupleComparer,给Tuple<T1, T2>类的Equals()方法传递一个新实例。Tuple类的Equals()方法为要比较的每一项调用TupleComparer的Equals()方法。所以,对于Tuple<T1, T2>类,要调用两次TupleComparer,以检查所有项是否相等:
if (t1.Equals(t2, new TupleComparer())) { WriteLine("equals using TupleComparer"); }