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

引言

最近一个朋友正在找工作,他说在笔试题中遇到Equals和==有什么区别的题,当时跟他说如果是值类型的,它们没有区别,如果是引用类型的有区别,但string类型除外。为了证实自己的说法,也研究了一下,以免误导别人,这里将研究结果总结一下,如果我有什么地方说的不对的地方,望指出。

相等性

在定义类或结构时,您将决定为类型创建值相等性(或等效性)的自定义定义是否有意义。 通常,当类型的对象预期要添加到某类集合时,或者当这些对象主要用于存储一组字段或属性时,您将实现值相等性。 您可以基于类型中所有字段和属性的比较来定义值相等性,也可以基于子集进行定义。 但在任何一种情况下,类和结构中的实现均应遵循五个等效性保证条件:

1.x.Equals(x) 返回 true. 。这称为自反属性。
2.x.Equals(y) 返回与 Equals(x) 相同的值。 这称为对称属性。
3.如果 (x.Equals(y) && y.Equals(z)) 返回 true,则 x.Equals(z) 返回 true。 这称为可传递属性。
4.只要不修改 x 和 y 所引用的对象,x.Equals(y) 的后续调用就返回相同的值。
5.x.Equals(null) 返回 false。 但是,null.Equals(null) 会引发异常;它不遵循上面的第二条规则。

您定义的任何结构已经具有它从 Object.Equals(Object) 方法的 System.ValueType 重写中继承的默认值相等性实现。 此实现使用反射来检查类型中的所有公共和非公共字段以及属性。 尽管此实现可生成正确的结果,但与您专门为类型编写的自定义实现相比,它的速度相对较慢。

类和结构的值相等性的实现详细信息不同。 但是,类和结构都需要相同的基础步骤来实现相等性:

重写 Object.Equals(Object)虚方法。 大多数情况下,您的 bool Equals( object obj ) 实现应只调入作为 System.IEquatable<T> 接口的实现的类型特定 Equals 方法。 (请参见步骤 2。)

通过提供类型特定的 Equals 方法实现 System.IEquatable<T> 接口。 实际的等效性比较将在此接口中执行。 例如,您可能决定通过仅比较类型中的一两个字段来定义相等性。 不要从 Equals 中引发异常。 仅适用于类:此方法应仅检查类中声明的字段。 它应调用 base.Equals 来检查基类中的字段。 (如果类型直接从 Object 中继承,则不要这样做,因为 Object.Equals(Object) 的 Object 实现会执行引用相等性检查。)

可选,但建议这样做:重载 == 和 != 运算符。

重写 Object.GetHashCode,使具有值相等性的两个对象生成相同的哈希代码。

可选:若要支持“大于”或“小于”定义,请为类型实现 IComparable<T> 接口,并同时重载 <= 和 >= 运算符。

——MSDN(http://msdn.microsoft.com/zh-cn/library/dd183755.aspx)这里将msdn的说法贴在此处,方便查看。

值类型

这里就以int类型的为代表进行分析,在分析之前先复习一下什么是重载?重载:简单的说就是一个类中的方法与另一个方法同名,但是参数列表个数或者类型或者返回值类型不同,则这两个方法构成重载。那么重载方法的调用规则是什么?那么先看下面的一段测试代码:

代码如下:

namespace Wolfy.EqualsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            int a =1, b = 1;
            Console.WriteLine(Add(a,b));
            Console.Read();
        }
        static int Add(object a, object b)
        {
            Console.WriteLine("调用了object类型参数列表的方法:");
            return (int)a + (int)b;
        }
        static int Add(int a, float b)
        {
            Console.WriteLine("调用了int,float类型参数列表的方法:");
            return a + (int)b;
        }
        static int Add(int a, int b)
        {
            Console.WriteLine("调用了int类型参数列表的方法:");
            return a + b;
        }
    }
}

测试结果:

说明根据传入实参的类型,优先匹配最相近的形参列表的方法。

那么我们将Add(int a ,int b)这个方法注释掉,那么会调用哪个方法?

为什么花费那么多口舌说明上面的问题,那么现在看一下Int32反编译的代码:

Int32有一个自己的Equals方法,有一个重写的Equals方法,如果两个int类型的值进行比较,Equals和==是一样的,因为它优先调用了下面的Equals方法,如果是下面的代码,则会选择重写的Equals方法。

代码如下:

static void Main(string[] args)
        {
            int a = 1;
            object b = 1;
            Console.WriteLine(a.Equals(b));
            Console.Read();
        }

可见,对于值类型的Equals和==是一样的。

引用类型

