65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (13投票s)

2017年1月22日

CPOL

8分钟阅读

viewsIcon

29913

在本文中,您将了解非原始值类型的相等运算符 (==) 的行为。

背景

本文是对 .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== operatorObject.Equals 方法调用在后台的工作方式不同,可以通过检查生成的 IL 代码来验证。它还使用 ceq 指令来比较内存地址。

如果以上几点对您来说没有意义,最好从头开始阅读,以下是相关先前内容的链接

值类型的相等运算符

我们已经了解到相等运算符对于原始类型和引用类型的作用。有一个我们还没有测试过的情况,那就是非原始值类型会发生什么。这次,我们将重点关注值类型。

我们将使用之前使用过的相同示例,因此我们将声明一个 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 方法比较 p1p2 将没有问题,但生成在第 2 行使用 == 运算符的代码会失败,并出现以下错误消息

运算符‘==’不能应用于类型为‘Person’和‘Person’的操作数

因此,上述内容使我们清楚了一点:相等运算符对于非原始值类型没有任何作用,要使用相等运算符对非原始值类型,我们需要为该类型提供运算符重载。

让我们修改上面的示例,为 Person struct 添加相等运算符重载,我们现在将指定 == 运算符对于比较的两个 Person 对象应该做什么,如果我们想提供重载 == 运算符的实现,使其在用于两个 Person 类型对象时执行我们指定的操作,则语法如下

public static bool operator ==(Person p1, Person p2)
{
}

Person struct 中添加上述代码后,我们将能够编译 ProgramMain 方法中的代码,但请注意,这仍然无法编译,因为根据签名,重载的返回类型是 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();
     }
}

您可以看到,我们正在实例化两个包含相同值的元组,即 12Tuple 是一个泛型类,属于微软提供的 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 等,== operatorObject.Equals 方法都会比较值,也就是说,1 等于 1,但 1 不等于 0
  • 对于大多数 Reference Types== operatorObject.Equals 方法默认都会比较引用,您可以将 == 运算符重载或 Object.Equals 方法重写来修改此行为,但如果您希望两者的行为保持一致,并且不想让其他开发人员和您自己感到意外,那么您必须同时执行这两项操作(重载 == 运算符并重写 Equals 方法)。
  • 对于非原始值类型,Object.Equals 方法将使用反射进行值相等性比较,这很慢,这当然是重写后的行为,但相等运算符默认情况下不适用于值类型,除非您为该类型重载 == 运算符,正如我们在上面的示例中看到的。
  • 还有一个细微的差别是,对于引用类型,如果第一个参数是 null,则 virtual Equals 方法将无法工作,但这微不足道,作为一种解决方法,可以使用 static Equals 方法,该方法将要比较的两个对象都作为参数。

摘要

因此,在以上所有观点和讨论之后,我们可以得出结论,很多时候,运算符和方法在实践中会给出相同的结果,但由于运算符的语法非常方便,开发人员大多数时候更喜欢运算符。在下一篇文章中,我们将讨论在哪些情况下 == 运算符可能不是首选,而是 Equals 方法更可取。

© . All rights reserved.