《Effective Ruby:改善Ruby程序的48条建议》一第12条:理解等价的不同用法

第12条:理解等价的不同用法

看看下面的IRB会话然后自问一下:为什么方法equal?的返回值和操作符“==”的不同呢?

事实上,有四种方式来检查对象之间的等价性。有时各种不同的方法相互重叠地使用相同方式进行比较,但如你所见,并不总是这样。基于不同的比较方法和对象调用,你能得到意外的结果。但这种情况不会再发生了,因为我们已经搞清楚了它们的区别。
似乎通过四种不同方式来比较对象的等价性有点过分了。对多数对象是这样的,因为这四种方法最终做了相同的事情。但有些时候,这些等价方法常常存在少许(并非那么少)差别。比如,很多表示数字的类在使用“==”操作符比较对象时会做类型的隐式转换。

每当你定义一个类时,它都继承了四个不同的等价性检查方法。如果你想重载它们中的任何一个,理解它们的工作方式就非常重要。如果你计划如第13条中讨论的那样实现所有的比较操作符,那理解它们就更有用了。我们将从简单的开始——你可能从来没有注意过的一个方法。
你可能对于上述例子中equal?方法和“=”操作符的返回结果不同感到惊讶。显然,它们测试的东西是不同的。就目前而言我敢说equal?这个名字是一个误用。它实际上并不是比较两个对象的内容和值,而是检查它们是否为同一个对象。也就是说,如果它们具有相同的object_id,其值才会为真。(从内部实现上讲,equal?检查两个对象是否指向内存中同一个块。)
即使上面两个字符串具有相同的内容,但它们显然不是同一个对象。它们只是恰好具有相同字符的不同对象而已。每次Ruby评估一个字符串时,会分配一个新的String对象,即使具有相同字符串的对象已经在内存中存在了。当然,这是你希望发生的。你肯定不希望在改变一个字符串的同时也意外地改变了程序中另一个具有相同内容的字符串。(如果你是想对字符串做写时拷贝(copy-on-write),不只有你想这么做。Ruby 2.1中提供了可以共享内存的不变字符串,即相同字符串,详见第47条。)
关于equal?方法重要的一点是其值相等的那些对象的行为应该是完全一致的。即,你不该在重载方法时给它不同的实现。因存在依赖这个方法的几个类,如果改变了其实现,会以奇怪的方式破坏它们。如果你希望比较对象的值,equal?方法可能并不是合适的选择。你也许会对“==”操作符更感兴趣。
在比较对象时,“==”做了你想做的事,有时它做得好到让你惊讶。每个类都可以重定义“==”,其惯例行为是:当两个对象表示相同的值时返回真值。这解释了前面的字符串对比以及对1和1.0的值的判断,顺便说一下,1和1.0是由两个不同的类Fixnum和Float表示的。这可能和你预想的等价性的比较是一致的。
如果你没有自定义“==”的实现,它就会继承默认实现,其行为与方法equal?一样。这可能不是很有用的行为,因为显然有些对象即使没有在相同的内存位置上存储,它们在比较时也应该被认为是相等的。这时你需要使用“==”操作符来比较对象的内容。也许这只是简单地对对象属性、记录ID的比较,或是将比较行为委派给另一个对象。无论哪种方式,“==”操作符应该都比equal?方法高明。不过你要抵制直接定义“==”的诱惑。
第13条解释了如何通过定义“<=>”操作符来毫无代价地定义“==”(以及其他操作符)并将其嵌入到模块Comparable中。如果你对定义排序操作符如“>”感兴趣,你应该遵循那一条中的建议。
下一个要讲的等价性方法的命名很不明确并有些难以理解。不过这个方法很重要。eql?方法在Hash类中被广泛用于比较对象的键。你肯定不想在Hash中将一个键插入多次,因此定义合理的eql?方法只是完成了一半。我们等会将学习另一半。
eql?方法的默认比较行为和equal?一样,并且它可能比你想得更加严格。如果你定义一个类并使用这个类的实例如哈希的键,同时又没有重载其默认实现,你将很快感到惊讶。由于eql?方法严格基于object_id比较对象,你很可能最终拥有一个很大的哈希,而这并不是你预期的。比如,假设有如下这个Color类。

如果我们创建两个Color类并赋予相同的参数值,随后将其用于哈希的键,让我们看看会发生什么:

即使不知道eql?方法会做什么,你大概也会预期最终的哈希只存在一个键,而非两个。要将哈希中的这两个键合并成一个,我们需要在定义eql?方法的同时讲讲另一个非常重要的方法hash。当Hash类将对象用作键的时候,需要决定将对象存在数据结构的什么地方。它通过调用对象上的hash方法来实现这个功能。当两个对象表示相同键时它们的hash方法应该返回相同的值。但是不同的对象在调用hash方法时也可以返回相同的值。也就是说,它们并不需要一定是唯一的。当两个对象的hash方法返回了相同的值,会引发冲突,这将通过继续比较eql?方法的值来确定其是否具有相同的键。因此,如果我们希望这两个相似的Color对象表示相同的哈希键,我们需要同时实现hash方法和eql?方法。对于这个简单的类,我们将手工委派这两个方法到@name属性上:

