C#-'==','Equals()','ReferencEquals()'

目录
核心结论
| 方法 | 默认作用 |
|---|---|
== |
运算符比较 |
Equals() |
对象逻辑相等性比较 |
ReferenceEquals() |
比较是否为同一个对象 |
需要特别注意:
==可以被重载(Operator Overload)Equals()可以被重写(Override)ReferenceEquals()永远比较引用,不能被重写
一、普通引用类型(Class)
定义一个普通类:
class Person
{
public string Name { get; set; }
public Person(string name)
{
Name = name;
}
}创建对象:
Person p1 = new Person("Tom");
Person p2 = new Person("Tom");
Person p3 = p1;内存关系:
p1 ──► Person("Tom")
p2 ──► Person("Tom")
p3 ──┘其中:
- p1 和 p2 是两个不同对象
- p1 和 p3 指向同一个对象
示例
Console.WriteLine(p1 == p2);
Console.WriteLine(p1.Equals(p2));
Console.WriteLine(ReferenceEquals(p1, p2));
Console.WriteLine();
Console.WriteLine(p1 == p3);
Console.WriteLine(p1.Equals(p3));
Console.WriteLine(ReferenceEquals(p1, p3));输出:
False
False
False
True
True
True原因
默认情况下:
==
≈ 比较引用
Equals()
≈ 比较引用
ReferenceEquals()
= 比较引用因此三者结果完全一致。
二、重写 Equals()
很多时候我们希望:
名字相同
=
同一个人而不是:
引用相同
=
同一个人此时可以重写 Equals()。
class Person
{
public string Name { get; set; }
public Person(string name)
{
Name = name;
}
public override bool Equals(object? obj)
{
return obj is Person p &&
Name == p.Name;
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
}测试:
Person p1 = new Person("Tom");
Person p2 = new Person("Tom");
Console.WriteLine(p1 == p2);
Console.WriteLine(p1.Equals(p2));
Console.WriteLine(ReferenceEquals(p1, p2));输出:
False
True
False原因
此时:
==
比较引用
Equals()
比较 Name
ReferenceEquals()
比较引用三、重载 == 运算符
如果希望:
p1 == p2也能比较内容,则需要重载运算符。
class Person
{
public string Name { get; set; }
public Person(string name)
{
Name = name;
}
public override bool Equals(object? obj)
{
return obj is Person p &&
Name == p.Name;
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
public static bool operator ==(
Person? left,
Person? right)
{
if (ReferenceEquals(left, right))
return true;
if (left is null || right is null)
return false;
return left.Name == right.Name;
}
public static bool operator !=(
Person? left,
Person? right)
{
return !(left == right);
}
}测试:
Person p1 = new Person("Tom");
Person p2 = new Person("Tom");
Console.WriteLine(p1 == p2);
Console.WriteLine(p1.Equals(p2));
Console.WriteLine(ReferenceEquals(p1, p2));输出:
True
True
False四、ReferenceEquals()
ReferenceEquals() 永远比较:
是否指向同一个对象例如:
Person p1 = new Person("Tom");
Person p2 = new Person("Tom");
Console.WriteLine(
ReferenceEquals(p1, p2)
);输出:
False因为:
p1 ──► 对象A
p2 ──► 对象B即使内容完全相同,也返回 False。
五、string 的特殊情况
string s1 = new string("abc".ToCharArray());
string s2 = new string("abc".ToCharArray());实际上:
s1 ──► 对象A
s2 ──► 对象B是两个不同对象。
测试:
Console.WriteLine(s1 == s2);
Console.WriteLine(s1.Equals(s2));
Console.WriteLine(ReferenceEquals(s1, s2));输出:
True
True
False原因
string 类型已经重载了:
operator ==因此:
==
比较字符串内容
Equals()
比较字符串内容
ReferenceEquals()
比较引用六、低版本 C# 如何实现 Record 的值相等
Record 出现之前的标准做法
在 C# 9 之前,如果希望对象按照值进行比较,而不是按照引用进行比较,通常会实现:
IEquatable<T>
+
Equals()
+
GetHashCode()
+
(可选) == 和 !=这也是微软长期推荐的方式。
示例
public class Person : IEquatable<Person>
{
public string Name { get; }
public int Age { get; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
public bool Equals(Person? other)
{
if (other is null)
return false;
return Name == other.Name && Age == other.Age;
}
public override bool Equals(object? obj)
{
return Equals(obj as Person);
}
public override int GetHashCode()
{
return HashCode.Combine(Name, Age);
}
public static bool operator ==(Person? left, Person? right)
{
if (ReferenceEquals(left, right))
{
return true;
}
if (left is null || right is null)
{
return false;
}
return left.Equals(right);
}
public static bool operator !=(Person? left, Person? right)
{
return !(left == right);
}
}使用:
var p1 = new Person("Tom", 18);
var p2 = new Person("Tom", 18);
Console.WriteLine(p1 == p2);
Console.WriteLine(p1.Equals(p2));输出:
True
True为什么推荐 IEquatable
如果只重写:
Equals(object obj)则每次比较都需要:
object
↓
类型检查
↓
强制转换而:
IEquatable<Person>允许:
p1.Equals(p2)直接进行强类型比较。
对于:
HashSet<T>
Dictionary<TKey,TValue>
List<T>.Contains()等集合,性能更好。
七、Record 类型
C# 9 引入了 Record。
public record Person(string Name, int Age);测试:
var p1 = new Person("Tom", 18);
var p2 = new Person("Tom", 18);
Console.WriteLine(p1 == p2);
Console.WriteLine(p1.Equals(p2));输出:
True
TrueRecord 自动生成:
- Equals()
- GetHashCode()
- == 运算符
- != 运算符
- ToString()
因此非常适合:
- DTO
- Value Object
- 配置对象
- 不可变对象
Record 的本质
可以理解为:
record
≈
IEquatable<T>
+
Equals()
+
GetHashCode()
+
== 和 !=
的语法糖编译器自动帮我们生成这些代码。
八、实际开发建议
判断内容是否相等
优先:
obj.Equals(other)或者:
IEquatable<T>判断是否是同一个对象
使用:
ReferenceEquals(obj1, obj2)自定义类
如果重写了:
Equals()必须同时重写:
GetHashCode()否则:
HashSet<T>
Dictionary<TKey,TValue>会出现不可预期的问题。
高频问题
== 和 Equals() 的区别?
简答:
==是运算符,可以重载Equals()是方法,可以重写- 默认情况下引用类型二者都比较引用
- 对于 string、record 等类型二者通常比较内容
为什么重写 Equals() 必须重写 GetHashCode()?
因为必须保证:
a.Equals(b) == true时:
a.GetHashCode() == b.GetHashCode()否则哈希集合行为会异常。
Record 出现之前如何实现值相等?
标准答案:
实现 IEquatable<T>
重写 Equals()
重写 GetHashCode()
必要时重载 == 和 !=总结
ReferenceEquals()
↓
永远比较是否为同一个对象
Equals()
↓
比较逻辑是否相等
==
↓
比较规则由类型作者决定对于普通 class:
==
≈ ReferenceEquals
Equals()
≈ ReferenceEquals对于 string、record、自定义值对象:
==
≈ Equals()
比较的是内容因此不要认为:
==
一定比较地址在 C# 中,真正含义取决于该类型是否重载了 == 运算符。