C# 中的相等运算符 (==) 和值类型






4.33/5 (13投票s)
在本文中,您将了解非原始值类型的相等运算符 (==) 的行为。
背景
本文是对 .NET 中相等性如何工作的系列文章的延续,目的是让开发人员更清楚地理解 .NET 如何处理不同类型的相等性。我们已经看到了原始类型、引用类型相等性的工作方式,我们还单独讨论了 String
类型的相等性是如何工作的。以下是我们到目前为止所理解的一些要点。
到目前为止的关键知识点
- 默认情况下,
virtual Object.Equals
方法对引用类型执行引用相等性,对值类型执行值相等性,但对于值类型,它使用反射,这对于值类型来说会带来性能开销,并且任何类型都可以override
Object.Equals
方法来更改其相等性检查逻辑,例如 String, Delegate
和Tuple
就是这样做的,以提供值相等性,即使它们是引用类型。 Object
类还提供了一个static
Equals
方法,当其中一个或两个参数可能为null
时可以使用该方法,除此之外,它的行为与virtual
Object.Equals
方法完全相同。- 还有一个
static ReferenceEquals
方法,它提供了一种保证检查引用相等性的方法。 IEquatable<T>
接口可以实现到某个类型上,以提供强类型的Equals
方法,该方法还可以避免值类型的装箱。它已为原始数值类型实现,但不幸的是,微软在FCL( Framework Class Library )
中对其他值类型的实现并不积极。- 对于
Value Types
,使用== operator
会得到与调用Object.Equals
相同的结果,但== operator
的底层机制在IL( Intermediate Language )
中与Object.Equals
不同,因此不会调用为该原始类型提供的Object.Equals
实现,而是会调用一个IL
指令ceq
,该指令表示比较当前加载到堆栈上的两个值,并使用 CPU 寄存器执行相等性比较。 - 对于
Reference Types
,== operator
和Object.Equals
方法调用在后台的工作方式不同,可以通过检查生成的 IL 代码来验证。它还使用ceq
指令来比较内存地址。
如果以上几点对您来说没有意义,最好从头开始阅读,以下是相关先前内容的链接
- .NET 中相等性的故事
- C# 中的相等性和 Object 类(第 2 部分)
- C# 中的相等性和 IEquatable<T> 接口(第 3 部分)
- C# 中的 == 运算符和原始类型(第 4 部分)
- C# 中的 == 运算符和引用类型(第 5 部分)
- C# 中的 == 运算符和 String 类(第 6 部分)
- C# 中的 == 运算符和值类型(第 7 部分)
- C# 中的 == 运算符与继承和泛型问题 (第 8 部分)
- 在 C# 中为值类型实现相等性 (第 9 部分)
- 在 C# 中为引用类型实现相等性 - 第十部分
值类型的相等运算符
我们已经了解到相等运算符对于原始类型和引用类型的作用。有一个我们还没有测试过的情况,那就是非原始值类型会发生什么。这次,我们将重点关注值类型。
我们将使用之前使用过的相同示例,因此我们将声明一个 Person
类型为 struct
,并且我们将比较两个实例,看看它们是否相等,而不是使用我们之前做过的 Object.Equals
方法,我们知道它执行值比较,这非常低效,因为对于值类型,它会使用反射来迭代字段并检查每个字段的相等性,但相反,我们将使用 == operator
来比较两个 Person
类型
对象。
Person
类型定义如下
public struct Person
{
public string Name { get; set; }
public Person(string name)
{
Name = name;
}
public override string ToString()
{
return Name;
}
}
如果我们将在 Main
中编写以下代码并构建项目,我们会看到什么
class Program
{
static void Main(String[] args)
{
Person p1 = new Person("Ehsan Sajjad");
Person p2 = new Person("Ehsan Sajjad");
Console.WriteLine(p1.Equals(p2));
Console.WriteLine(p1 == p2);
Console.ReadKey();
}
}
当我们尝试构建上述程序时,第一行我们使用 Equals
方法比较 p1
和 p2
将没有问题,但生成在第 2 行使用 ==
运算符的代码会失败,并出现以下错误消息
因此,上述内容使我们清楚了一点:相等运算符对于非原始值类型没有任何作用,要使用相等运算符对非原始值类型,我们需要为该类型提供运算符重载。
让我们修改上面的示例,为 Person struct
添加相等运算符重载,我们现在将指定 ==
运算符对于比较的两个 Person
对象应该做什么,如果我们想提供重载 ==
运算符的实现,使其在用于两个 Person
类型对象时执行我们指定的操作,则语法如下
public static bool operator ==(Person p1, Person p2)
{
}
在 Person struct
中添加上述代码后,我们将能够编译 Program
的 Main
方法中的代码,但请注意,这仍然无法编译,因为根据签名,重载的返回类型是 bool
,但我们没有返回任何内容,这只是为了说明如何为用户定义的 Value Type 编写 ==
运算符重载实现。
我们在之前的某个帖子中看到 String
重载了相等运算符,以确保它与 Equals
方法执行相同的操作,因此,每当您定义新类型时,请确保它对方法和运算符执行相同的操作,如果我们提供其中之一,通常是件好事。以下是一个代码示例,可以帮助我们理解为什么这是一个好习惯
class Program
{
static void Main(string[] args)
{
Tuple<int int,int> tuple1 = Tuple.Create(1, 2);
Tuple<int int,int> tuple2 = Tuple.Create(1, 2);
Console.WriteLine(ReferenceEquals(tuple1, tuple2));
Console.WriteLine(tuple1.Equals(tuple2));
Console.WriteLine(tuple1 == tuple2);
Console.Read();
}
}
您可以看到,我们正在实例化两个包含相同值的元组,即 1
和 2
,Tuple
是一个泛型类,属于微软提供的 Framework Class Libraries,它只是提供了一种将几个值组合到一个对象中的方法。Tuple.Create(1, 2);
是实例化新元组的一种更简洁的方式,可以节省开发人员在代码中显式编写泛型类型。
现在我们正在比较元组以查看它们是否相等,Tuple
是一个引用类型,所以我们正在使用 ReferenceEquals
检查来确认我们处理的是两个不同的实例,接下来我们正在比较它们是否相等使用 Equals
方法,最后我们使用相等运算符(也称为 ==
运算符)进行比较。让我们运行这个程序,下面是该程序的输出
对某些人来说,结果可能令人惊讶,我们可以看到 ReferenceEquals
返回了 false
,这意味着它们都是不同的实例,但是 == operator
和 Equals
方法返回了相反的结果,== operator
说两者不相等,而 Equals
方法说它们相等。
实际上发生的是 Tuple
重载了 Equals
方法,使其能够检查对象的价值相等性。微软认为,当您处理一个目的只是封装几个字段的类型时,这可能就是您想要的相等性含义,因此,由于两个 Tuple 实例具有相同的值,Equals
方法会说它们是相等的,但 Microsoft
没有提供 == operator
的重载,这意味着 == operator
只是做了它应该做的事情,并且对于不提供重载的引用类型将始终执行此操作,并检查引用相等性,在这种情况下返回 False
,因为它们是不同的实例。
我敢肯定这会让您感到困惑,当然,这种行为很令人困惑,在我深入研究它的时候,它也让我感到困惑。几乎没有人会期望这种行为,强烈建议不要将此类行为添加到我们定义的任何类型中。
如果您 override
了相等性,那么最好提供 == operator
重载,以确保方法和运算符始终给出相同的结果,如果您实现了 IEquatable<T>
接口,那么也应该为该接口做同样的事情。
== 运算符和 Object.Equals 方法的比较
最后,让我们快速看看 ==
运算符和 Equals
方法在行为方面有何不同
- 对于原始类型,
例如
int
,float
,long
,bool
等,== operator
和Object.Equals
方法都会比较值,也就是说,1
等于1
,但1
不等于0
- 对于大多数
Reference Types
,== operator
和Object.Equals
方法默认都会比较引用,您可以将==
运算符重载或Object.Equals
方法重写来修改此行为,但如果您希望两者的行为保持一致,并且不想让其他开发人员和您自己感到意外,那么您必须同时执行这两项操作(重载==
运算符并重写Equals
方法)。 - 对于非原始值类型,
Object.Equals
方法将使用反射进行值相等性比较,这很慢,这当然是重写后的行为,但相等运算符默认情况下不适用于值类型,除非您为该类型重载==
运算符,正如我们在上面的示例中看到的。 - 还有一个细微的差别是,对于引用类型,如果第一个参数是
null
,则virtual Equals
方法将无法工作,但这微不足道,作为一种解决方法,可以使用static Equals
方法,该方法将要比较的两个对象都作为参数。
摘要
因此,在以上所有观点和讨论之后,我们可以得出结论,很多时候,运算符和方法在实践中会给出相同的结果,但由于运算符的语法非常方便,开发人员大多数时候更喜欢运算符。在下一篇文章中,我们将讨论在哪些情况下 ==
运算符可能不是首选,而是 Equals
方法更可取。