《Effective Ruby:改善Ruby程序的48条建议》一第13条:通过"<=>"操作符实现比较和比较模块

简介:

本节书摘来自华章出版社《Effective Ruby:改善Ruby程序的48条建议》一书中的第2章,第2.8节,作者 [美]彼得 J.琼斯(Peter J. Jones),更多章节内容可以访问云栖社区“华章计算机”公众号查看

第13条:通过"<=>"操作符实现比较和比较模块

在第12条中提到了四种测试对象相等性的方法。如果你对对象的排序和比较有兴趣,那么你就需要进一步定义其他的比较操作符了。与等价操作符不同的是,类并没有从其他比较操作符中继承默认实现。还好,Ruby提供了一种简便的方式来实现它,这一点我们会在稍后讨论。
首先,让我们做一件有意思的事情,就是实现一个具有特殊序列的类。作为程序员,我们已经习惯了奇怪的版本号的定义,因此它们并不会给我们造成太多的困扰。但对外行来说就完全不同了。如何比较“10.10.3”和“10.9.8”这两个版本号呢?很明显,如果我们使用词典序来比较它们那就错了。为了得到正确的答案,你需要分别比较它们的每个部分。这就是接下来我们在Version类中要做的事情。
为了更清楚地表现,我们暂且处理仅含三个部分的版本号(就像前面描述的那样)。第一部分是主版本,接下来是次版本,最后是修订版本。同时,为了更加专注于版本号的比较,我们仅处理那些符合格式要求的版本号。这样,将一个用来描述版本的字符串解析成独立的几个部分的工作就变得简单多了。
image

我想再次强调,一般说来类不会自动继承比较操作符,但有一个例外。这一点我们会在之后讨论,现在你要做的就是为这个类定义一个比较操作符,也就是“<=>”操作符。这个特别的操作符其实继承自Object类,但是这个继承的实现却是不完整的。让我们看看如果试图对一组Version类的对象进行排序时会发生什么:
image

这对我们来说并没有什么帮助。要怪就怪“<=>”操作符的默认实现好了。它只考虑了两个对象是否相同(使用equal?和“===”操作符),却没有按我们的需要完整地进行比较。如果两个被比较的对象不相同就返回nil,从而告诉sort方法这个比较是非法的。但也还好,毕竟你不能对一个通用的实现抱有太大的期望,所以咱们还是自己动手实现一个吧。
完整地实现比较操作符需要两步。最难的一部分是编写一个合理的“<=>”操作符(通俗的说法是“太空舱”操作符)。要记住在Ruby语言中,二元操作符最终会被转换成方法调用的形式,左操作数对应着方法的接收者,右操作数对应着方法第一个也是唯一的那个参数。当编写一个比较操作符时,通常的做法是将参数命名为“other”,因为它代表着要比较的另一个对象。
由于“<=>”操作符的返回值非常灵活,因此它可以被看成全能的比较操作符。它可以返回如下四种情况:
当接收者和参数的比较无意义时,比较操作符理应返回nil。因为参数可能是另一个类的实例,甚至可以是nil。对于某些类来说,在比较之前将参数转换成正确的类型是很有用的,但通常情况下更好的做法是,当接收者和要比较的参数不是同一个类的实例时就直接返回nil。我们要实现的Version类就将这样做。
如果接收者比参数小,则返回-1。换句话说,如果使用“<”操作符比较的结果是真,那么使用“<=>”比较的结果就应该是一个负值。
如果接收者比参数大,则返回1。这就要用“>”操作符来解释了。如果使用“>”操作符比较的结果是真,那么使用“<=>”比较的结果就应该是一个正值。
如果接收者与参数相等,返回0。换句话说,只有当“<=>”返回值为0时,使用“==”比较的结果才是真。
我们想让Version类中的比较操作符和数值类中的比较操作符有一致的行为。事实上,我们可以像数值类一样实现我们的版本。你可以看到,它的实现遵循了前面提及的规则:
image

当编写“<=>”时,通常的做法是将比较方法代理给对象的实例变量。Version类中的三个变量都是Fixnum类的实例,这意味着它们都实现了可用的“<=>”操作符。这大大简化了我们的工作。为了比较版本号,我们需要考虑接收者(左操作数)中的实例变量以及那些参数中的实例变量(右操作数)从主版本到修订版本的顺序。一旦接收者中有一个变量与参数中对应的变量不相符,我们就可以停止比较。换句话说,如果要比较的两个版本的主版本号不相符,那么无需比较次版本号或修订版本号就已经可以知道谁大谁小。但是如果主版本号相同,就需要比较次版本号。以此类推,当次版本号也相同时就需要比较修订版本号。当所有部分和另一个都相同时,我们的比较操作符应该返回0以表示两个Version对象的等价性。而其他情况下,只需使用“<=>”操作符对第一组不匹配的变量进行操作,并将其结果返回。思考Version类中比较操作符的实现:
image

