C#中的==运算符用法讲解

目录
  • ==运算符与基元类型
  • ==运算符与引用类型
  • ==运算符与String类型
  • ==运算符与值类型
  • ==运算符与泛型
  • 总结

==运算符与基元类型

我们分别用两种方式比较两个整数,第一个使用的是Equals(int)方法,每二个使用的是==运算符:

class Program
{
    static void Main(String[] args)
    {
        int num1 = 5;
        int num2 = 5;

        Console.WriteLine(num1.Equals(num2));
        Console.WriteLine(num1 == num2);
    }
}

运行上面的示例,两个语句出的结果均为true。我们通过ildasm.exe工具进行反编译,查看IL代码,了解底层是如何执行的。

如果您以前从来没有接触过IL指令,不过没关系,在这里您不需要理解所有的指令,我们只是想了解这两个比较方式的差异。

您可以看到这样一行代码:

IL_0008:  call       instance bool [mscorlib]System.Int32::Equals(int32)

在这里调用的是int类型Equals(Int32)方法(该方法是IEquatable<Int>接口的实现)。

现在再来看看使用==运算符比较生成的IL指令:

IL_0015:  ceq

您可以看到,==运行符使用的是ceq指令,它是使用CPU寄存器来比较两个值。C#==运算符底层机制是使用ceq指令对基元类型进行比较,而不是调用Equals方法。

==运算符与引用类型

修改上面的示例代码,将int类型改为引用类型,编译后通过ildasm.exe工具反编译查看IL代码。

class Program
{
    static void Main(String[] args)
    {
        Person p1 = new Person();
        p1.Name = "Person1";

        Person p2 = new Person();
        p2.Name = "Person1";

        Console.WriteLine(p1.Equals(p2));
        Console.WriteLine(p1 == p2);
    }
}

上述C#代码的IL代码如下所示:

我们看到p1.Equals(p2)代码,它是通过调用Object.Equals(Object)虚方法来比较相等,这是在意料之中的事情;现在我们来看==运算符生成的IL代码,与基元类型一致,使用的也是ceq指令。

==运算符与String类型

接来下来看String类型的例子:

class Program
{
    static void Main(String[] args)
    {
        string s1 = "Sweet";
        string s2 = String.Copy(s1);

        Console.WriteLine(ReferenceEquals(s1, s2));
        Console.WriteLine(s1 == s2);
        Console.WriteLine(s1.Equals(s2));
    }
}

上面的代码与我们以前看过的非常相似,但是这次我们使用String类型的变量。我们建一个字符串,并付给s1变量,在下一行代码我们创建这个字符串的副本,并付给另一个变量名称s2

运行上面的代码,在控制台输出的结果如下:

您可以看到ReferenceEquals返回false,这意味着这两个变量是不同的实例,但是==运算符和Equals方法返回的均是true。在String类型中,==运算符执行的结果与Equals执行的结果一样。

同样我们使用过ildasm.exe工具反编译查看生成IL代码。

在这里我们没有看到ceq指令,对String类型使用==运算符判断相等时,调用的是一个op_equality(string,string)的新方法,该方法需要两个String类型的参数,那么它到底是什么呢?

答案是String类型提供了==运算符的重载。在C#中,当我们定义一个类型时,我们可以重载该类型的==运算符;例如,对于以前的例子中我们实现的Person类,如果我们为它重载==运算符,大致的代码如下:

public class Person
{

    public string Name { get; set; }

    public static bool operator ==(Person p1, Person p2)
    {
        // 注意这里不能使用==,否则会导致StackOverflowException
        if (ReferenceEquals(p1, p2))
            return true;

        if (ReferenceEquals(p1, null) || ReferenceEquals(p2, null))
            return false;

          return p1.Name == p2.Name;
    }

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

上面的代码很简单,我们实现了==运算符重载,这是一个静态方法,但这里要注意的是,方法的名称是perator==,与静态方法的相似性;事实上,它们会被由编译器成一个名称为op_Equality()的特殊静态方法。

为了使用事情更加清楚,我们查看微软实现的String类型。

在上面的截图中,我们可以看到,有两个运算符的重载,一个用于相等,另一个是不等式运算符,其运算方式完全相同,但是否定等于运算符输出。需要注意的一点是,如果您想重载一个类型的==运行符的实现,那么您还需要重载!=操作符的实现,否则编译会报错。

==运算符与值类型

在演示值类型的示例前,我们先将Person类型从引用类型改为值类型,Person定义如下:

public struct Person
{
    public string Name { get; set; }

