在 C# 中,"相等"并不是一个单一概念。根据类型、上下文和运算符/方法的不同,相等性判断可能表示引用相等 、值相等 或自定义语义相等 。
常见的四种判断方式如下:
a.Equals(b)Object.Equals(a, b)a == bObject.ReferenceEquals(a, b)
它们在实现机制、空值处理、可重写性以及适用场景上均存在显著差异。本文将从 CLR 角度出发,对它们进行逐一剖析,并给出实践建议。
一、相等性的三种语义
在进入具体 API 之前,先明确三种核心语义:
| 语义 | 含义 |
|---|---|
| 引用相等(Reference Equality) | 两个变量是否指向同一个对象实例 |
| 值相等(Value Equality) | 两个对象的内容是否相同 |
| 语义相等(Semantic Equality) | 按业务规则定义的"相等" |
C# 的不同判断方式,本质上是在这三者之间切换。
二、Object.ReferenceEquals(a, b)
1. 定义
csharp
public static bool ReferenceEquals(object? objA, object? objB);
2. 行为特征
- 只判断引用是否相同
- 不可重写
- 不会调用
Equals - 不会触发运算符重载
- 对值类型参数会发生装箱
3. 判定规则
| 情况 | 结果 |
|---|---|
| 两者引用同一实例 | true |
| 两者引用不同实例 | false |
两者均为 null |
true |
一个为 null |
false |
4. 示例
csharp
string a = "hello";
string b = new string("hello".ToCharArray());
Object.ReferenceEquals(a, b); // false
5. 使用场景
- 判断是否为同一实例
- 底层框架、缓存、单例检测
- 调试对象生命周期
⚠️ 不用于业务逻辑中的"相等"判断
三、a.Equals(b)
1. 定义来源
csharp
public virtual bool Equals(object? obj);
该方法定义在 System.Object 中,可被重写。
2. 调用机制
-
实例方法
-
调用的是 运行时类型 的
Equals实现 -
可能是:
Object.Equals- 重写后的
Equals - 值类型的结构比较
3. 默认行为(未重写)
csharp
public virtual bool Equals(object obj)
{
return ReferenceEquals(this, obj);
}
即:默认等价于引用相等
4. 示例(值相等)
csharp
public class Person
{
public string Name { get; set; }
public override bool Equals(object obj)
{
return obj is Person p && p.Name == Name;
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
}
csharp
var p1 = new Person { Name = "Tom" };
var p2 = new Person { Name = "Tom" };
p1.Equals(p2); // true
5. 注意事项
- 若
a为null,会抛出NullReferenceException - 重写
Equals必须同时重写GetHashCode
四、Object.Equals(a, b)
1. 定义
csharp
public static bool Equals(object? objA, object? objB);
2. 内部逻辑(简化)
csharp
if (objA == objB) return true;
if (objA == null || objB == null) return false;
return objA.Equals(objB);
3. 核心特性
- 空安全
- 若两者非空,调用
objA.Equals(objB) - 尊重类型重写的
Equals
4. 示例
csharp
Object.Equals(null, null); // true
Object.Equals(null, obj); // false
5. 使用场景
- 通用工具方法
- 框架/基础库
- 不确定是否为
null的对象比较
✅ 推荐在公共代码中使用
五、a == b
1. 本质
-
运算符
-
行为取决于:
- 是否为值类型
- 是否重载
operator ==
2. 对引用类型(未重载)
csharp
// 等价于 ReferenceEquals
3. 对值类型
- 比较字段值(逐字段)
- 不可为
null
4. 对重载的引用类型(如 string)
csharp
string a = "hi";
string b = new string("hi".ToCharArray());
a == b; // true
string 对 == 做了值相等重载。
5. 风险点
- 同一表达式,不同类型行为不同
- 重载不当会破坏直觉一致性
六、行为对比总表
| 判断方式 | 空安全 | 是否可重写 | 是否值比较 | 是否引用比较 |
|---|---|---|---|---|
ReferenceEquals |
✔ | ❌ | ❌ | ✔ |
a.Equals(b) |
❌ | ✔ | 取决于实现 | 取决于实现 |
Object.Equals(a,b) |
✔ | ✔ | 取决于实现 | 取决于实现 |
a == b |
取决于类型 | ✔(运算符) | 取决于类型 | 取决于类型 |
七、实践建议(最佳实践)
1. 业务对象
- 重写
Equals+GetHashCode - 同时重载
==与!= - 保持三者语义一致
2. 框架/工具代码
- 使用
Object.Equals(a, b) - 避免
a.Equals(b)直接调用
3. 判断实例唯一性
- 使用
ReferenceEquals
八、结语
C# 中的"相等"是一个多层次、多语义的问题 。
理解这四种方式的差异,不仅能避免隐藏 bug,还能写出更健壮、可维护的代码。