多数时候,你可能希望像第13条描述的那样实现“<=>”操作符,然后简单地用eql?来作为“==”的别名。那么如果它只是被作为相等操作符的别名,我们为什么还需要这个方法呢?没错,这确实是个问题。让我们通过回顾“==”操作符在对数字类所做的类型转换来回答这个问题吧。再来看看这些类的“==”和eql?方法的区别:

使用“==”操作符时认为1和1.0相等,而使用eql?方式时认为它们不等可能是合理的,因此这就是为什么它们都会存在。当你定义自己的类时,你需要决定这个类的对象如何使用哈希键相互比较。如果你选择宽松地实现“==”操作符,你需要严格地定义eql?方法,否则,用eql?作为“==”的别名。之前我说过但这里有必要重申:记住,如果你没有自行定义eql?方法,它将使用默认实现,也就是与equal?方法相同的行为。
最后,让我们看看你始终在使用的操作符(你甚至没有意识到它)——case equality操作符。这个操作符写作三个等号(“===”),但常常通过case表达式间接使用它。自己来看:

有两种不同的方式来使用case表达式。我们关心的这一种是当你为case关键字提供一个表达式时Ruby选择的(上面的命令变量)。case使用“===”操作符来比较被赋值的表达式与每个when语句的等价性。如果你移除语法糖并揭示出其下的if表达式,可能更容易看出发生了什么:

注意case关键字之后的表达式总是出现在“===”的右边。这非常重要。在Ruby中,左操作数是消息的接收者,右操作数是方法调用的唯一参数。这也意味着在Ruby中,操作符不是必然可换的,因为它们的行为是由左操作数决定的。同时考虑这两点,意味着你能够用普通方法调用来使用任何操作符:

case equality操作符在这里能做些什么呢?好吧,需要知道的一件重要的事是哪个对象会作为接收者。当你知道了when语句所接收的表达式会作为“===”操作符的左操作数(也就是接收者)后,你就明白了当前操作符的实现方式。Ruby核心类对case equality操作符有一些有用的变形。
“===”的默认实现很枯燥,它只是将计算对象传给“==”。这是在你自己的类中将会继承的版本。不过当你注意一些类比如Regexp时,事情就变得有趣起来了。Regexp类定义了“===”操作符,如果作为字符串的参数和接收者(正则表达式)匹配,那么返回值为真。你得将正则表达式作为左操作数;否则,你将不能使用Regexp类中的“===”实现。

类和模块中也定义了类方法版本的“===”操作符。它们共享了一个通用实现,如果右操作数是左操作数的一个实例,那么其值为真。这几乎是对接收者和参数转化过的is_a?方法的操作符版本。这使我们能将类和模块名作为when语句的参数。了解如果没有case语法会怎样工作是非常好的事情。思考is_a?和“===”的相似性。

你可能不用直接自定义“===”操作符,因为Object的默认实现已经足够了,你的类可以通过继承来使用。不过,如果你希望你的类或对象在被用作case表达式中的when语句的参数时拥有特殊的行为,你得知道去重载哪个操作符。仅需记住,哪个操作数该作为接收者,哪个该作为参数。
要点回顾
绝不要重载equal?方法。该方法的预期行为是,严格比较两个对象,仅当它们同时指向内存中同一对象时其值为真(即,当它们具有相同的object_id时)。
Hash类在冲突检测时使用eql?方法来比较键对象。默认实现可能和你的想象不同。遵循第13条的建议之后再使用别名eql?来替代“==”书写更合理的hash方法。
使用“==”操作符来测试两个对象是否表示相同的值。有些类比如表示数字的类会有一个粗糙的等号操作符进行类型转换。
case表达式使用“===”操作符来测试每个when语句的值。左操作数是when的参数,右操作数是case的参数。

时间: 2024-10-21 17:49:45

《Effective Ruby:改善Ruby程序的48条建议》一第12条:理解等价的不同用法的相关文章

编写高质量代码改善C#程序的157个建议[IEnumerable&lt;T&gt;和IQueryable&lt;T&gt;、LINQ避免迭代、LINQ替代迭代]

原文:编写高质量代码改善C#程序的157个建议[IEnumerable<T>和IQueryable<T>.LINQ避免迭代.LINQ替代迭代] 前言 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议29.区别LINQ查询中的IEnumerable<T>和IQueryable<T> 建议30.使用LINQ取代集合中的比较器和迭代器 建议31.在LINQ查询中避免不必要的迭代

编写高质量代码改善C#程序的157个建议[为泛型指定初始值、使用委托声明、使用Lambda替代方法和匿名方法]