在类(引用类型)上,两种 Object.Equals(Object) 方法的默认实现均执行引用相等性比较,而不是值相等性检查。 当实施者重写虚方法时,目的是为了为其指定值相等性语义。
即使类不重载 == 和 != 运算符,也可以将这些运算符与类一起使用。 但是,默认行为是执行引用相等性检查。 在类中,如果您重载 Equals 方法,则应重载 == 和 != 运算符,但这并不是必需的。

——MSDN

测试代码:

代码如下:

static void Main(string[] args)
        {

Person p1 = new Person() { Name = "wolfy" };
            Person p2 = new Person() { Name = "wolfy" };
            Person p3 = p2;
            bool r1 = p1 == p2;
            bool r2 = p1.Equals(p2);
            bool r3 = p2 == p3;
            bool r4 = p2.Equals(p3);
            bool r5 = object.ReferenceEquals(p1, p2);
            bool r6 = object.Equals(p1,p2);
            bool r7 = object.ReferenceEquals(p2, p3);
            Console.WriteLine("==\t"+r1);
            Console.WriteLine("Equals\t"+r2);
            Console.WriteLine("p3=p2\t"+r3);
            Console.WriteLine("p2.Equals(p3)\t"+r4);
            Console.WriteLine("object.ReferenceEquals\t" + r5);
            Console.WriteLine("object.Equals(p1,p2)\t" + r6);
            Console.WriteLine("object.ReferenceEquals(p2, p3)\t" + r7);
            Console.Read();
        }

结果:

顺便反编译一下Equals和ReferenceEquals方法,看看他们的实现如何?

代码如下:

