理解对象相等的机制对逻辑表达式的编程非常重要, 另外对实现运算符重载和类型强制转换也非常重要。
对象相等的机制有所不同,这取决于比较的是引用类型(类的实例),还是值类型(基本数据类型, 结构 或 枚举的实例)。
本章会单独介绍引用类型的相等性和值类型的相等性。
比较引用类型的相等性
System.Object 定义了3个不同的方法来比较对象的相等性 : ReferenceEquals() 和两个版本的 Equals(). 再加上比较运算符 == ,实际上有4种比较相等性的方法。 这些方法有一些细微的区别, 下面就介绍它们:
1. ReferenceEquals() 方法
ReferenceEquals() 是一个静态方法,其测试两个引用是否指向类的同一个实例, 特别是两个引用是否包含内存中的相同地址。作为静态方法,它不能重写, 所以 System.Object 的实现代码保持不变。 如果提供的两个引用指向同一个实例, 则 ReferenceEquals() 总是返回 true; 否则就返回 false。 但是它认为 null 等于 null 。 下面做个试验:
public class TestEquals { }
void Main()
{
// 测试两个引用不同的对象
TestEquals testA = new TestEquals();
TestEquals testB = new TestEquals();
Console.WriteLine( ReferenceEquals(testA, testB) );
// 测试两个引用相同的对象
TestEquals testC = testA;
Console.WriteLine(ReferenceEquals(testA, testC));
// 测试两个null的结果
Console.WriteLine(ReferenceEquals(null, null));
}
// 测试返回结果
false
true
true
2. Equals() 虚方法
Equals() 虚方法在自己的类中重写它,从而按值来比较对象。 在重写 Equals()方法时要注意, 重写的代码不应抛出异常。 同理,这是因为如果抛出异常, 字典类就会出问题, 一些在内部调用这个方法的 .net 基类也可能出问题。
public class TestEquals
{
public string Name { get; }
public TestEquals(string name)
{
this.Name = name;
}
// core
public override bool Equals(object obj)
{
return this.Name == (obj as TestEquals) ?.Name;
}
}
void Main()
{
TestEquals testA = new TestEquals("张三");
TestEquals testB = new TestEquals("李四");
TestEquals testC = new TestEquals("张三");
// 测试检查相等性
Console.WriteLine( testA.Equals(testB) );
Console.WriteLine( testA.Equals(testC) );
}
// 输出结果
false
true
3.静态的Equals()方法
静态版本与虚版本的作用相同, 其区别是静态版本带有两个参数,并对它们进行相等性比较。 这个方法可以处理两个对象中有一个是 null 的情况。 因此,如果一个对象可能是 null ,这个方法就可以抛出异常, 提供额外保护。静态重载版本首先要检查传递给它的引用是否为 null。 如果他们都是null 就返回 true(因为 null 与 null 相等)。 如果只有一个引用是 null, 就返回false。 如果两个引用实际上引用了某对象, 它就调用 Equals 的虚实例版本。 这表示在重写 Equals的实例版本时, 其效果相当于也重写了静态版本。
void Main()
{
TestEquals testA = new TestEquals("张三");
TestEquals testB = new TestEquals("李四");
TestEquals testC = new TestEquals("张三");
// 测试检查相等性
Console.WriteLine( Equals(testA, testB) );
Console.WriteLine( Equals(testA, testC) );
}
// 输出结果
false
true
4. 比较运算符 ==
最好将比较运算符看做严格的值比较和严格的引用比较。
void Main()
{
TestEquals testA = new TestEquals("张三");
TestEquals testB = new TestEquals("李四");
TestEquals testC = testA;
// 测试检查相等性
Console.WriteLine( testA == testB );
Console.WriteLine( testA == testC );
}
// 输出结果
false
true
比较值类型的相等性
在比较值类型的相等性时, 采用与引用类型相同的规则:ReferenceEquals() 用于比较引用, Equals() 用于比较值, == 可以看做是一个中间项,既可以比较引用又可以比较值。 但最大的区别是值类型需要装箱,才能把它们转换为引用,进而才能对它们执行方法。
这里有一个特别注意的地方就是 ReferenceEquals() 在应用于值类型时总是返回 false,因为调用这个方法,值类型需要装箱到对象中。造成引用不同,所以会总是返回false
bool b = ReferenceEquals(v, v); // 这里的两个v会单独进行装箱操作,所以他们的引用会不同
出于上述原因, 调用ReferenceEquals() 来比较值类型实际上没有意义。