每个部分都会分别被比较,并将其比较的结果存放于一个数组中。我们需要做的仅仅是在这个数组中寻找第一个非零元素(第一对不相同的部分)。如果所有部分都相同,那么detect方法就会返回nil,而此时比较操作符返回0。现在,我们就可以对一组Version对象进行排序了。
image

为了完整地实现Version对象的比较功能,我们还需添加一段代码。除了排序之外,我们想让这些对象能够使用诸如“>”、“>=”这样的操作符。事实上,这五个操作符构成了完整的排序操作符,它们是:“<”、“<=”、“==”、“>”和“>=”。你如果知道我们无需自己手动实现它们一定会很高兴吧。我们要做的只是引入一个名为Comparable的模块。
image

就这么简单。现在我们可以使用所有的比较操作符以及一个额外的helper方法:
image

当使用Comparable模块时,还有些因素需要考虑一下。首先,对于某些类你可能想实现自己的“==”操作符,因此这比使用Comparable模块中的方法间接一些。在第12条中有一个很好的例子,其在做比较之前对数值类型进行了转换。如果你想这样做,你需要编写自己的等价操作符或者改变使“<=>”操作符返回0的条件。具体如何选择,应由你希望其他的比较运算符表现出什么样的行为来决定。
如果你想让“>=”、“<=”和“==”返回一致的结果,那么应该改变“<=>”计算相等性的方式。如果无需保持一致,简单地重载“==”就可以了,使其相较于其他比较运算符不那么严格。但是对于多数类,你可能希望使其所有运算符都相互一致。
最后,如果你希望类的实例能被作为哈希表的键来使用,你需要再做两件事情。首先,使“eql?”方法作为“==”操作符的别名。因为“eql?”的默认实现和“equal?”相同,如果不重新实现它会使类中的“<=>”操作符变得没有意义。而别名则使得哈希类使用由Comparable模块定义的“==”运算符来进行比较。
你还需要定义一个返回Fixnum类对象的哈希方法。为了使哈希类达到最好的性能,应该确保不同的对象返回不同的哈希值。下面是一个Version类的简单的示范实现(编写一个优化的哈希方法不在本书的讨论范围之内)。
image

要点回顾
通过定义“<=>”操作符和引入Comparable模块实现对象的排序。
如果左操作数不能与右操作数进行比较,“<=>”操作符应该返回nil。
如果要实现类的“<=>”运算符,应该考虑将eql?方法设置为“==”操作符的别名,特别是当你希望该类的所有实例可以被用来作为哈希键的时候,就应该重载哈希方法。

相关文章
|
2月前
|
Ruby
|
2月前
|
Ruby
|
2月前
|
Ruby
|
2月前
|
Java C++ Ruby
|
10月前
|
缓存 监控 数据库
使用Ruby构建可扩展的Web应用程序
在当今科技驱动的世界中,Web应用程序成为了企业和个人进行业务活动、提供服务和与用户互动的重要方式。而Ruby作为一种简洁、优雅且易于学习的编程语言,已经成为许多开发者的选择。本篇博客将介绍如何使用Ruby构建可扩展的Web应用程序。
82 0
|
测试技术 C++ Ruby
C++程序中嵌入Ruby脚本系统
Ruby,一种为简单快捷面向对象编程(面向对象程序设计)而创的脚本语言,由日本人松本行弘(まつもとゆきひろ,英译:Yukihiro Matsumoto,外号matz)开发,遵守GPL协议和Ruby License。
1582 0
|
消息中间件 安全 Ruby
Nanite:Ruby程序的一个自我装配集群
本文讲的是Nanite:Ruby程序的一个自我装配集群,Nanite(由Ezra Zygmuntowicz开发)是Engine Yard云计算策略的一个新兵:它是“Ruby程序的一个自我装配集群”,用以构筑高度可伸缩的Web应用的后端。
1102 0
|
程序员 Ruby
《Effective Ruby:改善Ruby程序的48条建议》一导读
学习一门新的编程语言通常需要经过两个阶段。第一阶段是学习这门编程语言的语法和结构。如果我们具有其他编程语言的经验,这个阶段通常只需要很短的时间。以Ruby为例,接触过其他面向对象语言的程序员对Ruby的语法也会比较熟悉。有经验的程序员对于语言的结构(如何根据语法构建应用程序)是很熟悉的。
1216 0