// object
 [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
 public static bool Equals(object objA, object objB)
 {
     return objA == objB || (objA != null && objB != null && objA.Equals(objB));
 }

// object
[__DynamicallyInvokable, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
public static bool ReferenceEquals(object objA, object objB)
{
    return objA == objB;
}

通过上面的代码,我们可以得出这样的结论,引用类型中Equals和ReferenceEquals的行为是相同的,==与ReferenceEquals的行为也相同,但string除外。

对特殊应用类型string的相等性,遵循值类型的相等性。string类型的反编译后的Equals方法和==代码如下:

代码如下:

public override bool Equals(object obj)
        {
            if (this == null)
            {
                throw new NullReferenceException();
            }
            string text = obj as string;
            return text != null && (object.ReferenceEquals(this, obj) || (this.Length == text.Length && string.EqualsHelper(this, text)));
        }
        [__DynamicallyInvokable, ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail), TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
        public bool Equals(string value)
        {
            if (this == null)
            {
                throw new NullReferenceException();
            }
            return value != null && (object.ReferenceEquals(this, value) || (this.Length == value.Length && string.EqualsHelper(this, value)));
        }
        [__DynamicallyInvokable, SecuritySafeCritical]
        public bool Equals(string value, StringComparison comparisonType)
        {
            if (comparisonType < StringComparison.CurrentCulture || comparisonType > StringComparison.OrdinalIgnoreCase)
            {
                throw new ArgumentException(Environment.GetResourceString("NotSupported_StringComparison"), "comparisonType");
            }
            if (this == value)
            {
                return true;
            }
            if (value == null)
            {
                return false;
            }
            switch (comparisonType)
            {
            case StringComparison.CurrentCulture:
                return CultureInfo.CurrentCulture.CompareInfo.Compare(this, value, CompareOptions.None) == 0;
            case StringComparison.CurrentCultureIgnoreCase:
                return CultureInfo.CurrentCulture.CompareInfo.Compare(this, value, CompareOptions.IgnoreCase) == 0;
            case StringComparison.InvariantCulture:
                return CultureInfo.InvariantCulture.CompareInfo.Compare(this, value, CompareOptions.None) == 0;
            case StringComparison.InvariantCultureIgnoreCase:
                return CultureInfo.InvariantCulture.CompareInfo.Compare(this, value, CompareOptions.IgnoreCase) == 0;
            case StringComparison.Ordinal:
                return this.Length == value.Length && string.EqualsHelper(this, value);
            case StringComparison.OrdinalIgnoreCase:
                if (this.Length != value.Length)
                {
                    return false;
                }
                if (this.IsAscii() && value.IsAscii())
                {
                    return string.CompareOrdinalIgnoreCaseHelper(this, value) == 0;
                }
                return TextInfo.CompareOrdinalIgnoreCase(this, value) == 0;
            default:
                throw new ArgumentException(Environment.GetResourceString("NotSupported_StringComparison"), "comparisonType");
            }
        }
        [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
        public static bool Equals(string a, string b)
        {
            return a == b || (a != null && b != null && a.Length == b.Length && string.EqualsHelper(a, b));
        }
        [__DynamicallyInvokable, SecuritySafeCritical]
        public static bool Equals(string a, string b, StringComparison comparisonType)
        {
            if (comparisonType < StringComparison.CurrentCulture || comparisonType > StringComparison.OrdinalIgnoreCase)
            {
                throw new ArgumentException(Environment.GetResourceString("NotSupported_StringComparison"), "comparisonType");
            }
            if (a == b)
            {
                return true;
            }
            if (a == null || b == null)
            {
                return false;
            }
            switch (comparisonType)
            {
            case StringComparison.CurrentCulture:
                return CultureInfo.CurrentCulture.CompareInfo.Compare(a, b, CompareOptions.None) == 0;
            case StringComparison.CurrentCultureIgnoreCase:
                return CultureInfo.CurrentCulture.CompareInfo.Compare(a, b, CompareOptions.IgnoreCase) == 0;
            case StringComparison.InvariantCulture:
                return CultureInfo.InvariantCulture.CompareInfo.Compare(a, b, CompareOptions.None) == 0;
            case StringComparison.InvariantCultureIgnoreCase:
                return CultureInfo.InvariantCulture.CompareInfo.Compare(a, b, CompareOptions.IgnoreCase) == 0;
            case StringComparison.Ordinal:
                return a.Length == b.Length && string.EqualsHelper(a, b);
            case StringComparison.OrdinalIgnoreCase:
                if (a.Length != b.Length)
                {
                    return false;
                }
                if (a.IsAscii() && b.IsAscii())
                {
                    return string.CompareOrdinalIgnoreCaseHelper(a, b) == 0;
                }
                return TextInfo.CompareOrdinalIgnoreCase(a, b) == 0;
            default:
                throw new ArgumentException(Environment.GetResourceString("NotSupported_StringComparison"), "comparisonType");
            }
        }
        [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
        public static bool operator ==(string a, string b)
        {
            return string.Equals(a, b);
        }
        [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
        public static bool operator !=(string a, string b)
        {
            return !string.Equals(a, b);
        }

从上面的代码可以看出string类型的Equals和==是一样的。

代码如下:

public static bool operator ==(string a, string b)
        {
            return string.Equals(a, b);
        }
[__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
        public static bool operator !=(string a, string b)
        {
            return !string.Equals(a, b);
        }

总结

值类型具有它从 Object.Equals(Object) 方法的 System.ValueType 重写中继承的默认值相等性实现。特殊的引用类型string类型,因为重写了Equals和==方法,所以string类型的Equals和==与值类型的相等性一样。对于其他的引用类型此时的Equals和==与引用相等ReferenceEquals的行为相同。

以上是由一个同事的问题引起,中间也查了很多资料,发现这篇文章在草稿箱中躺了很久了,今天突然看到就拿出来晒晒。中间修修改改,总尝试着用哪种方式来说明这个老生常谈的问题更好些。以上有些观点,纯属个人见解,如果你有更好的理解方式,不妨分享一下。如果对你有所帮助,不妨点一下推荐,让更多的人看到,说说自己对Equals和==的理解。

(0)

相关推荐

  • 浅析C# 中的类型系统(值类型和引用类型)

    今天要写的东西都是书中一些概念性的东西,就当抄笔记,以提问对话的方式将其写出来吧,说不定以后面试能有点谈资~~~ Q1.C#1系统类型包含哪三点特性? A1.C#1类型系统是静态的.显式的和安全的. Q2.为什么称为静态类型? A2.静态类型是用来描述表达式在编译时的类型,当声明一个类型的变量时,不能将变量指向其它类型的对象. Q3.显式类型和隐式类型的区别? A3.显式类型和隐式类型只有静态类型中的语言才有意义.显式类型需要显式声明一个变量的类型,而隐式类型则将类型的判断责任推给编译器,但是在

  • c# 引用类型与值类型的区别详解

    解析:CLR支持两种类型:值类型和引用类型.用Jeffrey Richter(<CLR via C#>作者)的话来说,"不理解引用类型和值类型区别的程序员将会把代码引入诡异的陷阱和诸多性能问题".这就要求我们正确理解和使用值类型和引用类型.值类型包括C#的基本类型(用关键字int.char.float等来声明),结构(用struct关键字声明的类型),枚举(用enum关键字声明的类型):而引用类型包括类(用class关键字声明的类型)和委托(用delegate关键字声明的特

  • 一看就懂:图解C#中的值类型、引用类型、栈、堆、ref、out

    C# 的类型系统可分为两种类型,一是值类型,一是引用类型,这个每个C#程序员都了解.还有托管堆,栈,ref,out等等概念也是每个C#程序员都会接触到的概念,也是C#程序员面试经常考到的知识,随便搜搜也有无数的文章讲解相关的概念,貌似没写一篇值类型,引用类型相关博客的不是好的C#程序员.我也凑个热闹,试图彻底讲明白相关的概念. 程序执行的原理 要彻底搞明白那一堆概念及其它们之间的关系似乎并不是一件容易的事,这是因为大部分C#程序员并不了解托管堆(简称"堆")和线程栈(简称"栈

  • C#中值类型和引用类型的区别深度分析

    本文通俗易懂的分析了C#中值类型和引用类型的区别.分享给大家供大家参考.具体分析如下: 似乎"值类型和引用类型的区别"是今年面试的流行趋势,我已然是连续三次(目前总共也就三次)面试第一个问题就遇到这个了,这是多大的概率啊,100%,哈哈,我该买彩票去! 言归正传,咱还是先来探讨探讨这二者之间有什么区别吧.记得有一次电话面试中,我直接跟面试官说:"值类型是现金,引用类型是存折",后来想想当时说这话虽是有点儿冲动地脱口而出,但也没什么不妥.我这人不善于背理论的教条,喜欢

  • C#值类型和引用类型的深入理解

    从概念上看,值类型直接存储其值,而引用类型存储对其值的引用.这两种类型存储在内存的不同地方.在C#中,我们必须在设计类型的时候就决定类型实例的行为.这种决定非常重要,用<CLR via C#>作者Jeffrey Richter的话来 说,"不理解引用类型和值类型区别的程序员将会给代码引入诡异的bug和性能问题(I believe that a developer who misunderstands the difference between reference types and

  • c#值类型和引用类型使用示例

    在刚参加工作面试时,我们经常会遇到有关值类型和引用类型的问题,你回答的怎么样直接影响你在别人心目中的印象,你回答的不好说明你对C#没有深入的了解学习,今天我带大家回顾下C#中的引用类型和值类型.CLR支持两种类型:引用类型和值类型.虽然FCL中大多数类型都是引用类型但程序中用的最多的还是值类型.引用类型总是从托管堆上分配的,C#的new操作会返回对象的内存地址--也就是指向对象的数据的内存地址.设想假设每次使用一个Int32值时,都进行一次内存分配,性能会受到多大的影响,为了提升简单.常用的类型

  • C#预定义数据类型之值类型和引用类型介绍

    C#的预定义数据类型包括两种,一种是值类型,一种是引用类型.值类型的变量在内存中是存储在堆栈中的,字面上理解就是直接保存其值,如声明一个属于值类型的整型变量,并给它赋予另一个整型变量的值,则在内存中事先复制一个整型变量的值,然后将其赋予刚刚声明的整型变量,这时内存中就会有两个整型值.引用类型的变量在内存中是直接保存其值的引用.C#语言中大多数复杂数据类型都是引用类型. 先来一个值类型的常量的例子 根据用户输入圆的半径,求圆的面积的代码: using System; using System.Co

  • C#中值类型和引用类型解析

    在C#中,值类型和引用类型是相当重要的两个概念,必须在设计类型的时候就决定类型实例的行为.如果在编写代码时不能理解引用类型和值类型的区别,那么将会给代码带来不必要的异常.很多人就是因为没有弄清楚这两个概念从而在编程过程中遇到了很多问题,在这里博主浅谈对值类型和引用类型的认识. 首先从概念上看,值类型直接存储其值,而引用类型存储对其值的引用.从而这两种类型存储在内存的不同地方. 其次从内存空间上看,值类型是在栈中操作,而引用类型则在堆中分配存储单元. 栈在编译的时候就分配好内存空间,在代码中有栈的

  • C#中的DateTime是值类型还是引用类型

    近期遇到了DateTime到底是值类型还是引用类型的疑惑,顺势较深入地了解一下DateTime相关的内容 结论:DateTime是值类型,因为DateTime是结构体,而结构体继承自System.ValueType,属于值类型 一.DateTime是值类型还是引用类型的探索 二.了解DateTime结构体 三.DateTime.Now和DateTime.UtcNow是怎么计算出来的 一.DateTime是值类型还是引用类型的探索 1. 先编写测试代码 将dateTime1赋值给dateTime2

  • 浅谈C#中的值类型和引用类型

    一.基本概念 C#只有两种数据类型:值类型和引用类型 值类型在线程栈分配空间,引用类型在托管堆分配空间 值类型转为引用类型称成为装箱,引用类型转为值类型称为拆箱 以下是值类型和引用类型对照表 从上图可以简单看出:string,Object,数组,class是引用类型,简单类型,枚举,结构是值类型. 二.代码展示 定义一个类和结构调用赋值 内存分配情况如下图: 从这张图可以看出,class实例化出来的对象,指向了内存堆中分配的空间:truct实例化出来的对象,是在内存栈中分配. 修改代码如下: 内

随机推荐