C#高级编程(第10版) C# 6 & .NET Core 1.0 (.NET开发经典名著)
上QQ阅读APP看书,第一时间看更新

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");
        }