c# 高级编程 6章133页 【比较对象的相等性】【结构比较】

460 阅读5分钟

对象相等的机制,有所不同,取决于是引用类型还是值类型


比较引用类型的相等性

System.Object定义了3个不同的方法来比较对象的相等性:

  1. ReferenceEquals()
  2. 可以被重写的虚拟实例方法:Equals()
  3. 静态方法:Equals()

外加可以以下途径:

  1. 实现接口IEquality<T>
  2. 比较运算符 ==

总共有上述5种方法。


1. ReferenceEquals()

  • 是静态方法 (静态,所以不能被重写)
  • 是看两个引用的值是否相等,是否指向相同的内存地址(或者说是看否为同一个实例)
  • object.ReferenceEquals(null, null),会返回true
        private static void ReferenceEqualsSample()
        {
            SomeClass x = new SomeClass(), y = new SomeClass(), z = x;

            bool b1 = object.ReferenceEquals(null, null); // returns true
            bool b2 = object.ReferenceEquals(null, x);    // returns false
            bool b3 = object.ReferenceEquals(x, y);       // returns false because x and y
                                                          // reference different objects
            bool b4 = object.ReferenceEquals(x, z);       // references the same object
        }

2. 可以被重写的虚拟实例方法:Equals()

  • System.Object的这个虚拟实例方法Equals() 比较的是引用(也就是看是否为同一个实例)。
  • 但是,可以在System.Object的子类(也就是任何类)里面重写这个Equals()方法,让它按某个值来比较对象实例(而不是直接比较对象实例的引用)。

这里有一个形象的使用场景示例:

  • 类的实例做字典的键 (键不能重复,所以字典的实现里会调用Equals()方法来比较键的相等性)。这时,就可以根据需要,重写Equals()方法,让作为键的实例实际上按照某个成员的值来比较相等性

  • 这里重写Equals() 方法,需要特别注意,重写的代码不应该抛出异常。否则调用这个Equals方法的.NET基类,例如字典类就可能会出问题。

注意: 当字典的键为类实例时,相等性比较,其实也可以用重写Object.GetHashCode()的方法来做,但是和重写Equals()方法相比,重写Object.GetHashCode()工作效率比较低。


3. 静态Equals()

  • Equals()的静态版本和虚实例版本作用相同
  • 静态版带两个参数

它的工作原理如下:

  • 两个参数,如果都是null, 则返回true;
  • 有一个参数是null, 另一个不是null, 则返回false;
  • 两个参数都不是null, 则调用Equals()的虚实例版本来进一步比较 (因此如果重写了Equals()的虚实例版本,也就等同于重写了Equals()的静态版本)

5. 比较运算符 ==

  • 比较运算符==, 比较的是引用
  • 但为了直观,一些类会重写这个运算符==,以值来做相等性比较。例如System.String就重写了这个运算符==,会比较字符串的内容,而不是比较引用

比较值类型的相等性

  1. System.ValueType类中重载了的实例方法Equals()
  2. 没有实际意义但存在的ReferenceEquals()

1. System.ValueType类中重载了的实例方法Equals()

  • 比较的是值
  • 如果是struct, sA.Equals(sB) 会挨个比较两者的所有字段,看它们的值是否相同
  • 如果是struct(且字段里有引用类型),会挨个比较两者的所有字段,遇到引用类型的字段,只比较引用,这时,如果只比较引用类型字段的引用不符合需求的话,可以考虑重写这个Equals()方法,让它按照合适的值进行相等性比较。

2. 没有实际意义但存在的ReferenceEquals()

  • 比较的是值类型的引用
  • 但,值类型要先装箱,才能转换成引用,才能用ReferenceEquals()
  • 装箱是单独装箱,这意味着会得到不同的引用
  • 所以ReferenceEquals()用于值类型时,总是返回false
bool b = ReferenceEquals(v, v); 

数组的结构比较

  • 本小节内容见于c#高级编程7章164页-数组 使用场景:
  • 数组是引用类型,persons1和persons2, 指向不同的内存地址,引用不同, 所以persons1.Equals(persons2)false!=也是比较引用。
  • 但是persons1和persons2,实际上数组长度相同,每个Person元素的成员的内容是相同的。

数组的结构比较:只看数组长度是否相同,每个数组元素的所有成员的内容是否相同。不看数组的引用,不看数组元素的引用。


    class Program
    {
        static void Main()
        {
            var janet = new Person(1, "Janet", "Jackson");
            Person[] persons1 = { new Person(2, "Michael", "Jackson"), janet };
            Person[] persons2 = { new Person(2, "Michael", "Jackson"), janet };
            if (persons1 != persons2)
            {
                Console.WriteLine("not the same reference");
            }

            if (!persons1.Equals(persons2))
            {
                Console.WriteLine("equals returns false - not the same reference");
            }

            if ((persons1 as IStructuralEquatable).Equals(persons2, EqualityComparer<Person>.Default))
            {
                Console.WriteLine("the same content");
            }
        }
    }
    public class Person : IEquatable<Person>
    {
        public int Id { get; }
        public string FirstName { get; }
        public string LastName { get; }

        public Person(int id, string firstName, string lastName)
        {
            Id = id;
            FirstName = firstName;
            LastName = lastName;
        }

        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();

        #region IEquatable<Person> Members

        public bool Equals(Person other)
        {
            if (other == null)
                return base.Equals(other);

            return Id == other.Id && FirstName == other.FirstName && LastName == other.LastName;
        }

        #endregion
    }

输出:

not the same reference
equals returns false - not the same reference
the same content

对数组进行结构化比较的时候,数组元素的类型需要实现IEquatable接口原因如下:

  • 数组默认实现了IStructuralEquatable这个接口,直接调用就完事了。

  • 对数组做结构比较时,需要强制转换成IStructuralEquatable。即: (persons1 as IStructuralEquatable)

  • IStructuralEquatable接口定义了一个方法Equals(object, IEqualityComparer<T>)

  • 这个Equals方法有两个参数,具体是这样使用.Equals(persons2, EqualityComparer<Person>.Default)

  • 其中,EqualityComparer<T>类完成了IEqualityComparer的一个默认实现。

    • 这个实现检查类型T是否实现了IEquatable接口,并调用IEquatable.Equals()方法。
    • 如果类型T没有实现IEquatable,就调用基类Object中实现的Equals方法进行比较。