Tip
本文是已了解至少一种编程语言并正在学习 C# 的开发人员的 “基础知识 ”部分的一部分。 如果你不熟悉编程,请先 学习入门 教程。
来自另一种语言? 在 Java 中为对象使用 ==,以及在 JavaScript 中为对象使用 ===,测试的是标识,而非内容。 默认情况下,C# 类的工作方式相同。 在 Python 中,== 默认会调用 __eq__ 并按内容进行测试,这类似于 C# 记录的比较方式。 调用 Equals 时,C# 结构体 也会按值比较。
C# 区分两种相等性。 值相等 意味着两个实例在数据匹配时相等。 引用相等 意味着,仅当两个变量指向内存中的同一对象时,两个变量才相等。 此条件也称为 标识。 值类型通常比较数据,引用类型通常比较标识。 类型作者可以更改这些默认设置,但这种思维模型有助于避免一些隐蔽的 bug:例如,两个看起来完全相同的对象却不被认为是相等的,或者通过一个变量进行修改会悄然改变另一个变量所见的内容。
值类型、引用类型和相等默认值
C# 中的每个类型都是 值类型 或 引用类型。 值类型直接在变量中保存其数据。 引用类型保存对对象的引用。 将引用类型变量分配给另一个变量时,这两个变量都引用同一对象。 有关值类型和引用类型的详细信息,请参阅 类型系统概述。
默认相等行为通常遵循类型:
-
内置数值类型和 枚举 是值类型。 当
int两个变量的数值匹配时,两个变量相等。 -
结构 是值类型。 普通
struct在调用 Equals时使用值相等性。 - 元组 是值类型。 当两个元组的所有元素值都相同时,这两个元组就相等。
-
类 是引用类型。 纯类使用引用相等性,因此
==测试 Equals 两个变量是否指向同一对象。
某个类使用引用相等进行比较。 具有相同数据的两个单独的对象不相等,但引用同一对象的两个变量相等:
var order1 = new Order(42, "Shoes");
var order2 = new Order(42, "Shoes");
Console.WriteLine(order1 == order2); // => False
Console.WriteLine(order1.Equals(order2)); // => False
Console.WriteLine(ReferenceEquals(order1, order2)); // => False
Order order3 = order1;
Console.WriteLine(order1 == order3); // => True
struct 通过 Equals 显示值相等。 当两个结构实例的字段匹配时,两个结构实例相等:
var pt1 = new Point(3, 4);
var pt2 = new Point(3, 4);
Console.WriteLine(pt1.Equals(pt2)); // => True
纯结构不会获取预定义 == 运算符。 只有当该结构体声明了自己的 operator == 时,在普通结构体上编写 p1 == p2 才能通过编译。 如果需要对结构进行运算符比较,请定义 == 并 != 将它们定义为对,并使其与 Equals 和 GetHashCode保持一致。
元组也是值类型。 当每个对应元素的值都相同时,两个元组相等。 命名元组中的元素名称仅在编译时提供便利,在进行比较时不会被考虑。 只有位置和值很重要:
var t1 = (Name: "Grace", Role: "Engineer");
var t2 = (Name: "Grace", Role: "Engineer");
Console.WriteLine(t1 == t2); // => True
有关元组语法和析构的详细信息,请参阅 元组和解构。
使用 Object.ReferenceEquals 直接测试身份
ReferenceEquals 始终测试标识相等性,而不考虑类型如何替代 Equals 或重载 ==。 当需要确认两个变量是否指向完全相同的对象时,请将其用作标识诊断:
var doc1 = new Document("Report");
var doc2 = new Document("Report");
var doc3 = doc1;
Console.WriteLine(ReferenceEquals(doc1, doc2)); // => False
Console.WriteLine(ReferenceEquals(doc1, doc3)); // => True
一个常见用法是在 Equals 替代中用于对完整比较进行短路处理:当两个参数是同一引用时,它们始终相等,无需逐个检查字段。
注释
当变量类型化为 接口时, == 检查接口变量是否引用同一对象。 调用 Equals 仍会运行底层对象的实现。
注释
ReferenceEquals 在比较值类型时始终返回 false ,即使这两个参数都包含相同的值。 之所以会出现这种行为,是因为每个值类型参数在传递给 时,都会被独立ReferenceEquals为单独的堆对象。
类型可以定义不同的相等语义
类型 可以 定义与默认行为不同的相等语义。 最常见的原因是实现值相等。 如果创建表示数据的类型(例如银行帐户、库存中的产品或系统中的用户),请考虑具有相同值的实例。 选择用于实现值相等性的记录类型,编译器会为你生成所有必要的相等成员。
注释
字符串是类,但==和Equals比较的是字符串内容,而不是对象标识。
-
==:相等运算符。 大多数类型都使用此运算符作为主要相等性检查。 其行为取决于类型是具有内置运算符还是用户定义的==运算符。 -
!=:不相等运算符。 当类型定义用户定义的==运算符时,它还必须定义!=。 - Equals:每个类型继承的虚拟方法。 你可以重写它,以更改某个类型的相等性语义。
- GetHashCode:供基于哈希的集合使用的虚拟方法。 当两个值相等时,它们的哈希代码也必须相等。
- ReferenceEquals:始终执行标识测试的静态方法。
使用记录进行值相等性比较
当类型可以是记录时,请使用 record 修饰符为类型提供数据导向的值相等性。 编译器生成EqualsGetHashCode==/!=和成员来比较每个声明的属性值。
A record class 仍然是引用类型,但它比较值而不是标识:
var person1 = new Person("Ada", "Lovelace");
var person2 = new Person("Ada", "Lovelace");
Console.WriteLine(person1 == person2); // => True
Console.WriteLine(person1.Equals(person2)); // => True
Console.WriteLine(ReferenceEquals(person1, person2)); // => False
ReferenceEquals确认person1person2内存中存在不同对象,同时==并Equals返回True,因为编译器生成的相等性比较属性值。
相同的编译器生成适用于 record struct 类型:
var dim1 = new Dimension(1920, 1080);
var dim2 = new Dimension(1920, 1080);
Console.WriteLine(dim1 == dim2); // => True
Console.WriteLine(dim1.Equals(dim2)); // => True
记录类型会为其自身类型生成整套相等性成员。 和record classrecord struct类型都替代Equals和 GetHashCode。 它们还会为记录类型生成 == 和 != 运算符,以及一个类型化的 Equals 方法。 与普通的 struct 不同,record struct 因此会自动支持 == 和 !=。 有关记录类型及其相等语义的详细信息,请参阅 记录。
包含引用类型成员的记录
记录相等使用成员自己的相等语义。 使用自己的 Equals 方法比较每个属性或字段。 对于大多数标量值(例如 int, string或 DateTime)此方法比较记录成员的值。 微妙之处在于,像 List<T> 或 T[] 这样的常见可变集合:这些类型是按引用比较的,因此,在编译器合成的记录相等性中,包含 内容相同但列表对象不同 的两个记录实例 不 被视为相等。
var playlist1 = new Playlist("Chill", new List<string> { "Song A", "Song B" });
var playlist2 = new Playlist("Chill", new List<string> { "Song A", "Song B" });
Console.WriteLine(playlist1.Equals(playlist2)); // => False (different List instances)
Console.WriteLine(playlist1.Tracks.SequenceEqual(playlist2.Tracks)); // => True
playlist1 并且 playlist2 是单独的 List<string> 实例。 即使它们的内容一致,Equals 也会返回 false。
需要记录相等性来反映集合 内容时,有一些选项:
- 在该记录上实现
IEquatable<T>,并重写Equals以对集合成员使用Enumerable.SequenceEqual。 -
使用具有值相等性的集合类型——例如,自定义
IEqualityComparer<T>或其自身的Equals会比较元素的类型。 - 围绕标识进行设计:如果记录表示实体而不是纯值,则其集合成员的引用相等性可能是有意的。
Important
在 C# 中,手动实现相等性很少见。 记录会自动处理值相等的常见方案。 如果需要手动实现相等性(例如,因为类型必须派生自非记录基类)-请参阅 当类型不能是语言引用中的记录时自行实现相等性 。
另见
- 类型系统概述
- 类
- 结构
- 记录
- 元组和析构。
- 相等运算符(语言参考)。
- 类层次结构中的相等性 - 有关多态相等的高级指南。
- 算术、比较、逻辑和赋值运算符 — 相等运算符概述,以及算术、比较、逻辑和赋值运算符。