对象相等的机制,有所不同,取决于是引用类型还是值类型
比较引用类型的相等性
System.Object定义了3个不同的方法来比较对象的相等性:
- ReferenceEquals()
- 可以被重写的虚拟实例方法:Equals()
- 静态方法:Equals()
外加可以以下途径:
- 实现接口
IEquality<T> - 比较运算符 ==
总共有上述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就重写了这个运算符==,会比较字符串的内容,而不是比较引用
比较值类型的相等性
- System.ValueType类中重载了的实例方法Equals()
- 没有实际意义但存在的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方法进行比较。
- 这个实现检查类型