C#如何重写Equals

C#如何重写Equals

在自定义struct的时候,经常需要重写Equals方法,原因是一方面ValueType内部使用反射进行各字段相等判断,效率比较低;另一方面在调用基类Equals的时候,会造成值类型的装箱,(详细可以参考《值类型的拆箱与装箱》),除非能够确定类型不会进行相等性判断,否则建议进行Equals重写来提高性能。

同一性与相等性

同一性是指两个变量引用的是内存中的同一个对象,C#使用Object.ReferenceEquals(obj,obj)进行判断;
相等性是指两个变量所包含的数值相等,一般来说是对各个字段进行值比较。从概念上来说,同一性比相等性更加严格,满足同一性必然满足相等性,但符合相等性不一定满足同一性。

什么时候需要重写Equals

  1. 自定义结构体,处于性能考虑,避免不必要的装箱和反射。
  2. class类型需要相等性而非同一性的比较,例如System.String类型,虽然是引用类型,但是两个string变量进行比较时,是进行相等性比较,而非引用。

重写Equals遵守的规则

  1. Equals必须自反:x.Equals(x)肯定返回true。
  2. Equals必须对称:x.Equals(y)和y.Equals(x)必须返回相同的值。
  3. Equals必须可传递:x.Equals(y)返回true,y.Equals(z)返回true,则x.Equals(z)肯定返回true。
  4. Equals必须一致。比较的两个值不变,Equals返回也不能变。

重写Object.Equals流程

  1. 重写GetHashCode方法。
  2. 实现System.IEquatable接口的Equals方法,该接口允许定义类型安全的Equals方法。
  3. 重写Object.Equals,并在内部调用类型安全的Equals方法。
  4. 重载==和!=操作符,并在内部调用类型安全的Equals方法。

引用类型重写

这里实现Person类型的相等性重写,即如果Person.Id相等,则认为Person是相同的,如果需要进行同一性判断,需要使用Object.ReferenceEquals(obj,obj)进行判断。

internal class Program
{
    public static void Main(string[] args)
    {
        Person p1= new Person("1");
        Person p2= new Person("1");

        //True
        Console.WriteLine(p1==p2);

        //True
        Console.WriteLine(p1.Equals(p2));

        //False
        Console.WriteLine(ReferenceEquals(p1,p2));

        Console.Read();
    }
}

public class Person : IEquatable<Person>
 {
     private string _Id;

     public Person(string id)
     {
         _Id = id;
     }

     public bool Equals(Person other)
     {
         //this非空,obj如果为空,则返回false
         if (ReferenceEquals(null, other)) return false;

         //如果为同一对象,必然相等
         if (ReferenceEquals(this, other)) return true;

         //对比各个字段值
         if(!string.Equals(_Id, other._Id, StringComparison.InvariantCulture))
             return false;

         //如果基类不是从Object继承,需要调用base.Equals(other)
         //如果从Object继承,直接返回true
         return true;
     }

     public override bool Equals(object obj)
     {
         //this非空,obj如果为空,则返回false
         if (ReferenceEquals(null, obj)) return false;

         //如果为同一对象,必然相等
         if (ReferenceEquals(this, obj)) return true;

         //如果类型不同,则必然不相等
         if (obj.GetType() != this.GetType()) return false;

         //调用强类型对比
         return Equals((Person) obj);
     }

     //实现Equals重写同时,必须重写GetHashCode
     public override int GetHashCode()
     {
         return (_Id != null ? StringComparer.InvariantCulture.GetHashCode(_Id) : 0);
     }

     //重写==操作符
     public static bool operator ==(Person left, Person right)
     {
         return Equals(left, right);
     }

     //重写!=操作符
     public static bool operator !=(Person left, Person right)
     {
         return !Equals(left, right);
     }
 }

值类型重写

值类型必须实现相等性重写,因为两个值类型变量,不可能指向内存同一个对象,即Object.Reference(ValueType,ValueType)返回false。

