C#中,有四种方式可以应用于“相等判断”,如下。
代码
public static bool ReferenceEquals( object left, object right ); public static bool Equals( object left, object right ); public virtual bool Equals( object right); public static bool operator==( MyClass left, MyClass right );
对于一个判断是否相等的操作,为什么会有四种形式呢,究其原因,还要看C#的数据类型,C#的数据类型分为值类型和引用类型,其中值类型直接存储在堆栈上,而引用类型的值存储在堆上,在栈中保留一个指向堆中地址的引用。这样在判断相等的时候,就会产生两种判断方式:1)判断变量在堆栈上存储的值是否相等,这对于值类型来说,就足够了,但是对于引用类型来说,只是判断了堆栈中保存的引用是否相等,还是不全面的;2)判断变量在堆中存储的值是否相等,这主要用于引用类型。
关于如何判断“相等”,如果两个引用类型的变量指向同一个对象,那么它们将被认为是“引用相等”;如果两个值类型的变量类型相同,而且包含同样的内容,它们被认为是“值相等”。
对于上述提供的四种用于判断“相等”的方法,其中前两种都是Object类带有的静态方法,其中Object.ReferenceEquals()方法用于判断两个变量的对象标识是否相同,不论是值类型还是引用类型,都是判断是否“引用相等”,而不是“值相等”,这意味着如果我们对于两个值类型使用该方法,那么总是会返回false。
上述描述的第二种方式,是Object类型的静态方法Equals(),当我们不知道两个变量的运行时类型时,可以使用这个方法来判断两个变量是否相等,由于刚方法并不知道变量的类型,因此,“相等判断”的操作是依赖于类型的,即它会调用其中一个对象实例的Equals方法。静态Object.Equals()方法的实现如下。
代码
1 public static bool Equals( object left, object right ) 2 { 3 // Check object identity 4 if (left == right ) 5 return true; 6 // both null references handled above 7 if ((left == null) || (right == null)) 8 return false; 9 return left.Equals (right); 10 }
上述第三种方式,是对象实例的Equals()方法,其中System.Object类作为所有类的基类,本身也定义了Equals()方法,Object实例中的Equals()方法,是判断“引用相等”,其行为和ReferenceEquals()方式完全一样。而System.ValueType作为所有值类型的基类,它重写了Equals()方法,在重写方法中,是按照“值相等”的方式来进行判断的,但是,ValueType重写的Equals()方法效率不高,原因是它使用了反射来得到对象的所有属性,进而判断属性的值是否相同,这样会导致性能很差,因此,当我们定义一个值类型时,应该总是重写Equals()方法。
一般,我们重写Equals()方法的形式如下。
代码
1 public class Foo 2 { 3 public override bool Equals( object right ) 4 { 5 // check null: 6 // the this pointer is never null in C# methods. 7 if (right == null) 8 return false; 9 10 if (object.ReferenceEquals( this, right )) 11 return true; 12 13 // Discussed below. 14 if (this.GetType() != right.GetType()) 15 return false; 16 17 // Compare this type's contents here: 18 return CompareFooMembers( 19 this, right as Foo ); 20 } 21 } 22 23
我们在重写Equals()方法时,应该遵循以下三个原则:
- 自反性,即a=a
- 交换性,即如果a=b,那么b=a
- 传递性,即如果a=b,b=c,那么a=c
当我们在一个有类继承层次关系的结构中,为父类和子类都重写Equals()方法, 那么很可能造成非常诡异的Bug,我们来看下面的代码。
代码
1 public class B 2 { 3 public override bool Equals( object right ) 4 { 5 // check null: 6 if (right == null) 7 return false; 8 9 // Check reference equality: 10 if (object.ReferenceEquals( this, right )) 11 return true; 12 13 // Problems here, discussed below. 14 B rightAsB = right as B; 15 if (rightAsB == null) 16 return false; 17 18 return CompareBMembers( this, rightAsB ); 19 } 20 } 21 22 public class D : B 23 { 24 // etc. 25 public override bool Equals( object right ) 26 { 27 // check null: 28 if (right == null) 29 return false; 30 31 if (object.ReferenceEquals( this, right )) 32 return true; 33 34 // Problems here. 35 D rightAsD = right as D; 36 if (rightAsD == null) 37 return false; 38 39 if (base.Equals( rightAsD ) == false) 40 return false; 41 42 return CompareDMembers( this, rightAsD ); 43 } 44 45 } 46 47 //Test: 48 B baseObject = new B(); 49 D derivedObject = new D(); 50 51 // Comparison 1. 52 if (baseObject.Equals(derivedObject)) 53 Console.WriteLine( "Equals" ); 54 else 55 Console.WriteLine( "Not Equal" ); 56 57 // Comparison 2. 58 if (derivedObject.Equals(baseObject)) 59 Console.WriteLine( "Equals" ); 60 else 61 Console.WriteLine( "Not Equal" ); 62 63
如果你认为上述代码应该返回两个“Equals”或者两个“Not Equals”,那么无可厚非,但实际上,对于上述的两次比较,第二次总是会返回false,而第一次有时会返回true,有时会返回false。原因在于类型转换,子类型是可以默认转换为父类型的,但是父类型不可以转换为子类型。
因此,当我们重写Equals()方法时,有一个很好的建议:如果基类的Equals()方法不是由System.Object或者System.ValueType提供的话,那么我们也应该在重写子类的Equals()方法时,调用基类的Equals()方法。
关于上述判断“相等”的方式,总结如下:
- 永远不要重写Object类的ReferenceEquals()和Equals()两个静态方法。
- 对于值类型来说,为了提高效率,我们应该总是重写实例的Equals()方法和==()操作符,对于引用类型,如果我们认为相等的含义并非是对象标识相同的话,那么也需要重写Equals()方法,但是不应该重写==()操作符,.NET建议所有引用类型上应用==操作时,都遵循“引用相等”。
|
请发表评论