《Effective C#》读书笔记——条目6:理解几个等同性判断之间的关系<C#语言习惯>

简介:

创建自定义的类型时(无论是类还是struct),应为类型定义”同等性“的含义。在C#中为我们提供了四种不同的函数来判断两个对象是否”相等“:

1 public static bool ReferenceEquals(object left, object right);
2 public static bool Equals(object left, object right);
3 public virtual bool Equals(object right);
4 public static bool operator ==(MyClass left, MyClass right);

引用相等和值相等

  C#允许我们创建两种类型:值类型和引用类型。如果两个引用类型的变量指向的是同一个对象,它们将被认为是“引用相等”。如果两个值类型的变量类型相同且包含同样的内容,它们被认为是“值相等”。这也正是同等性判断需要如此多方法的原因。

 

为什么不应该重新定义静态的ReferenceEquals()和Equals()方法

  对于前两个静态函数,我们永远都不应该去重新定义,因为它们已经很好的完成了它们的工作,且判断与运行时具体类型无关:判断两个不同变量的对象标志(object identity)是否相等。无论比较的是值类型还是引用类型静态的ReferenceEquals方法的判断依据都是对象标志,所以比较两个值类型永远返回false,即使是值类型和它本身比较也是,这是因为装箱的原因。当我们不知道两个变量的运行时类型时,可以使用静态的Equals方法来判断两个变量是否相等,同等判断是以来类型,所以静态的Equals通过委托其中一个类型来做的判断的,静态的Ojbect.Equals()方法实现如下:

复制代码
 1   public static new bool Equals(object left, object right)
 2         {
 3             //检查对象引用
 4             if (Object.ReferenceEquals(left, right))
 5                 return true;
 6             //是否为null
 7             if (Object.ReferenceEquals(left, null))
 8                 return false;
 9             //调用实例的Equals()方法
10             return left.Equals(right);
11         }
复制代码

我们可以看到静态的Equals()方法将判断的工作交给left参数的实例Equals()方法执行,所以它会使用left参数的类型中定义的规则来进行等同性判断。

 

什么情况下需要重写Equals()实例方法

  当Equals()实例方法的默认行为与我们的类型要求不一致时,自然需要覆写。该方法默认使用对象标志判断,即比较两个对象是否引用相等。

值类型(使用Struct关键字创建的类型):System.ValueType(所有值类型的基类)覆写了Object.Equals()方法:两个值类型变量类型相同,内容一致,两个变量才认为相等。由于ValueType是所有值类型的基类,为了提供正确的行为,必须能够在不知道对象运行时类型的情况下比较其派生类中的所有成员变量,这意味着要使用反射来实现。而反射又是非常损耗性能的。而等同性判断又是一个非常基础的功能,所以我们有必要(追求性能时)为自己的值类型提供一个更快的Equals()覆写版本。

引用类型:只有我们希望更改其预定义的语义时,才应该覆写Equals()方法。在.NET类库中许多类都是使用值语义而不是引用语义来做等同判断的,例如:如果两个string对象包含相同的内容就被认为相等;若两个DataRowView对象引用同一个DataRow,那么将被认为相等。

 

如何覆写Equals()实例方法

   覆写Equlas()实例方法是需要实现IEquatable<T>接口,该接口包含了一个方法Equals(Tother),实现了IEquatable<T>以为着你的类型支持类型安全的等同性比较。若你认为Equals()仅仅应该在比较的两边属于同一个类型时才返回true,那么IEquatable<T>将会让编译器帮你找到可能出现的种种类型相关的不相等情况。

下面是覆写System.Object.Equals()实例方法的标准实现模式(只是一个示例,具体情况还需根据我们的代码需求来确定):

复制代码
 1         public class foo : IEquatable<foo>
 2         {
 3             public override bool Equals(object right)
 4             {
 5                 //是否为null
 6                 if (Object.ReferenceEquals(right, null))
 7                     return false;
 8                 //是否引用相等
 9                 if (Object.ReferenceEquals(this, right))
10                     return true;
11                 //可能是子类,所以需要精确的类型判断
12                 if (this.GetType() != right.GetType())
13                     return false;
14                 //调用实例的Equals()方法
15                 return this.Equals(right as foo);
16             }
17 
18             //IEquatable<foo> 成员
19             public bool Equals(foo other)
20             {
21                 //略去
22                 return true;
23             }
24         }
复制代码

