相等与否的判定在任何一门语言中都很是很重要的一部分,在决策、分支问题上意义重大。
本文针对C#语言中的相等技术进行测试,如果需要更详细的了解,可以阅读本文的参考博文(见文章最后)。
C#中提供的四种比较技术
- ReferenceEquals()
- 运算符比较 == !=
- static Equals()
- instance Equals()
ReferenceEquals()
C#中的对象分为引用类型和值类型,因此相等也可以分为两种,值类型相等(两个值类型的变量在他们的类型和内容都相同时应该是相等)和引用类型相等(两个引用类型的变量在引用同一个对象时是相等的)。
- 无论值类型还是引用类型,ReferenceEquals()方法比较的总是两者的对象ID是否一致,即两者是不是同一个对象。
- 针对值类型使用ReferenceEquals,结果永远都是false,因为该方法要求传入两个引用对象,这里会对两个值类型的对象分别进行装箱操作,得到的两个引用自然不同
- 该方法实现的功能和他的名称保持一致,即比较两个变量的对象id是否一致,不存在重新定义的需求。
static void Main(string[] args)
{
//值类型
int a = 20, b = 20;
//false: 虽然值相同,但两者具有不同的对象ID
Console.WriteLine(Object.ReferenceEquals(a, b));
//false:对于值类型使用ReferenceEquals,总会返回false,即使比较同一个变量
//因为该方法要求两个引用对象,这里会对两个值分别进行装箱操作,得到两个引用自然不同
Console.WriteLine(Object.ReferenceEquals(a, a));
//引用类型
object c = 10;
object d = 10;
//false:两个引用不指向同一个对象
Console.WriteLine(Object.ReferenceEquals(c, d));
//true: 指向同一个对象
Console.WriteLine(Object.ReferenceEquals(c, c));
}
运行结果:
运算符比较 == !=
- 默认情况下,值类型的数据使用值类型相等,引用类型使用引用类型相等。
- 涉及到重写问题,可以参考 instance Equals方法
static void Main(string[] args)
{
//值类型比较:默认比较两个值是否相同
int x = 5;
int y = 5;
//由于两次装箱操作,基本无意义 永远都是false
Console.WriteLine(" x,y 两个变量是否具有相同的对象ID:" + Object.ReferenceEquals(x, y));
//True
Console.WriteLine(" x,y 两个变量是否值类型相等:" + (x == y));
//装箱,引用类型比较:默认比较两个引用是否指向同一个对象
object a = 5;
object b = 5;
//false:不是同一个对象
Console.WriteLine(" a,b 两个变量是否具有相同的对象ID:" + Object.ReferenceEquals(a, b));
//false:不是同一个对象
Console.WriteLine(" a,b 两个变量是否引用类型相等:" + (a == b));
}
static Equals(object left,object right)
- 在c#中,object类是所有类型的基类,因此可以传入值类型,也可以传入引用类型
- 使用时机:当不清楚两个参数的运行类型时使用
- 该方法仅提供了一个比较的思路,不涉及具体的比较,因此不涉及重写问题。
- 无论值类型还是引用类型,都默认实现了GetHashCode方法,所以你不用重写这个方法,除非你需要重写Equals方法。(因此,如果你重写了GetHashCode方法,那么你肯定是需要重写Equals方法)。
- 具体实现原理如下代码所示:
public static bool Equals(object left,object right)
{
//值类型比较值类型,引用类型比较引用类型
if (left == right)
return true;
//检查是否有值为null,一旦存在null,永远返回false
if (left == null || right == null)
return false;
//使用instance Equals方法
return left.Equals(right);
}
相等的数学性质:自反性,对称性,传递性
在讨论 instance Equals之前,必须明确相等具有哪些数学性质。
- 自反性:一个对象必须永远是等于自己的,a==a 必须成立
- 对称性:如果a==b为真,那么b == a也必须为真。
- 传递性:如果a == b,b==c,那么a == c必须成立。
instance Equals()
- 当该函数的默认行为与开发者所需要的判定方式不一致时,需要进行自定义,有重写需求。
- 如果没有重写,相等的判断方式与ReferenceEquals一致
- 值类型是不同的,值类型没有重载Object.Equals,ValueType.Equals()实现这一机制。
static void Main(string[] args)
{
//值类型一般需要比较的都是值是否相等,ValueType.Equals()实现这一个功能
int x = 5;
int y = 5;
Console.WriteLine(x.Equals(y));
//默认情况下,针对引用类型,处理机制与ReferenceEquals保持一致
//运行结果:false
Test my01 = new Test(10);
Test my02 = new Test(20);
Console.WriteLine(my01.Equals(my02));
//StringBuilder进行了重写,自定义了自己的判断机制
//运行结果:true
StringBuilder text01 =new StringBuilder("abc") ;
StringBuilder text02 = new StringBuilder("abc");
Console.WriteLine(text01.Equals(text02));
}
public class Test
{
private int a;
public Test(int x)
{
a = x;
}
}
运行结果:
重载自己的instance Equals()的要求和标准模式
- Equals方法不应该也不能抛出异常。两个变量要么相等,要么不等,没有其他结果。
- 需要检查传出的值是否为null
- 如果对象Id一致,直接判定相等,自反性原则。
- 判定数据类型是否相同
- 自定义的判断逻辑
public override bool Equals(object right)
{
//检查是否为空
if (right == null)
return false;
//检查是否为同一对象
if (object.ReferenceEquals(this, right))
return true;
//类型检查
if (this.GetType() != right.GetType())
return false;
//自定义比较方法,因为涉及到派生概念,所以需要使用as做一次转换
return CompareMethod(this, right as Foo);
}
IEquatable
为了保持完整性,建议在重写Equals方法时,同时实现IEquatable接口。接口方法的结果应当与自定义重写后Equals方法的结果一致。如果你已经重写了Equals方法,那么实现IEquatable不需要额外的实现代码(直接调用Equlas方法即可)
internal class Staff : IEquatable<Staff>
{
public string FirstName { get; set; }
// implements IEquatable<Staff>
public bool Equals(Staff other)
{
return this.FirstName.Equals(other.FirstName);
}
// override Equals
public override bool Equals(object obj)
{
if (obj == null)
return this == null;
if (!(obj is Staff))
return false;
Staff s = obj as Staff;
return this.FirstName == s.FirstName;
}
// override GetHashCode
public override int GetHashCode()
{
return this.FirstName.GetHashCode();
}
}
参考文章
- Effective C# 中文版 改善C#程序的50种方法(旧版):“原则9:明白几个相等运算之间的关系"
- C#相等性比较
- C#之相等比较(常规比较)
- C#比较两个对象是否相等(深度比较)
- 浅析C#中的IEquatable接口