    public Person(string name)
    {
        Name = name;
    }

    public override string ToString()
    {

        return Name;
    }
}

我们将示例代码改为如下:

class Program
 {
     static void Main(String[] args)
     {
         Person p1 = new Person("Person1");
         Person p2 = new Person("Person2");

         Console.WriteLine(p1.Equals(p2));
         Console.WriteLine(p1 == p2);
     }
 }

当我们在尝试编译上述代码时,VS将提示如下错误:

根据错误提示,我们需要实现Person结构体的==运算符重载,重载的语句如下(忽略具体的逻辑):

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

当添加上面代码后,重新编译程序,通过ildasm.exe工具反编译查看IL代码,发现值类型==运算符调用也是op_Equality方法。

关于值类型,我们还需要说明一个问题,在不重写Equals(object)方法时,该方法实现的原理是通过反射遍历所有字段并检查每个字段的相等性,关于这一点,我们不演示;对于值类型,最好重写该方法。

==运算符与泛型

我们编写另一段示例代码,声明两个String类型变量,通过4种不同的方式比较运算:

public class Program
{
    public static void Main(string[] args)
    {
        string str = "Sweet";
        string str1 = string.Copy(str);

        Console.WriteLine(ReferenceEquals(str, str1));
        Console.WriteLine(str.Equals(str1));
        Console.WriteLine(str == str1);
        Console.WriteLine(object.Equals(str, str1));
    }
}

输出的结果如下:

首先,我们使用ReferenceEquals方法判断两个String变量都引用相同,接下来我们再使用实例方法Equals(string),在第三行,我们使用==运算符,最后,我们使用静态方法Object.quals(object,object)(该方法最终调用的是String类型重写的Object.Equals(object)方法)。我们得到结论是:

ReferenceEquals方法返回false,因为它们不是同一个对象的引用;String类型的Equals(string)方法返回也是true,因为两个String类型是相同的(即相同的序列或字符);==运算符也将返回true,因为这两个String类型的值相同的;虚方法Object.Equals也将返回true,这是因为在String类型重写了方法,判断的是String是否值相同。

现在我们来修改一下这个代码,将String类型改为Object类型:

public class Program
{
    public static void Main(string[] args)
    {
        object str = "Sweet";
        object str1 = string.Copy((string)str);

        Console.WriteLine(ReferenceEquals(str, str1));
        Console.WriteLine(str.Equals(str1));
        Console.WriteLine(str == str1);
        Console.WriteLine(object.Equals(str, str1));
    }
}

运行的结果如下:

第三种方法返回的结果与修改之前不一致,==运算符返回的结果是false,这是为什么呢?

这是因为==运算符实际上是一个静态的方法,对一非虚方法,在编译时就已经决定用调用的是哪一个方法。在上面的例子中,引用类型使用的是ceq指令,而String类型调用是静态的op_Equality方法;这两个实例不是同一个对象的引用,所以ceq指令执行后的结果是false

再来说一下==运算符与泛型的问题,我们创建一个简单的方法,通过泛型方法判断两个泛型参数是否相等并在控制台上打印出结果:

static void Equals<T>(T a, T b)
{
    Console.WriteLine(a == b);
}

但是当我们编译这段代码时,VS提示如下错误:

上面显示的错误很简单,不能使用==运算符比较两个泛型T。因为T可以是任何类型,它可以是引用类型、值类型,不能提供==运算符的具体实现。

如果像下面这样修改一下代码:

static void Equals<T>(T a, T b) where T : class
{
    Console.WriteLine(a == b);
}

当我们将泛型类型T改为引用类型,能成功编译;修改Main方法中的代码,创建两个相同的String类型,和以前的例子一样:

public class Program
{
    static void Main(string[] args)
    {
        string str = "Sweet";
        string str1 = string.Copy(str);

        Equals(str, str1);
    }