我们仔细观察这个实现:第一个坚持判断右边对象是否为null,对于this指针引用则不需要这一步,因为在C#中this指针永远不会为null。第二个判断两个对象是否为同一个引用,如果两个对象引用相同,则对象内容一定相等。第三个函数用来判断两个对象的类型是否相同。这里使用精确的比较是非常重要的。首先,没有假设this指针的类型为Foo,而是再次调用this.GetType()获取,因为实际的类型可能继承自Foo。

 

小节:

对于所有的值类型,都应该覆写其Equals()方法;对于引用类型,当System.Object提供的引用语义不能满足我们的需求时,才应该去覆写Equals()方法。覆写Equals()方法时也应该同时覆写GetHashCode()方法(条目7)。对于operator==()则比较简单,只要创建的是值类型,都必须重新覆写一个operator==(),理由和覆写ValueType.Equals()实例函数完全一样。引用类型应该尽量避免覆写operator==(),.NET 希望所有引用类型都应用的operator==()都遵循引用语义,因为系统提供的默认版本时通过比较两个值类型实例的内容,并且是用反射来实现的。(其实如果对于性能不是那么敏感的话可以忽略)。

本文转自gyzhao博客园博客,原文链接:http://www.cnblogs.com/IPrograming/archive/2012/09/01/Effective_CSharp_06.html ,如需转载请自行联系原作者
相关文章
《More Effective C# 》读书笔记 第一章
《More Effective C# 》读书笔记 第一章
|
C++ 容器 数据库连接
读书笔记 effective c++ Item 8 不要让异常(exceptions)离开析构函数
1.为什么c++不喜欢析构函数抛出异常 C++并没有禁止析构函数出现异常,但是它肯定不鼓励这么做。这是有原因的,考虑下面的代码: 1 class Widget { 2 3 public: 4 5 .
832 0
|
C++ 编译器 安全
读书笔记 effective c++ Item 6 如果你不想使用编译器自动生成的函数,你需要明确拒绝
问题描述-阻止对象的拷贝   现实生活中的房产中介卖房子,一个服务于这个中介的软件系统很自然的会有一个表示要被销售的房屋的类: 1 class HomeForSale { ... };   每个房产中介会立刻指出来,要销售房屋的每个属性都是唯一的,没有两个完全一样的房屋。
833 0
|
C++ 容器 C语言
读书笔记 effective c++ Item 7 在多态基类中将析构函数声明为虚析构函数
1. 继承体系中关于对象释放遇到的问题描述 1.1 手动释放 关于时间记录有很多种方法,因此为不同的计时方法创建一个TimeKeeper基类和一些派生类就再合理不过了: 1 class TimeKeeper { 2 3 public: 4 5 TimeKeeper(); 6 7 ~TimeKeeper(); 8 9 .
1139 0
|
C++ 编译器
读书笔记 effective c++ Item 5 了解c++默认生成并调用的函数
1 编译器会默认生成哪些函数  什么时候空类不再是一个空类?答案是用c++处理的空类。如果你自己不声明,编译器会为你声明它们自己版本的拷贝构造函数,拷贝赋值运算符和析构函数,如果你一个构造函数都没有声明,编译器同样会为你声明一个默认拷贝构造函数。
969 0
|
安全 程序员 C++
读书笔记 effective c++ Item 3 在任何可能的时候使用 const
Const可以修饰什么?   Const 关键字是万能的,在类外部,你可以用它修饰全局的或者命名空间范围内的常量,也可以用它来修饰文件,函数和块作用域的静态常量。在类内部,你可以使用它来声明静态或者非静态的数据成员。
872 0
|
C++ 编译器 安全
读书笔记 effective c++ Item 2 尽量使用const,枚举(enums),内联(inlines),不要使用宏定义(define)
这个条目叫做,尽量使用编译器而不要使用预处理器更好。#define并没有当作语言本身的一部分。 例如下面的例子: 1 #define ASPECT_RATIO 1.653 符号名称永远不会被编译器看到。
1038 0
|
C语言 C++ 程序员
读书笔记 effective c++ Item 1 将c++视为一个语言联邦
Item 1 将c++视为一个语言联邦 如今的c++已经是一个多重泛型变成语言。支持过程化,面向对象,函数式,泛型和元编程的组合。这种强大使得c++无可匹敌,却也带来了一些问题。所有“合适的”规则看上去都有例外。
960 0
|
存储 安全 编译器
[笔记]读书笔记 C++设计新思维《一》基于策略的类设计(下)
[笔记]读书笔记 C++设计新思维《一》基于策略的类设计(下)
|
存储 关系型数据库 编译器
C++ Primer Plus 第6版 读书笔记(9)第 9章 函数——内存模型和名称空间
C++ Primer Plus 第6版 读书笔记(9)第 9章 函数——内存模型和名称空间
112 1