在 .NET 中比较值是否相等:标识与等价






4.89/5 (101投票s)
一篇阐明在 .NET 中比较两个值是否相等的各种方式的文章
引言
在 .NET 中比较两个值是否相等的各种方式可能会非常令人困惑。事实上,如果我们要在 C# 中有两个对象 `a` 和 `b`,至少有四种方法可以比较它们的标识,外加一个看起来像标识比较的运算符,这更增加了混淆。
if (a.Equals(b)) {}
if (object.Equals(a, b)) {}
if (object.ReferenceEquals(a, b) {}
if (a == b) {}
if (a is b) {}
如果这还不够令人困惑,那么这些方法和运算符的行为还会根据以下情况而有所不同:
- `a` 和 `b` 是引用类型还是值类型
- 它们是引用类型,但为了这些目的而表现得像值类型(`System.String` 就是其中之一)。
本文旨在阐明我们为什么会有所有这些相等性版本,以及它们各自的含义。
什么意味着“相同”?
首先,我们必须理解对象实际上有两种基本类型的相等性:
- 标识(引用相等):两个对象是相同的,如果它们实际上是内存中的同一个对象。也就是说,指向它们的引用指向相同的内存地址。
- 等价(值相等):两个对象是等价的,如果它们包含的值相同。
所以,如果我们有两个整数 `a` 和 `b`,都设置为值 3,那么它们是等价的(它们具有相同的值),但不一定是相同的(`a` 和 `b` 可以指向不同的内存地址)。
然而,如果两个对象是相同的(同一个对象),那么它们必须是等价的(具有相同的基础值)。
我们期望哪种类型的相等性?
显然,这些标识和等价的概念与引用类型和值类型的概念有关。
值类型被设计为轻量级对象,具有值语义:两个对象相同,如果它们的值相同,然后可以互换使用。因此,上面示例中的整数 `a` 和 `b` 是相同的,因为它们的值都是 3,而 `a` 和 `b` 的引用是否实际指向内存中的同一个底层对象并不重要。
通常我们不期望引用类型像这样工作。假设我们有两个单独的 `Book` 类型对象(一个类)。`Book` 有一个名为 '`title`'(一个 `string`)的成员变量。如果我们仅仅因为它们具有相同的 `title` 就认为它们是“相同”的 `Book` 吗?我们可能会这样做,但并不明确。
为了澄清情况,我们可以添加一个额外的字段 '`BookId`',该字段对于给定实际书籍是唯一的。然后,我们可以说两本书是相同的,如果它们具有相同的 `BookId`,即使它们的标题不同。但那样的话,我们通常不期望在同一时间在内存中有两个具有相同 `BookId` 的单独 `Book` 对象:只有一个底层书籍。所以我们可能只需要比较内存地址来判断两本书是否相同。
关键在于引用类型的相等性定义起来更棘手。我们默认的定义将是,两个引用类型是相同的,如果它们是相同的。
相等性类型
现在,我将依次介绍第一段中提到的各种相等性类型,并尝试解释它们为什么存在。我还将解释它们是如何为值类型和引用类型实现的,以及何时应该重写或重载它们。
-
a.Equals(b)
-
概述
`Equals()` 是 `System.Object` 上的一个虚拟方法。这意味着每个对象都可以调用它,并且在您自己的类型定义中,您可以重写它以提供您想要的行为。
`Equals()` 的基类 `System.Object` 实现是为了进行标识比较。然而,`Equals()` 的目的是测试标识或等价,以适当的方式(参见上面段落的讨论)。
-
值类型
对于值类型,此方法被重写以进行值(等价)比较。特别是,`System.ValueType` 本身,所有值类型的根,包含一个重写,它将通过反射其内部字段来比较两个对象,以查看它们是否都相等。如果您继承此(通过设置一个结构),则您的结构将默认获得此重写。
-
引用类型
对于引用类型,如上所述,情况更棘手。通常我们期望引用类型的 `Equals()` 进行标识比较(检查对象是否实际上是内存中的同一个对象)。
然而,某些引用类型不够轻量级,无法像值类型那样工作,但它们仍然具有值语义。典型的例子是 `System.String`。`System.String` 是一个引用类型。然而,如果我们有 `a = "abc"` 和 `b = "abc"`,我们期望 `a` 等于 `b`。因此,在框架中,`Equals()` 被重写以进行值比较。
-
重写还是不重写?
如上所述,对于值类型,基类 `System.ValueType` 中有一个默认的 `a.Equals(b)` 重写,它可以用于您设置的任何结构。此方法使用反射来迭代您要比较的两个值类型的所有字段,检查它们的值是否相等。通常这就是您想要的值类型比较。
但是,重写的 `Equals()` 方法使用反射,这很慢,并且涉及一定量的装箱。为了提高速度,可以重写此方法。有关此的更详细讨论,请参见 Jeffrey Richter 的书籍《Applied Microsoft .NET Framework Programming》。
在定义新的引用类型(类)时,通常认为将 `Equals()` 保留在其默认标识比较上是一个好习惯。例外情况是,当您知道希望类具有值语义(如 `System.String`)时,或者当您希望 `Equals` 以特定方式工作时。特别是,如果您的类将用作 `Hashtable` 的键,则需要重写 `Equals` 以提高效率。
请注意,如果您重写了 `a.Equals(b)`,您也应该重写 `GetHashCode()`,并考虑重写 `IComparable.CompareTo()`。
-
-
object.Equals(a, b)
-
概述
`object.Equals(a, b)` 是 `object` 类上的一个静态方法。Jeffery Richter 将其描述为“一个小辅助方法”。最容易将其视为一个进行一些 `null` 检查然后调用 `a.Equals(b)` 的方法。
它存在的原因是,如果 `a` 是 `null`,调用 `a.Equals(b)` 将会抛出 `NullReferenceException`。如果 `a` 可能为 `null`,调用 `object.Equals(a, b)` 比显式检查 `null` 更容易。如果 `a` 不可能是 `null`,则不需要额外的检查,调用 `a.Equals(b)` 会更好。
-
详细信息
详细来说,此方法对于调用 `object.Equals(a, b)` 执行以下操作:
- 检查 `a` 和 `b` 是否相同(即,它们指向同一内存位置或两者都是 `null`)。如果是,则返回 `true`。
- 检查 `a` 或 `b` 中是否有任何一个为 `null`。我们知道它们不都是 `null`,否则该例程将返回 1)以上,因此如果其中任何一个为 `null`,则返回 `false`。
- `a` 和 `b` 都不是 `null`:返回 `a.Equals(b)` 的值。
-
值类型和引用类型
由于值类型 `a` 和 `b` 不可能为 `null`,因此 `object.Equals(a, b)` 与 `a.Equals(b)` 相同。通常,对于值类型,您应该优先调用 `a.Equals(b)` 而不是 `object.Equals(a, b)`。
对于引用类型,如上所述,如果 `a` 在调用 `a.Equals(b)` 时可能为 `null`,则应调用此方法。
-
重写还是不重写?
`object.Equals(a, b)` 是 `System.Object` 上的静态方法,因此无法重写。但是,由于它调用 `a.Equals(b)`,因此 `Equals` 的任何重写也会影响对此方法的调用。
-
-
object.ReferenceEquals(a, b)
-
概述
虽然上面 `Equals()` 的两个实例根据底层类型检查标识或等价,但 `ReferenceEquals` 旨在始终检查标识。
-
值类型和引用类型
对于引用类型,`object.ReferenceEquals(a, b)` 当且仅当 `a` 和 `b` 具有相同的底层内存地址时返回 `true`。
通常我们不应该关心值类型是否占据相同的底层内存地址。这对我们通常想要使用它们做的任何事情都不相关。但是上面的定义在我们用 `ReferenceEquals` 比较值类型时会带来问题。
困难在于 `ReferenceEquals` 期望两个 `System.Objects` 作为参数。这意味着,当我们的值类型作为参数传递给此例程时,它们会被装箱到堆上。通常,由于装箱过程的工作方式,它们会被单独装箱到堆上的不同内存地址。这当然意味着 `ReferenceEquals` 的调用返回 `false`。
因此,例如,`object.ReferenceEquals(10, 10)` 返回 `false`,原因如下。
您可以在以下代码中看到装箱导致了问题
// Set up value type in int variable - no boxing int value = 10; object one = value; // Cast to object so boxed object two = value; // Cast again so boxed again separately // one and two are now separate memory locations on the heap Console.WriteLine(object.ReferenceEquals(one, two)); // false // Set up value type in object variable which // immediately boxes it onto the heap object value2 = 10; // value is boxed already object three = value2; // three points to the boxed value object four = value2; // four also points to the same boxed value Console.WriteLine(object.ReferenceEquals(three, four)); // true
-
重写还是不重写?
`ReferenceEquals` 是 `object` 上的一个静态方法,因此再次无法重写。它将始终执行上面概述的标识检查。
-
-
a == b
-
概述
`==` 显然是一个运算符,而不是一个方法。恕我直言,它之所以被包含在 C# 中,主要是为了语法上的便利,并使该语言看起来像 C/C++。
与 `a.Equals(b)` 一样,`==` 的目的是根据需要测试标识或等价(参见上面“我们期望哪种类型的相等性?”段落的讨论。事实上,在几乎所有情况下,`==` 的行为都应该像 `a.Equals(b)`。
-
值类型
对于 .NET Framework 中的值类型,`==` 的实现符合预期,并将测试等价(值相等)。但是,对于您实现的任何自定义值类型(结构),除非您提供一个,否则默认的 `==` 将不可用。
-
引用类型
对于引用类型,默认的 `==` 是可用的,它将测试标识(引用相等)。对于 .NET Framework 中的大多数引用类型,`==` 将再次测试标识,但是,就像 `a.Equals(b)` 一样,有一些类重载了该运算符以执行值比较。`System.String` 再次是典型示例,原因已在本文第一部分讨论过。
-
重写(重载?)还是不重写?
由于 `==` 是一个运算符,我们不能重写它。但是,我们可以重载它以提供与上述基本功能不同的功能。
对于引用类型,Microsoft 建议您不要重载 `==`,除非您有行为像值类型的引用类型,如上所述。这意味着,即使您重写了 `a.Equals(b)` 以提供一些自定义功能,也应该让您的 `==` 运算符执行标识测试。我认为,这是 `==` 应该与 `a.Equals(b)` 行为不同的唯一场合。
对于值类型,如上所述,将不可用默认的 `==` 重载,如果您需要的话,您必须提供一个。最简单的方法是在您的结构中通过运算符重载调用 `a.Equals(b)`:通常,您的 `==` 实现不应与 `a.Equals(b)` 不同。
请注意,如果您重载了 `==`,您也应该重载 `!=`。您还应该重写 `a.Equals(b)` 以执行相同的操作,因此也应该重载 `GetHashCode`。最后,您应该考虑重写 `IComparable.CompareTo()`。
-
使用 `==` 和引用类型时的注意事项
最后需要注意的一点是,运算符重载的行为不像重写。如果您在使用引用类型时没有仔细考虑就使用 `==` 运算符,这可能会成为一个问题。
例如,假设您有一个非类型化的 `DataSet ds`,其中包含一个 `DataTable dt`。假设它有 Id 和 Name 列。`dt` 有两行。考虑以下代码
// Create DataSet DataSet ds= new DataSet("ds"); DataTable dt= ds.Tables.Add("dt"); dt.Columns.Add("Value", typeof(int)); // Add two rows, both with Value column set to 1 DataRow row1= dt.NewRow();row1["Value"] = 1;dt.Rows.Add(row1); DataRow row2= dt.NewRow();row2["Value"] = 1;dt.Rows.Add(row2); Console.WriteLine(row1["Value"] == row2["Value"]); // Compare with == returns false. Console.WriteLine(row1["Value"].Equals(row2["Value"])); // Compare with .Equals returns true.
在上面的示例中,当我们使用 `==` 进行比较时,我们得到 `false`,即使两行的列都包含整数 `1`。原因是 `row1[Value]` 和 `row2[Value]` 都返回对象,而不是整数。因此,`==` 将使用 `System.Object` 中的 `==`,而不是整数中的任何重载版本。`System.Object` 中的 `==` 执行标识比较(引用相等测试)。底层值已被单独装箱到堆上,因此不在同一个内存地址,测试失败。
当我们使用 `.Equals` 进行比较时,我们得到 `true`。这是因为 `System.Int32` 中的 `.Equals` 被重写以执行值比较,因此比较使用了重写版本来正确比较两个整数的值。
-
-
a is b
-
概述
`a is b` 实际上并不是一个对象相等性测试,尽管它看起来像。这里的 `b` 必须是一个类型名称(所以 `b` 需要是一个类名,例如)。该运算符测试对象 `a` 是否属于类型 `b` 或可以转换为它而不抛出异常。这相当于 VB.NET 中的 `TypeOf a Is b`,后者稍微更清晰一些。
-
值类型/引用类型
对于值类型和引用类型,该运算符的工作方式相同。
-
重写(重载?)还是不重写?
该运算符无法重载(或重写,显然)。
-
最后的转折:字符串驻留
基于以上内容,这应该会做什么?
object a = "Hello World";
object b = "Hello World";
Console.WriteLine(a.Equals(b));
Console.WriteLine(a == b);
乍一看,您可能会说:
- `a` 和 `b` 是包含字符串的引用类型(您是对的)。
- `string` 类重写了 `.Equals` 以执行等价(值)比较,并且值相等。因此 `a.Equals(b)` 为 `true`(您仍然是对的)。
- 然而,`a == b` 是一个重载,在 `object` 类型上它执行标识比较,而不是值比较(您仍然是对的)。
- `a` 和 `b` 是内存中不同的对象,因此 `a == b` 为 `false`(您错了)。
4. 实际上是错误的,但这仅仅是因为 CLR 中的一项优化。CLR 在应用程序中维护一个所有正在使用的字符串的列表,称为驻留池。当代码中设置一个新字符串时,CLR 会检查驻留池以查看该字符串是否已在使用中。如果是,它将不会为该字符串再次分配内存,而是会重用现有内存。因此,上面的 `a == b` 为 `true`。
您可以通过使用 `StringBuilder` 来阻止字符串驻留,如下所示。在这种情况下,`a.Equals(b)` 将为 `true`,而 `a == b` 将为 `false`,这正是您所期望的。
object a = "Hello World";
object b = new StringBuilder().Append("Hello").Append(" World").ToString();
Console.WriteLine(a.Equals(b));
Console.WriteLine(a == b);
VB.NET
本文主要讨论了 C#。然而,VB.NET 中的情况同样令人困惑。因为它们是 `System.Object` 上的方法,VB.NET 具有 `a.Equals(b)`、`object.Equals(a, b)` 和 `object.ReferenceEquals(a, b)` 方法,这些方法与上面描述的方法相同。
VB.NET 没有 `==` 运算符,或任何等效的运算符。
VB.NET 还具有 `Is` 运算符。`TypeOf a Is b` 语句中该运算符的使用在上面“`a` is `b`:概述”中进行了讨论。
VB.NET:a Is b
在 VB.NET 中,`Is` 运算符还可以用于两个引用类型的标识(引用相等)比较。然而,与 `a.ReferenceEquals(b)`(对引用类型执行相同操作)不同的是,`Is` 运算符根本不能用于值类型。Visual Basic 编译器不会编译 `a Is b` 语句中 `a` 或 `b` 是值类型的代码。
参考文献
- Jeffrey Richter "Applied Microsoft .NET Framework Programming"
http://www.microsoft.com/mspress/books/sampchap/5353.aspx#SampleChapter - Interning strings
http://msdn2.microsoft.com/en-us/library/system.string.intern.aspx - When to overload `==`
http://msdn2.microsoft.com/en-us/library/ms173147.aspx - Ravi Gyani 的更简洁的“Understanding Equality in C#”
https://codeproject.org.cn/dotnet/Equality.asp