创建自定义的类型时(无论是类还是struct),应为类型定义”同等性“的含义。在C#中为我们提供了四种不同的函数来判断两个对象是否”相等“:
1 public static bool ReferenceEquals(object left, object right); 2 public static bool Equals(object left, object right); 3 public virtual bool Equals(object right); 4 public static bool operator ==(MyClass left, MyClass right);
引用相等和值相等
C#允许我们创建两种类型:值类型和引用类型。如果两个引用类型的变量指向的是同一个对象,它们将被认为是“引用相等”。如果两个值类型的变量类型相同且包含同样的内容,它们被认为是“值相等”。这也正是同等性判断需要如此多方法的原因。
为什么不应该重新定义静态的ReferenceEquals()和Equals()方法
对于前两个静态函数,我们永远都不应该去重新定义,因为它们已经很好的完成了它们的工作,且判断与运行时具体类型无关:判断两个不同变量的对象标志(object identity)是否相等。无论比较的是值类型还是引用类型静态的ReferenceEquals方法的判断依据都是对象标志,所以比较两个值类型永远返回false,即使是值类型和它本身比较也是,这是因为装箱的原因。当我们不知道两个变量的运行时类型时,可以使用静态的Equals方法来判断两个变量是否相等,同等判断是以来类型,所以静态的Equals通过委托其中一个类型来做的判断的,静态的Ojbect.Equals()方法实现如下:
1 public static new bool Equals(object left, object right) 2 { 3 //检查对象引用 4 if (Object.ReferenceEquals(left, right)) 5 return true; 6 //是否为null 7 if (Object.ReferenceEquals(left, null)) 8 return false; 9 //调用实例的Equals()方法 10 return left.Equals(right); 11 }
我们可以看到静态的Equals()方法将判断的工作交给left参数的实例Equals()方法执行,所以它会使用left参数的类型中定义的规则来进行等同性判断。
什么情况下需要重写Equals()实例方法
当Equals()实例方法的默认行为与我们的类型要求不一致时,自然需要覆写。该方法默认使用对象标志判断,即比较两个对象是否引用相等。
值类型(使用Struct关键字创建的类型):System.ValueType(所有值类型的基类)覆写了Object.Equals()方法:两个值类型变量类型相同,内容一致,两个变量才认为相等。由于ValueType是所有值类型的基类,为了提供正确的行为,必须能够在不知道对象运行时类型的情况下比较其派生类中的所有成员变量,这意味着要使用反射来实现。而反射又是非常损耗性能的。而等同性判断又是一个非常基础的功能,所以我们有必要(追求性能时)为自己的值类型提供一个更快的Equals()覆写版本。
引用类型:只有我们希望更改其预定义的语义时,才应该覆写Equals()方法。在.NET类库中许多类都是使用值语义而不是引用语义来做等同判断的,例如:如果两个string对象包含相同的内容就被认为相等;若两个DataRowView对象引用同一个DataRow,那么将被认为相等。
如何覆写Equals()实例方法
覆写Equlas()实例方法是需要实现IEquatable<T>接口,该接口包含了一个方法Equals(Tother),实现了IEquatable<T>以为着你的类型支持类型安全的等同性比较。若你认为Equals()仅仅应该在比较的两边属于同一个类型时才返回true,那么IEquatable<T>将会让编译器帮你找到可能出现的种种类型相关的不相等情况。
下面是覆写System.Object.Equals()实例方法的标准实现模式(只是一个示例,具体情况还需根据我们的代码需求来确定):
1 public class foo : IEquatable<foo> 2 { 3 public override bool Equals(object right) 4 { 5 //是否为null 6 if (Object.ReferenceEquals(right, null)) 7 return false; 8 //是否引用相等 9 if (Object.ReferenceEquals(this, right)) 10 return true; 11 //可能是子类,所以需要精确的类型判断 12 if (this.GetType() != right.GetType()) 13 return false; 14 //调用实例的Equals()方法 15 return this.Equals(right as foo); 16 } 17 18 //IEquatable<foo> 成员 19 public bool Equals(foo other) 20 { 21 //略去 22 return true; 23 } 24 }
我们仔细观察这个实现:第一个坚持判断右边对象是否为null,对于this指针引用则不需要这一步,因为在C#中this指针永远不会为null。第二个判断两个对象是否为同一个引用,如果两个对象引用相同,则对象内容一定相等。第三个函数用来判断两个对象的类型是否相同。这里使用精确的比较是非常重要的。首先,没有假设this指针的类型为Foo,而是再次调用this.GetType()获取,因为实际的类型可能继承自Foo。
小节:
对于所有的值类型,都应该覆写其Equals()方法;对于引用类型,当System.Object提供的引用语义不能满足我们的需求时,才应该去覆写Equals()方法。覆写Equals()方法时也应该同时覆写GetHashCode()方法(条目7)。对于operator==()则比较简单,只要创建的是值类型,都必须重新覆写一个operator==(),理由和覆写ValueType.Equals()实例函数完全一样。引用类型应该尽量避免覆写operator==(),.NET 希望所有引用类型都应用的operator==()都遵循引用语义,因为系统提供的默认版本时通过比较两个值类型实例的内容,并且是用反射来实现的。(其实如果对于性能不是那么敏感的话可以忽略)。