    static void Equals<T>(T a, T b) where T : class
    {
        Console.WriteLine(a == b);
    }
}

输出的结果如下:

结果与您预期的结果不一样吧,我们期待的结果是true,输出的结果是false。不过仔细思考一下,也许会找到答案,因为泛型的约束是引用类型,==运算符对于引用类型使用的是引用相等,IL代码可以证明这一点:

如果我们泛型方法中的==运算符改为使用Equals方法,代码如下:

static void Equals<T>(T a, T b)
{
    Console.WriteLine(object.Equals(a, b));
}

我们改用Equals,也可以去掉class约束;如果我们再次运行代码,控制台打印的结果与我们预期的一致,这是因为调用是虚方法object.Equals(object)重写之后的实现。

但是其它的问题来了,如果对于值类型,这里就会产生装箱,有没有解决的办法呢?关于这一点,我们直接给出答案,有时间专门来讨论这个问题。

将比较的值类型实现IEquatable<T>接口,并将比较的代码改为如下,这样可以避免装箱(关于这一点,可以参考博客:https://www.jb51.net/article/238142.htm):

static void Equals<T>(T a, T b)
{
    Console.WriteLine(EqualityComparer<T>.Default.Equals(a, b));
}

总结

对于基元类型==运算符的底层机制使用的是ceq指令,通过CPU寄存器进行比较;

对于引用类型==运算符,它也使用的ceq指令来比较内存地址;

对于重载==运算符的类型,实际上调用的是op_equality这个特殊的方法;

尽量保证==操作符重载和Object.Equals(Object)虚方法的写返回的是相同的结果;

对于值类型,Equals方法默认是通过反射遍历所有字段并检查每个字段的相等性,为了提高性能,我们需要重写该方法;

值类型默认情况下不能使用==运算符,需要实现==运算符的重载;

由于==运算符重载实现实际上是一个静态的方法,在泛型类或方法中使用时与实际的结果可能存在差别,使用Equals方法可以避免这个问题。

到此这篇关于C#中的==运算符用法讲解的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • C#中的Equals、RefrenceEquals和==的区别与联系

    C#中判断两个对象是否相等有Equals.RefrenceEquals和==三种,其中==为运算符,其它两个为方法,而Equals又有两种版本,一个是静态的,一个是虚拟的,虚拟的可以被实体类重写,静态的在方法体内也是调用虚拟的,如下: 复制代码 代码如下: public static bool Equals(object objA, object objB) { return ((objA == objB) || (((objA != null) && (objB != null)) &a

  • C#中的 == 和equals()区别浅析

    首先看看,如以下代码: int age = 25; short newAge = 25; Console.WriteLine(age == newAge); //true Console.WriteLine(newAge.Equals(age)); //false Console.ReadLine(); int和short为原始类型,但与"=="比较返回true,equals()比较返回false.为什么呢? 简而言之:"equals()"相比"= =&q

  • C#值类型、引用类型中的Equals和==的区别浅析

    引言 最近一个朋友正在找工作,他说在笔试题中遇到Equals和==有什么区别的题,当时跟他说如果是值类型的,它们没有区别,如果是引用类型的有区别,但string类型除外.为了证实自己的说法,也研究了一下,以免误导别人,这里将研究结果总结一下,如果我有什么地方说的不对的地方,望指出. 相等性 在定义类或结构时,您将决定为类型创建值相等性(或等效性)的自定义定义是否有意义. 通常,当类型的对象预期要添加到某类集合时,或者当这些对象主要用于存储一组字段或属性时,您将实现值相等性. 您可以基于类型中所有

  • C#基础:Equals()与运算符==的区别分析

    对于值类型,如果对象的值相等,则相等运算符 (==) 返回 true,否则返回 false.对于string 以外的引用类型,如果两个对象引用同一个对象,则 == 返回 true.对于 string 类型,== 比较字符串的值.==操作比较的是两个变量的值是否相等.equals()方法比较的是两个对象的内容是否一致.equals也就是比较引用类型是否是对同一个对象的引用.对于值类型的比较,这里就不做描述了,下面讨论引用类型的比较:首先我们看一段程序 复制代码 代码如下: using System

  • C#中重载相等(==)运算符示例

    运算符重载一直是一个很诡异事情,因为在写代码的时候,不知道某个运算符有没有被重载过.在 C++ 里面,运算符重载可以写在类的外面,当 intellisense 不工作的时候,找到一个运算符的重载函数是一件相当头疼的事情.这个问题在 C# 中改善了不少,因为运算符重载一定要写在类内,而且 intellisense 很强大.不过另一个问题又产生了-- 先来看 C++ 中的"=="重载: struct A{ int x; int y; }; inline bool operator == (

  • 详解C#中==、Equals、ReferenceEquals的区别

    本文导读: C#中Equals , == , ReferenceEquals都可以用于判断两个对象的个体是不是相等,对于相同的基本值类型,==和Equals()比较结果是一样的:由于ReferenceEquals()是判断两个对象的引用是否相等,对于值类型,因为每次判断前都必须进行装箱操作,也就是每次都生成了一个临时的object,因而永远返回false. 一.== 运算符 1.静态相等符号,对应存在的!=,这个符号是一个可以重载的二元操作符,可以用于比较两个对象是否相等. 2.它会根据需要自动

  • C#中==(双等于号)与equals()区别详解

    这两种方式也是大家在日常编码工作当中用的比较多的判断方式.之前在使用的时候也没太关注两者在比较不同类型的时候存在哪些区别 今天就和大家一起深入了解一下其中区别 一.值类型比较判断 对于值类型来说 两者之间比较的都是"内容"是否相同,即值类型中的数值是否一样,很显然此时两者是划等号的,代码展示如下: #region 值类型判断 int i = 10; int j = 10; Console.WriteLine($"双等于号判断结果为:{(i == j)}"); Con

  • C#中的==运算符

    在这篇文章中,我们将介绍如下内容: ==运算符与基元类型 ==运算符与引用类型 ==运算符与String类型 ==运算符与值类型 ==运算符与泛型 ==运算符与基元类型 我们分别用两种方式比较两个整数,第一个使用的是Equals(int)方法,每二个使用的是==运算符: class Program { static void Main(String[] args) { int num1 = 5; int num2 = 5; Console.WriteLine(num1.Equals(num2))

  • C#中的==运算符用法讲解

    目录 ==运算符与基元类型 ==运算符与引用类型 ==运算符与String类型 ==运算符与值类型 ==运算符与泛型 总结 ==运算符与基元类型 我们分别用两种方式比较两个整数,第一个使用的是Equals(int)方法,每二个使用的是==运算符: class Program { static void Main(String[] args) { int num1 = 5; int num2 = 5; Console.WriteLine(num1.Equals(num2)); Console.Wr

  • linux 中vim的用法讲解

    Vim 是 Linux 系统上的最著名的文本/代码编辑器,也是早年的 Vi 编辑器的加强版,而 gVim 则是其 Windows 版.它的最大特色是完全使用键盘命令进行编辑,脱离了鼠标操作虽然使得入门变得困难,但上手之后键盘流的各种巧妙组合操作却能带来极为大幅的效率提升. vim的命令的一些格式 1:vim xxx 直接打开一个xxx命名的vim文件,如果没有的话直接创建一个新的.默认光标定义到第一行 2:vim + xxx 打开光标并定义到最后一行 3:vim +num xxx 打开光标定义到

  • javascript中in运算符用法分析

    本文实例讲述了javascript中in运算符用法.分享给大家供大家参考.具体分析如下: in运算符希望它的左操作数是一个字符串或可以转换为字符串,希望他的右操作数是一个对象:如果右侧对象拥有一个名为左操作数值的属性名,那么表达式返回true: var point = {x:1,y:1}; "x" in point //true "z" in point //false "toString" in point //true var ary = [

  • python中append函数用法讲解

    如果在做一个地区的统计工作,可以使用列表来帮助我们.输入汉字或者其他字符,比如"01代表汉族",那么在写民族的时候有下拉列表,就可以打01,就会自动识别为汉族.列表是用来大规模数据填报的时候使用,在python中,也有很多使用到列表的时候,那你知道如何在列表的末尾添加新的对象?今天,我们就来认识一下python中可以在列表末尾添加元素的append函数. 1.append()函数 用于在列表末尾添加新的对象. 2. 语法 list.append(obj) 3.参数 list:列表对象:

  • python中waitKey实例用法讲解

    1.说明 用于等待按钮.当用户按下按钮时,句子将被执行并获得返回值. 2.语法 retval=cv2.waitKey([delay]) Retval:表示返回值: Delay:键触发的时间,单位为ms. 3.实例 import cv2 lena=cv2.imread("D:\pmjcv\lena.bmp") cv2.namedWindow("lesson") cv2.imshow("lesson",lena) key=cv2.waitKey()

  • C#中Invoke的用法讲解

    C#中Invoke的用法() invoke和begininvoke 区别 一直对invoke和begininvoke的使用和概念比较混乱,这两天看了些资料,对这两个的用法和原理有了些新的认识和理解. 首先说下,invoke和begininvoke的使用有两种情况: 1. control中的invoke.begininvoke. 2. delegrate中的invoke.begininvoke. 这两种情况是不同的,我们这里要讲的是第1种.下面我们在来说下.NET中对invoke和begininv

  • Perl语法中Perl运算符用法指南

    Perl语法 Perl运算符简介 1.Perl运算符 1.1 算数运算符 Perl固然在变量与数据范例都和C大不不异,然而它的运算符和C险些不异,除了C的范例转换运算符type,指针引用运算符*ptr和布局成员选择符,其他C的运算符险些完全呈现在Perl里,Perl新增了几个新运算符,好比在字符处置方面. ◆Perl到今朝撑持的算数运算符有一些几种: +加法运算符-减法运算符*乘运算符/除运算符(只对浮点数运算)%取模运算符(只对整数运算) 下面给出Perl算数运算的一些实例: $x=2.5;$

  • Mybatis配置映射文件中parameterType的用法讲解

    目录 Mybatis配置映射文件parameterType用法 1.基本数据类型 2.复杂数据类型:包含java实体类,map Mybatis parameterType # $符号的注意事项 Mybatis配置映射文件parameterType用法 在mybatis映射接口的配置中,有select,insert,update,delete等元素都提到了parameterType的用法,parameterType为输入参数,在配置的时候,配置相应的输入参数类型即可.parameterType有基

  • Oracle中的SUM用法讲解

    Oracle中的SUM条件查询 1.按照区域编码分组查询区域编码.IPTV_NBR不为空的数量.ACC_NBR不为空的数量.所有用户数量 SELECT AREA_CODE, SUM ( CASE WHEN IPTV_NBR IS NULL or IPTV_NBR = '' THEN 0 ELSE 1 END ), SUM ( CASE WHEN ACC_NBR IS NULL or ACC_NBR = '' THEN 0 ELSE 1 END ), COUNT (*) FROM GAT_SQMS

  • vue中$nextTick的用法讲解

    vue是非常流行的框架,他结合了angular和react的优点,从而形成了一个轻量级的易上手的具有双向数据绑定特性的mvvm框架.本人比较喜欢用之.在我们用vue时,我们经常用到一个方法是this.$nextTick,相信你也用过.我常用的场景是在进行获取数据后,需要对新视图进行下一步操作或者其他操作时,发现获取不到dom.因为赋值操作只完成了数据模型的改变并没有完成视图更新.在这个时候我们需要用到本章介绍的函数. 虽然 Vue.js 通常鼓励开发人员沿着"数据驱动"的方式思考,避免

随机推荐