CLR中将“相等性”分为两类:“值相等性”和“引用相等性”。
值相等性:两个变量所包含的数值相等。
引用相等性:两个变量引用的是内存中的同一个对象。
无论是操作符“==”,还是方法“Equals()”,都倾向于表达这样一个原则:
对于值类型,如果类型的值相等,就应该返回true;
对于引用类型,如果类型指向同一个对象,则返回true。
例:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 int i = 1; 6 int j = 1; 7 Console.WriteLine(i == j); //true 8 Console.WriteLine(i.Equals(j)); //true 9 j = i; 10 Console.WriteLine(i == j); //true 11 Console.WriteLine(i.Equals(j)); //true 12 13 object a = new Person("NO1"); 14 object b = new Person("NO1"); 15 Console.WriteLine(a == b); //false 16 Console.WriteLine(a.Equals(b)); //false 17 b = a; 18 Console.WriteLine(a == b); //true 19 Console.WriteLine(a.Equals(b)); //true 20 Console.Read(); 21 } 22 } 23 24 class Person 25 { 26 public string ID { get; private set; } 27 public Person(string value) 28 { 29 ID = value; 30 } 31 }
操作符“==”,还是方法“Equals()”,都是可以被重载的。比如string类型,它是一个特殊的引用类型,微软觉得它的现实意义更接近于值类型,所以,再FCL中,string的比较被重载为针对”类型的值“的比较,而不是”引用本身“的比较。
例:
1 string s1 = "aa"; 2 string s2 = "aa"; 3 Console.WriteLine(s1 == s2); //true 4 Console.WriteLine(s1.Equals(s2)); //true 5 s2 = s1; 6 Console.WriteLine(s1 == s2); //true 7 Console.WriteLine(s1.Equals(s2)); //true
从设计上来说,很多自定义类型(尤其是自定义的引用类型)会存在和string类型比较接近的情况。如第一个例子中的Person类,再现实生活中,如果两个人的ID是相等的,我们就会认为两者是同一个人,这个时候就要重载Equals方法了:
1 class Person 2 { 3 public string ID { get; private set; } 4 public Person(string value) 5 { 6 ID = value; 7 } 8 //重写Object.Equals(object o) 9 public override bool Equals(object obj) 10 { 11 return ID == (obj as Person).ID; 12 } 13 }
这时再去比较两个具有相同ID的Person对象的值就会返回true:
1 object a = new Person("NO1"); 2 object b = new Person("NO1"); 3 Console.WriteLine(a == b); //false 4 Console.WriteLine(a.Equals(b)); //true
此时,对于该类,可以用==判断两个实例是否为指向同一个对象,用Equals方法判断两个实例的值是否相等。
注意:FCL中提供了Object.ReferenceEquals方法来明确肯定是比较”引用相等性“。
但是,重写了Equals方法,编译器会提示一个信息:
”重写 Object.Equals(object o) 但不重写 Object.GetHashCode()”
这样在使用FCL中的Dictionary类时,可能隐含一些潜在的Bug。
在上面的代码基础下,增加PersonMoreInfo类:
1 class PersonMoreInfo 2 { 3 public string Info { get; set; } 4 public PersonMoreInfo(string value) 5 { 6 Info = value; 7 } 8 }
创建一个Dictionary,通过key寻找value:
1 class Program 2 { 3 static Dictionary<Person, PersonMoreInfo> PersonValues = new Dictionary<Person, PersonMoreInfo>(); 4 static void Main(string[] args) 5 { 6 AddPerson(); 7 Person mike = new Person("No2"); 8 Console.WriteLine(mike.GetHashCode()); 9 Console.WriteLine(PersonValues.ContainsKey(mike));//用key的HashCode寻找键 10 11 Console.Read(); 12 } 13 static void AddPerson() 14 { 15 Person mike = new Person("No2"); 16 PersonMoreInfo mikeValue = new PersonMoreInfo("Mike's info"); 17 PersonValues.Add(mike, mikeValue); 18 Console.WriteLine(mike.GetHashCode()); 19 Console.WriteLine(PersonValues.ContainsKey(mike)); 20 } 21 }
运行结果:
1 22008501 2 True 3 9008175 4 False
重写了Equals方法,所以在AddPerson方法里的mike和Main方法里的mike属于“值相等”,此时将“值”作为key放入Dictinoary中,再在某处根据mike将对应的键mikeValue取出来。但是上面代码运行结果却是Main方法中的mike没有对应的mikeValue。原因是键值对的集合会根据Key值的HashCode寻找Value值。CLR首先调用Person类的GetHashCode方法,因为Person类没有实现GetHashCode方法,所以就调用Object.GetHashCode()。Object为所有的CLR类型都提供了GetHashCode的默认实现,每new一个对象,CLR就会为该对象生成一个固定的整型值,该值在对象的生存周期内不会改变,GetHashCode方法就是实现对该整型值求HashCode。
所有上面的代码中,两个mike对象的HashCode是不一样的,导致Main方法里的mike没有对应的mikeValue。此时就要重写GetHashCode方法:
1 class Person 2 { 3 public string ID { get; private set; } 4 public Person(string value) 5 { 6 ID = value; 7 } 8 //重写Object.Equals(object o) 9 public override bool Equals(object obj) 10 { 11 return ID == (obj as Person).ID; 12 } 13 //重写Object.GetHashCode() 14 public override int GetHashCode() 15 { 16 return ID.GetHashCode(); 17 } 18 }
再次运行代码得到以下结果:
1 -54312782 2 True 3 -54312782 4 True
因为HashCode一般作为对象的特征,在对象生存周期内赋值就不应改变,所以因基于那些只读属性或特性生成HashCode。
GetHashCode方法永远只返回一个整型int,而int的容量显然无法满足字符串的容量,以下代码会生成两个同样的HashCode:
1 string s1 = "NB0903100006"; 2 string s2 = "NB0904140001"; 3 Console.WriteLine(s1.GetHashCode()); 4 Console.WriteLine(s2.GetHashCode());
所以为了减少两个不同类型之间根据字符串产生相同的HashCode几率,要改进GetHashCode方法:
1 //重写Object.GetHashCode() 2 public override int GetHashCode() 3 { 4 return (System.Reflection.MethodBase.GetCurrentMethod(). 5 DeclaringType.FullName + "#" + this.ID).GetHashCode(); 6 }
注意:重写Equals方法的同时,也应该实现一个类型安全的接口IEquatable<T>
1 class Person : IEquatable<Person> 2 { 3 public string ID { get; private set; } 4 public Person(string value) 5 { 6 ID = value; 7 } 8 //重写Object.Equals(object o) 9 public override bool Equals(object obj) 10 { 11 return ID == (obj as Person).ID; 12 } 13 //重写Object.GetHashCode() 14 public override int GetHashCode() 15 { 16 return (System.Reflection.MethodBase.GetCurrentMethod(). 17 DeclaringType.FullName + "#" + this.ID).GetHashCode(); 18 } 19 //重写IEquatable<Person>.Equals(T other) 20 public bool Equals(Person other) 21 { 22 return ID == other.ID; 23 } 24 }
参考:《编写高质量代码改善C#程序的157个建议》陆敏技