原文:编写高质量代码改善C#程序的157个建议[为泛型指定初始值.使用委托声明.使用Lambda替代方法和匿名方法] 前言 泛型并不是C#语言一开始就带有的特性,而是在FCL2.0之后实现的新功能.基于泛型,我们得以将类型参数化,以便更大范围地进行代码复用.同时,它减少了泛型类及泛型方法中的转型,确保了类型安全.委托本身是一种引用类型,它保存的也是托管堆中对象的引用,只不过这个引用比较特殊,它是对方法的引用.事件本身也是委托,它是委托组,C#中提供了关键字event来对事件进行特别区分.一旦我们

编写高质量代码改善C#程序的157个建议[用抛异常替代返回错误、不要在不恰当的场合下引发异常、重新引发异常时使用inner Exception]

原文:编写高质量代码改善C#程序的157个建议[用抛异常替代返回错误.不要在不恰当的场合下引发异常.重新引发异常时使用inner Exception] 前言 自从.NET出现后,关于CLR异常机制的讨论就几乎从未停止过.迄今为止,CLR异常机制让人关注最多的一点就是"效率"问题.其实,这里存在认识上的误区,因为正常控制流程下的代码运行并不会出现问题,只有引发异常时才会带来效率问题.基于这一点,很多开发者已经达成共识:不应将异常机制用于正常控制流中.达成的另一个共识是:CLR异常机制带来

编写高质量代码改善C#程序的157个建议[4-9]

原文:编写高质量代码改善C#程序的157个建议[4-9] 前言 本文首先亦同步到http://www.cnblogs.com/aehyok/p/3624579.html.本文主要来学习记录一下内容: 建议4.TryParse比Parse好 建议5.使用int?来确保值类型也可以为null 建议6.区别readonly和const的使用方法 建议7.将0值设为枚举的默认值 建议8.避免给枚举类型的元素提供显式的值 建议9.习惯重载运算符 建议4.TryParse比Parse好 如果注意观察,除st

编写高质量代码改善C#程序的157个建议[C#闭包的陷阱、委托、事件、事件模型]

原文:编写高质量代码改善C#程序的157个建议[C#闭包的陷阱.委托.事件.事件模型] 前言 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议38.小心闭包中的陷阱 建议39.了解委托的实质 建议40.使用event关键字对委托施加保护 建议41.实现标准的事件模型 建议38.小心闭包中的陷阱 首先我们先来看一段代码: class Program { static void Main(string[] arg

编写高质量代码改善C#程序的157个建议[为类型输出格式化字符串、实现浅拷贝和深拷贝、用dynamic来优化反射]

原文:编写高质量代码改善C#程序的157个建议[为类型输出格式化字符串.实现浅拷贝和深拷贝.用dynamic来优化反射] 前言 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议13.为类型输出格式化字符串 建议14.正确实现浅拷贝和深拷贝 建议15.使用dynamic来简化反射实现 建议13.为类型输出格式化字符串   有两种方法可以为类型提供格式化的字符串输出. 一种是意识到类型会产生格式化字符串输出,于是

编写高质量代码改善C#程序的157个建议[匿名类型、Lambda、延迟求值和主动求值]

原文:编写高质量代码改善C#程序的157个建议[匿名类型.Lambda.延迟求值和主动求值] 前言 从.NET3.0开始,C#开始一直支持一个新特性:匿名类型.匿名类型由var.赋值运算符和一个非空初始值(或以new开头的初始化项)组成.匿名类型有如下基本特性: 1.既支持简单类型也支持复杂类型.简单类型必须是一个非空初始值,复杂类型则是一个以new开头的初始化项. 2.匿名类型的属性是只读的,没有属性设置器,它一旦倍初始化就不可更改. 3.如果两个匿名类型的属性值相同,那么就任务这两个匿名类型

编写高质量代码改善C#程序的157个建议[动态数组、循环遍历、对象集合初始化]

原文:编写高质量代码改善C#程序的157个建议[动态数组.循环遍历.对象集合初始化] 前言   软件开发过程中,不可避免会用到集合,C#中的集合表现为数组和若干集合类.不管是数组还是集合类,它们都有各自的优缺点.如何使用好集合是我们在开发过程中必须掌握的技巧.不要小看这些技巧,一旦在开发中使用了错误的集合或针对集合的方法,应用程序将会背离你的预想而运行. 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议16.

编写高质量代码改善C#程序的157个建议[10-12]

原文:编写高质量代码改善C#程序的157个建议[10-12] 前言 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议10.创建对象时需要考虑是否实现比较器 建议11.区别对待==和Equals 建议12.重写Equals时也要重写GetHashCode 建议10.创建对象时需要考虑是否实现比较器 有对象的地方就会存在比较,就像小时候每次拿着考卷回家,妈妈都会问你隔壁的那谁谁谁考了多少分呀.下面我们也来举个简单