internal class Program
{
    public static void Main(string[] args)
    {
        CustomStruct p1 = new CustomStruct("1");
        CustomStruct p2 = new CustomStruct("1");

        //True
        Console.WriteLine(p1 == p2);

        //True
        Console.WriteLine(p1.Equals(p2));

        //Always False
        Console.WriteLine(ReferenceEquals(p1,p2));

        Console.Read();
    }
}


public struct CustomStruct : IEquatable<CustomStruct>
{
    private readonly string _Id;

    public CustomStruct(string id)
    {
        _Id = id;
    }

    public bool Equals(CustomStruct other)
    {
        return string.Equals(_Id, other._Id, StringComparison.InvariantCulture);
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        return obj is CustomStruct && Equals((CustomStruct) obj);
    }

    public override int GetHashCode()
    {
        return (_Id != null ? StringComparer.InvariantCulture.GetHashCode(_Id) : 0);
    }

    public static bool operator ==(CustomStruct left, CustomStruct right)
    {
        return left.Equals(right);
    }

    public static bool operator !=(CustomStruct left, CustomStruct right)
    {
        return !left.Equals(right);
    }
}

关于GetHashCode

在重写Equals方法,而不重写GetHashCode方法,编译器会提示警告信息:“重写Object.Equals(object o)但不重写Object.GetHashCode()”,在使用Dictionary、HashMap类是,可能发生一些意想不到的Bug,还是针对上面的Person类(为了演示,先注释掉GetHashCode),编写一个示例来演示这种Bug。

using System;
using System.Collections.Generic;

internal class Program
{
    public static void Main(string[] args)
    {
        Dictionary<Person,string> dic = new Dictionary<Person, string>();

        Person p1 = new Person("Mike");
        Person p2 = new Person("Mike");

        dic.Add(p1,"p1 info");

        //True
        Console.WriteLine(p1==p2);

        //True - Mike在dic当中
        Console.WriteLine(dic.ContainsKey(p1));

        //False - Mike不在dic当中
        //这其实是不正确的,因为p1的值等于p2的值,将该值存入到dic当中,应该能够在利用该值再次读取出来
        Console.WriteLine(dic.ContainsKey(p2));

        Console.Read();
    }
}


public class Person : IEquatable<Person>
{
    private string _Id;

    public Person(string id)
    {
        _Id = id;
    }

    public bool Equals(Person other)
    {
        //this非空,obj如果为空,则返回false
        if (ReferenceEquals(null, other)) return false;

        //如果为同一对象,必然相等
        if (ReferenceEquals(this, other)) return true;

        //对比各个字段值
        if(!string.Equals(_Id, other._Id, StringComparison.InvariantCulture))
            return false;

        //如果基类不是从Object继承,需要调用base.Equals(other)
        //如果从Object继承,直接返回true
        return true;
    }

    public override bool Equals(object obj)
    {
        //this非空,obj如果为空,则返回false
        if (ReferenceEquals(null, obj)) return false;

        //如果为同一对象,必然相等
        if (ReferenceEquals(this, obj)) return true;

        //如果类型不同,则必然不相等
        if (obj.GetType() != this.GetType()) return false;

        //调用强类型对比
        return Equals((Person) obj);
    }

    //实现Equals重写同时,必须重写GetHashCode
    //这里先注释掉
    /*
    public override int GetHashCode()
    {
        return (_Id != null ? StringComparer.InvariantCulture.GetHashCode(_Id) : 0);
    }
    */

    //重写==操作符
    public static bool operator ==(Person left, Person right)
    {
        return Equals(left, right);
    }

    //重写!=操作符
    public static bool operator !=(Person left, Person right)
    {
        return !Equals(left, right);
    }
}

造成这种问题的原因在于,在基于Key-Value的集合当中,会根据Key值来查找Value值,CLR内部会优化这种查找,实际上,最终是根据Key值得HashCode来查找Value值,示例当中p1虽然和p2值相等,但是Person类有没重写GetHashCode,所以最终调用System.Object的GetHashCode实现,默认System.Object的GetHashCode为不同对象返回不同的值,所以在使用p2进行查找时,不能找到对应的值。

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页