Ruby-黑魔法-全-
Ruby 黑魔法(全)
原文:
zh.annas-archive.org/md5/192c655a96f20cf46450d7392cfa7b74译者:飞龙
第一章:本书内容介绍
你找到了这本书!太棒了。我真的希望它能够到你手中。
想象一下,有人告诉你,他发现了一种新的写作方式。不是一种新的语言,比如法语、日语或精灵语,而是一种全新的写作方式,可以让你的故事真正发生。如果你描述了一个迷宫,人们可以进入——并迷失在——这个迷宫里。如果你写一个遥远的星球,在那里机器人海盗与忍者巫师展开战斗,那么那个星球就会真实存在。不仅如此,你还可以写出像“Beep boop shiver me circuits”这样的对话,或者施展像ninja_wizard.throw_flaming_ninja_stars这样的魔法。疯了,对吧?这可能正是你会说的话:这完全疯了,任何想到这一点的人肯定有太多时间在手,太多想象力了。

好吧,事实证明并没有“想象力过多”这种事。那么!想象一下:不仅这种疯狂的新写作方式是真实的,而且你可以学会如何做到这一点。你只需要一点练习,就能弄明白如何创造你自己的世界和规则。你将成为主宰,你可以做几乎任何你能想到的事情。不仅如此,如果你做得非常好,人们将从四面八方来体验你创造的世界,并使用你创造的所有神奇事物。
你可以暂时停止想象(至少现在可以)。我告诉你,这是真的!而这本书可以帮助你做到这一点。你现在手中拿着的页面是一本关于编程语言的指南,这种语言叫做Ruby,它将让你做所有这些事情,你所需要的只有大脑、一台计算机和 Ruby。
这怎么可能? 你可能在想。如果有这么酷而强大的东西存在,我现在肯定早就听说过了。
这引出了我们接下来的话题。
为什么要学习编程(以及为什么是 Ruby)?
当我年轻的时候,学习编程对我来说听起来很无聊。我认为编程和计算机都与数学和逻辑有关——没有空间去发挥创造力或做任何有趣的事情。整天,人们都在告诉我该做什么:去学校、遛狗、看牙医、做作业。我觉得编程大概也只是这些事情的延续,所以我完全避免接触它。相反,我写关于太空旅行、魔法和遥远世界的故事,在那些世界里,不仅有惊人的事情发生,而且我是主角!我现在依然一直写故事,但即使是最棒的故事也会在读者翻完最后一页后结束。尽管你多么希望星际飞船或忍者巫师成为现实,写关于它们的故事并不能让它们发生。所以我确实写了很多故事,但我也不得不去看牙医。
然后发生了一件非常奇怪的事:我决定尝试编程。我发现我原本以为会非常枯燥无味的事情,实际上完全相反——它既具有挑战性又很有趣。突然之间,我开始掌控一切!如果我让电脑做一个拼图游戏,它就做了一个拼图游戏。如果我让它做一个网站,它就做了一个网站。它创造出了我能看到、玩耍和使用的真实事物。就像我多年来一直在写的那些故事,现在可以活生生地呈现出来,而所有这一切只需要这个小小的框框和我能用来与它交流的语言。
的确,一些编程语言很难,一些甚至令人困惑。但 Ruby 不一样:它的设计目的是让你开心——让你更容易阅读和理解,而不仅仅是让计算机理解。Ruby 的创建是为了帮助你编写既能被计算机也能被人类理解的故事,因此你不会看到像 static 和 void 这样的奇怪符号或单词,取而代之的是像 unless、rescue、self,甚至 begin 和 end 这样的词语,程序看起来几乎像英语一样。
就像任何编程语言一样,学习 Ruby 会帮助你掌握重要的技能,制作很酷的东西,并且感觉到成就感。但最重要的是,你会玩得很开心。在编程语言中,我认为 Ruby 是最有趣的。
假设你想让电脑说“Howdy!”。如果你想用其他语言来实现——比如 Java——你可能需要写一些非常复杂的代码,像这样:
class Howdy {
public static void main (String[] args) {
System.out.println("Howdy!");
}
}
打印一个单词需要这么多代码。在 Ruby 中要做同样的事,你只需要输入:
puts "Howdy!"
就这样!Ruby 会把字显示在屏幕上。简单吧?Ruby 旨在让你成为一个快乐高效的程序员(哦对了——你现在是程序员了),所以它去掉了很多复杂的语法(比如 { 和 ;),让你不必到处写像 public static void main 这样的无聊代码。而且由于 Ruby 几乎可以做所有像 Java 这种更复杂语言能够做的事,你将能够更快、更省力地构建出令人惊叹的作品。
让我们开始吧!
所有成人请注意:安装 Ruby
好的——这部分你可能需要叫上妈妈、爸爸、爷爷、奶奶、叔叔、阿姨、老师或其他成年人的帮助来在你的电脑上安装 Ruby。Ruby 是免费的,但如果你还没有它,你需要连接互联网来下载。
方向会根据你使用的计算机类型有所不同,所以如果你不确定,可以问一下大人!
如果你使用的是 Windows,请跳转到 Windows 上的安装。
在 Mac 或 Linux 上安装
首先,我们来检查一下你是否已经安装了 Ruby。如果你使用的是 Mac 或运行 Linux 的电脑,你可以在命令行中检查你安装的 Ruby 版本——这也是你编写 Ruby 程序的地方。
命令行的使用方式可能和你通常通过点击图标和用鼠标移动的方式非常不同,但一旦习惯了,命令行的使用会更快速、更高效。
在 Mac 或 Linux 计算机上,你的命令行是在一个名为Terminal的应用程序中。找到并打开你的终端应用程序。你应该会看到类似以下内容:

一旦打开了终端,继续输入以下命令(你不需要输入美元符号——只需输入ruby -v部分)并按 ENTER 键:
$ **ruby -v**
如果 Ruby 已安装,你应该会看到类似以下内容的回应:
ruby 2.0.0p247
如果你收到这个响应并且包含2.0.0,那就万事大吉!跳到成就解锁:Ruby 安装完成!。如果你看到的版本不是2.0.0(比如1.9.3或1.8.7),我们需要将 Ruby 升级到 2.0.0(本书使用的版本)。如果你的计算机非常先进,可能已经安装了 Ruby 2.1——本书中的代码在 Ruby 2.1 上也能正常工作。为了获得最佳效果和最少的错误,建议你使用 Ruby 2.0.0 来运行所有示例。
如果你的计算机显示类似以下内容:
-bash: ruby: command not found
如果没有显示出来,那说明你的电脑上没有安装 Ruby。别担心,找个成年人帮忙,并跳转到附录 A,那里有详细的逐步说明。我们会在那儿安装 Ruby!安装完毕后,回来继续本章节的内容。
在 Windows 上安装
如果你在运行 Windows 的 PC 上,可以通过打开命令提示符来检查是否已安装 Ruby。我们在这个示例中使用的是 Windows 7。你可以从开始菜单打开命令提示符,或者通过搜索cmd.exe找到它;找到后,双击它打开应用程序。你应该会看到类似以下内容:

你的命令提示符——即>前面的部分——可能和我的不同,但没关系!输入ruby -v,然后按 ENTER 键:
> **ruby -v**
如果你收到包含2.0.0的响应,那就万事大吉!如果你看到的是除了 2.0.0 以外的 Ruby 版本,或者收到这个错误:
'ruby' is not recognized as an internal or external command,
operable program or batch file.
如果没有安装,我们就需要继续安装 Ruby。让我们开始吧!
使用 RubyInstaller
在 Windows 上安装 Ruby 最简单的方法是访问rubyinstaller.org/downloads/并下载 Ruby 2.0.0-p481。(如果安装网站上显示的p后面的数字稍微高于这里的数字,不用担心;这意味着该版本略微更新,但仍然是 Ruby 2.0,应该能够正常工作。)下载完成后,前往你保存.exe文件的文件夹,双击运行安装程序。安装程序会要求你执行以下操作:
-
当安装程序提示你选择安装时使用的语言时,选择“English”(或者你最熟悉的语言)。
-
安装程序会要求你接受其许可协议。勾选“我接受许可协议”后点击下一步。
-
安装程序会询问你希望将 Ruby 安装到哪里,默认位置是
C:\Ruby200。这很好!你还会看到一个复选框,上面写着“将 Ruby 可执行文件添加到 PATH”。确保勾选了该框,然后点击安装。 -
如果一切顺利,你应该会看到“完成 Ruby 设置向导”屏幕。点击完成,然后你就完成了!
安装程序运行完后,关闭命令提示符,重新打开它,并输入 ruby -v;你应该看到计算机打印出带有 ruby 2.0.0 的响应。我的显示是这样的(你的可能略有不同):
ruby 2.0.0p481 (2014-05-08) [i386-mingw32]

成就解锁:Ruby 安装完成!
完美!现在你已经安装了 Ruby,我们可以开始学习如何使用它了。在下一章中,我们将介绍一些 Ruby 基础知识,并学习如何互动式地使用 Ruby,也就是说,你只需按下 ENTER 键,就能看到 Ruby 执行你的代码。在接下来的章节中,你将通过一系列故事了解 Ruby 语言的细节。毕竟,Ruby 程序就是你为计算机写的故事,而 Ruby 的特点就是编写既适合人类又适合计算机阅读的代码,所以我认为通过故事来展示它的工作原理是非常合适的。我认为这些故事还挺不错的。
你可能会忍不住想,只看本书中的代码并对自己说:“没错,这很有道理!我不需要运行这些代码。”我开始编程时也这么想过,但我错了。学习编写代码的唯一方法就是——写代码,如果你只是阅读这些示例,而从不运行任何 Ruby 代码,那你就错过了很多真正酷的知识。
在我们开始冒险之前,还有一句建议:你可能需要读几遍某些内容,或者多次运行代码才能真正理解。这没关系!学习编程不仅仅是写代码的新方式——它也是一种全新的思维方式。有时候可能会有点难,但我保证只要你坚持下去,你一定能掌握它。相信我,有些人比你聪明和热情的程度都要低,但他们也学会了编程,如果他们能做到,你也能。
穿上 Ruby 套鞋
好的,你已经拥有了属于你自己的 Ruby 副本,并且知道 Ruby 是一种你可以用来指示计算机做任何事情的语言。但你可能有很多问题:Ruby 是从哪里来的?是谁创建的,为什么要创建它?用它创造了哪些惊人的东西?Ruby 有什么好处?别再疑惑了:我会给你所有这些问题的答案(还有一些额外的答案)。
虽然计算机大约在几百万年前就被发明了(你能识别为计算机的第一批设备是在 1940 年代制造的),但 Ruby 相对较晚才被创造出来,时间是在 1993 年。你可能认为 1993 年也已经是几百万年前的事情了,某种程度上你是对的。那时互联网只有大约一百个网站。没有智能手机。事实上,大多数人的手机是通过电线连接到墙上的。那是黑暗的时代。
但在 1990 年代中期的那个古老时代,一个名叫松本行弘(或者朋友们称他为“Matz”)的人正忙于尝试发明未来。他对那些旨在让计算机更容易使用,但却难以理解、阅读、记忆和使用的编程语言感到沮丧。为什么没有一种语言是为了让人类使用而设计的,一种清晰、简单,甚至是有趣的语言呢?
Matz 意识到他理想中的编程语言并不存在,于是他创造了它。Matz 曾说:“我希望看到 Ruby 帮助全球的每一位程序员提高生产力,享受编程,并且感到快乐。这是 Ruby 语言的主要目的。”^([1]) 这就是 Ruby 的精髓:一种有趣的方式,让你通过大脑和计算机创建游戏、网站,或者任何你能想象的东西。Matz 对他创造的语言产生了如此积极的影响,以至于 Ruby 程序员们有一句话:“Matz 很友好,所以我们也很友好”,简称 MINSWAN。在你学习 Ruby,尤其是教别人时,记得 MINSWAN!
这让我想起:用 Ruby 可以创建许多令人惊叹的东西。在过去的几年里,Ruby 被用于构建像 Twitter 和 Hulu 这样的大型网站、iPhone 应用,甚至是 NASA 的模拟。没错:你可以用 Ruby 来探索太空!每天都有越来越多的人使用 Ruby 来做各种项目,随着 Ruby 社区不断涌现出许多很酷的新工具和想法,你的想象力几乎是你在构建程序时唯一的限制。

这些程序是通过脚本编写的。这意味着,你不需要做一个长而乏味的过程叫做编译,你只需编写一个简单的 Ruby 程序,运行它,瞧!就完成了!
你的网站已经上线,游戏也在运行,你的宇宙飞船正在向女巫女王发射激光。那么如何运行这些 Ruby 脚本呢?为此,我们需要了解 ruby 命令和一个叫做 IRB 的小程序。
了解 IRB
在 Ruby 中,你可以通过输入 puts 命令将内容打印到屏幕上。假设我们想打印出“Ruby 很棒!”。让我们试试—首先我们需要打开 IRB,这是一个用于探索 Ruby 的程序。
如果你使用的是 Mac 或 Linux,打开终端并输入:
$ **irb**
你只需输入 irb,不需要输入美元符号;美元符号是我用来表示你在终端中应该输入的内容。
如果你使用的是 Windows,可以通过开始菜单运行 IRB。
一旦你打开了 IRB,应该会看到类似这样的内容:
2.0.0p247 :001 >
那就是 IRB 的提示符,它告诉你 IRB 已经准备好等待你输入内容了。根据你的 Ruby 版本,它可能看起来稍有不同,但应该以 > 结尾。
在本书中,我们会将它简化为如下形式:
>>
每当你看到 >> 时,我们就会使用 IRB。如果你在 >> 后输入内容(别忘了引号——它们非常重要!):
>> **puts "Ruby is awesome!"**
当你按下 ENTER 时,应该会看到 Ruby 打印出:
Ruby is awesome!
=> nil
太棒了!我们已经写了一个简单的程序,将一些文本打印到屏幕上。你还会看到 Ruby 输出一些关于nil的内容。暂时不用担心这个;稍后我会解释这一部分。(如果你迫不及待想了解:基本上,这是 Ruby 告诉你它已经完成打印,没有其他内容要给你了。你将在第七章中学习关于 nil 的所有内容。)酷的是,你刚刚写了你人生中的第一个 Ruby 程序!
IRB 会继续提示你,并等待你输入内容,直到你告诉它停止,你可以随时通过输入 exit(或直接退出终端程序)来停止。
使用文本编辑器和 ruby 命令
写 Ruby 命令的另一种方式是作为脚本,这种方式就是写很多行代码,然后一次性运行,而不是一行一行地执行。要写一个脚本,你需要一个叫做文本编辑器的程序。(这不是像 Microsoft Word 这样的文字处理软件;文字处理软件非常适合写故事或学校报告,但它对于写程序来说是糟糕的选择。)
注意
你可以在这里下载本书中出现的所有脚本: nostarch.com/rubywizardry/。但如果你正在学习编程并跟着书中的内容操作,尽量自己输入,而不是直接复制粘贴!你会学到更多。
Mac
所有 Mac 都带有一个叫做 TextEdit 的文本编辑器(你可以在 应用程序 文件夹中找到它)。它非常简单易用,非常适合写 Ruby 程序。如果你想要一些更有特色的东西,可以从 www.sublimetext.com/2 下载一个非常棒的免费文本编辑器,叫做 Sublime Text 2(你需要 OS X 10.6 或更高版本)。
Linux
Linux 有许多不错的编辑器,但 Gedit 是我最喜欢的之一。你可以从 wiki.gnome.org/Apps/Gedit 下载它。Sublime Text 2 也是 Linux 上非常好的编辑器,可以从 www.sublimetext.com/2 获取。
Windows
正如我刚才提到的,Microsoft Word 对于编写程序来说并不理想。而 Notepad++则是一个非常棒的 Windows 免费文本编辑器,你可以从notepad-plus-plus.org/download/v6.6.7.html下载。你还可以使用 Sublime Text 2 编辑器,网址是www.sublimetext.com/2。
创建你的第一个脚本
一旦你安装了文本编辑器,打开它并输入你在 IRB 中输入的相同内容:
puts "Ruby is awesome!"
接下来将这个文件保存为 awesome.rb 到你喜欢的任何文件夹(现在创建一个ruby文件夹来存放所有的 Ruby 程序是个好主意)。然后,打开你的终端并切换到保存awesome.rb的文件夹。使用cd命令可以这样做:
-
在 Mac 或 Linux 上,你的提示符(命令行中
$左边的部分)看起来像这样:/Users/username$。如果你把awesome.rb保存在名为ruby的文件夹里,可以通过在命令行输入以下命令来进入该文件夹:$ **cd /Users/*username*/ruby**不要输入
$部分,只需输入其后的内容。另外,不要字面上输入username;你应该用你在提示符中看到的内容替代它!(我的提示符是/Users/eweinstein/,但你的会有所不同。) -
在 Windows 上,你的提示符(命令行中
>左边的部分)看起来像这样:C:\Users\username。如果你把awesome.rb保存在名为ruby的文件夹里,可以通过在命令行输入以下命令来进入该文件夹:> **cd C:\Users\*username*\ruby**不要输入
>部分,只需输入其后的内容。同样,不要字面上输入username;你应该用你在提示符中看到的内容替代它。
一旦你进入了ruby文件夹,输入:
$ **ruby awesome.rb**
你应该看到 Ruby 输出:
Ruby is awesome!
就是这样,你已经成功运行了第一个 Ruby 脚本。做得好!
何时使用 IRB,何时使用文本编辑器
如果我们将相同的内容输入到 IRB 和脚本文件中,并且得到相同的输出,那它们之间有什么区别呢?基本上,IRB 每次只允许你尝试一行代码;每按一次 ENTER 键,IRB 就会读取或评估你写的代码并给出一个答案。这是一个很好的尝试方式,可以检查代码是否有效。
这意味着每次按下 ENTER 键时,Ruby 会中断你并返回计算每一行的结果,像这样:
>> **2 + 5**
=> 7
>> **24 * 10**
=> 240
>> **'Hi ' + 'there!'**
=> "Hi there!"
粗体中的代码是你输入的内容;其下是你按下 ENTER 键后 IRB 返回的响应。我们不总是需要这么多信息!有时候,我们只想知道所有工作结果的最终输出。为了实现这一点,我们可以将相同的代码写成脚本。只需打开之前的编辑器(例如,如果你是 Mac 用户,使用 TextEdit;如果是 Linux 用户,使用 Gedit;或者在 Windows 上使用 Notepad++),然后输入以下内容:
**puts 2 + 5**
**puts 24 * 10**
**puts 'Hi ' + 'there!'**
然后将脚本保存为 script_example.rb,或者任何你喜欢的名称,只要以.rb结尾(但不允许有空格!),使用cd切换到保存脚本的目录,最后通过ruby命令运行脚本:
$ **ruby script_example.rb**
这样,我们就能只输出我们想要的信息,而无需逐行输入:
7
240
Hi there!
这样不仅更容易阅读,而且我们现在可以通过ruby script_example.rb反复运行脚本,进行计算,而不必每次都重新输入所有命令。我们可以永久保存程序,修改它,并在之后继续构建。
本书中使用的提示符
在整本书中,我们会交替使用 IRB 来运行小段代码和使用脚本来运行较长的代码。每当你看到 IRB 提示符,它看起来是这样的:
>>
这意味着你应该使用 IRB 来运行代码;当你看不到它时,意味着你应该在文本编辑器中输入脚本并使用ruby命令来运行。下面是一个 IRB 示例:
>> **2 + 2**
=> 4
让我们花点时间来讲解这段代码的每一部分。>>部分表示,“我们在 IRB 中,IRB 是一个理解 Ruby 命令的程序。”记住,你不需要输入>>;这只是让你知道我们正在使用 IRB。书中的>>代表你自己电脑上的 IRB 提示符。
以2 + 2为例,这是你需要输入的命令——完全输入这些行,然后按 ENTER。当你看到 IRB 提示符下有几行加粗的代码时,只需逐行输入,每输入一行按一次 ENTER。
但是这段代码的第二行以=>开头。这是 IRB 在你按下 ENTER 后返回的结果。(这意味着你也不需要输入这些部分。)如果你在输入命令后遇到错误,而不是看到书中展示的结果,确保你输入的 Ruby 命令完全正确。计算机非常愚蠢:它们只会按你说的做,而不一定是你想要的!
书中的其他程序较长,因此你希望能够修改或改进它们并修正错误。这意味着你需要用文本编辑器来编写它们。我会在每个示例中提醒你该使用什么编辑器。但记住,当你看不见 IRB 的>>时,你就需要使用文本编辑器。
一旦我们进入故事,你会听到关于计算装置的描述。这些是与你的计算机类似的虚拟计算机版本,每当你在故事中看到角色在计算装置上运行 Ruby 时,他们实际上是在使用 IRB 和 Ruby,所以你也可以在家自己跟着操作。
最后,本书后面的某些脚本会比较长。我会把它们分成多个部分并逐个讲解。你会看到像这样的编号球:
➊ ➋ ➌ ➍ ➎
我会在文本中引用这些数字,这样你就可以一步步地跟着每个示例操作。你不需要把这些输入到电脑中;它们只是供参考!
再次提醒,如果你忘记了 IRB 和 Ruby 脚本提示符之间的区别,不用担心——我会在过程中提醒你!
进入闪亮的红色远方
不用担心理解你刚刚在这些例子中看到的所有代码。我们才刚开始学习 Ruby,我保证在接下来的几个章节中,我们将深入探讨它的所有秘密。我们将讲解如何处理文本和数字,如何帮助程序根据用户提供的信息做出决策,如何创建自己的 Ruby 命令,如何编写可以连接到互联网网站的脚本,等等。
我之前说过,编写 Ruby 更像是写故事,而不是为机器编写指令,因此我将通过故事来教你 Ruby 是如何工作的。在接下来的页面中,我将介绍一些角色,他们将帮助解释你想了解的 Ruby 知识。有些将是 Ruby 编程专家,有些像你一样是 Ruby 新手。许多人会遇到各种各样他们认为无法解决的问题,但通过一点努力和一些 Ruby 魔法,他们会发现这些问题并不像看起来那么严重。说到魔法,接下来也会有一点——一位国王、一位女王、一座城堡、一片被施了魔法(可能稍微有些鬼气)的森林、一位流浪的吟游诗人、一些巫师和魔法师、几条龙,当然,还有几位像你一样的小孩,他们迷失在这个疯狂的王国中,只能开始探索……

^([1]) Google 技术讲座,2008 年 2 月 (www.youtube.com/watch?v=oEkJvvGEtB4).
第二章:国王与他的绳子
一根短绳
国王心情极差。简直是非常糟糕,那种会对猫大喊大叫、把雪花球从三楼窗户扔出去的心情。如果你看到他愤怒地朝你走来,你会立刻换到街对面。如果他是你爸爸,你会一年到头给圣诞老人写信,要求换一个爸爸。真的,对每个人来说,都是资本字母B的坏消息。
事情是这样的,国王今天早晨丢失了他最心爱的物品,正好发生在他吃完平常的干燕麦后,准备进行他那次快步的午后散步之前。他翻遍了他的宫殿(字面意义上:国王有很多钱,也有很多仆人),但始终没有找到。当斯卡雷特和鲁本找到他时,他正坐在一把由纯金做成的软垫扶手椅上,悲伤得泪流满面。

“它看起来怎么样?”斯卡雷特问。
“什么看起来像什么?”国王问道,边说边让自己的咸泪顺着精心打理的胡须流到嘴里。
“你丢失的东西,”鲁本说。
“像一根绳子一样!”国王说。“因为那就是它:一根绳子,两端打着结,用来固定我的小物件和装饰品。这根绳子上有几个珠子,拼出了‘国王陛下的财产’字样,就像这样:
'Property of His Royal Highness, the King'
“一串字母,”鲁本说。
“更像是一串字符,”国王说。“每个字母都非常独特。例如,K是个弯曲的家伙。别让我再提P了——”

但鲁本和斯卡雷特并没有听。他们已经开始到处寻找国王丢失的绳子。
“你的绳子会不会掉进这个神秘的管道里?”斯卡雷特问,指着一个冒着黑烟的金属管道,管道上用白色粉笔写着神秘管道字样。
“不行,”国王说。“那神秘的管道顶部极其狭窄,像我这么长的绳子根本塞不进去。”
“你的绳子有多长?”鲁本问。
“我不确定,”国王说。“我想我们可以数一数所有字符,这样就知道了。”(相信我,这会非常无聊。)

“那样太无聊了,”斯卡雷特说。“我觉得有更好的办法。”她走到房间的一角,吹去一台非常古老的计算装置上的灰尘,小心翼翼地在它的小绿屏幕上输入了以下内容:
>> **'Property of His Royal Highness, the King'.length**
=> 40
“好外套!”国王说。“没错!我现在想起来了——我的绳子正好是 40 个字符长。可是你们怎么做到的?”
“露比有很多类似的好办法,”斯卡雷特说。“这是另一个。”
>> **'Property of His Royal Highness, the King'.reverse**
=> "gniK eht ,ssenhgiH layoR siH fo ytreporP"
国王点了点头。“是的,差不多就是我每次洗完澡后挂起来晾干时,它在镜子里看起来的样子。”
与此同时,Ruben 正在用他在神秘管道旁找到的粉笔数国王字符串中的字符数。“等一下,”他说,“我数到 42 个字符,包括两端的引号。”
国王像只肥胖的小猎犬一样哼了一声。“你不能把那些算上!”他说,“那些是两端的小结,用来包含字符!你只计算字符,不算引号。”
“这正是 Ruby 所做的,”Scarlet 解释道,“但你必须给你的字符串加上引号,否则 Ruby 会认为你在试图使用一个变量。”
关于变量的更多内容
相信我,这个让国王彻底困惑了。由于他不像你那样聪明,我就让 Ruben 和 Scarlet 花很长时间给他解释变量的概念,同时我给你解释一下。
一个 Ruby 变量只是一个名字(没有引号!),你可以将其赋给一个值(这是某种信息,比如构成国王字符串的单词)。一种值是字符串;另一种值是数字,你已经见过当 Ruby 告诉你国王的字符串长度是 40 时使用的数字。
你可以像这样创建一个变量:
>> **kings_string = 'A string fit for a king'**
>> **wiener_dog_weight = 22**
等号告诉 Ruby,“嘿!把右边的这个值保存到左边的这个名字下。”这意味着你以后可以输入变量名,直接得到对应的值:
>> **wiener_dog_weight**
=> 22
这在你试图追踪你那只走失的小宠物(我们叫它 Smalls)以及它不断变化的体重时,可能会派上用场:
>> **smalls_weight = 22**
=> 22
>> **pounds_lost = 4**
=> 4
>> **smalls_new_weight = smalls_weight - pounds_lost**
=> 18

不用担心 22 和 4 被重复返回给你;Ruby 只是想帮忙。Ruby 总是期望变量名在左边,值在右边,所以一定要小心不要弄错顺序!
你还会注意到我在变量名中使用了 _(称为下划线)而不是空格。Ruby 不允许名字中有空格,因此最好使用 _ 来代替。
听起来国王还在适应字符串(想象一下我把耳朵贴在他书房的厚橡木门上),所以我会再给你介绍 Ruby 的另一点魔法。当你看到像这样的代码时:
>> **'Property of His Royal Highness, the King'.reverse**
它意味着你在调用字符串上的 reverse 方法。当我们说我们在“调用一个方法”时,我们的意思是我们在请求 Ruby 执行一个命令:“嘿,Ruby!请反转这个字符串!”我稍后会详细讲解方法,但现在你可以把它们看作是作用于特定 Ruby 对象的命令。例如,字符串可以被反转,但数字不行:
>> **"18".reverse**
=> "81"
>> **18.reverse**
=> NoMethodError: undefined method `reverse' for 18:Fixnum
NoMethodError!?那是 Ruby 在说,“哇,哇,哇。我知道如何对字符串执行 reverse,但是我不知道如何对数字执行 reverse!”随着练习,你会了解到哪些方法适用于哪些 Ruby 对象。以作者的名誉保证(我从来不是童子军)。

Ruby 运算符
“让我看看我理解对了没有,”国王说。“变量是 Ruby 值的名字,比如字符串和数字。它们没有引号,也不能有空格。我可以用等号将一个变量赋值给一个值,然后我可以用变量的名字来取回这个值。”
“完全正确,”鲁本说。
“当我看到一个对象后面跟着一个点,再跟着一个命令时,那意味着我正在对这个对象使用这个命令,”国王说。
“正是如此,”斯卡雷特说。
“你提到过我不能对一个数字使用reverse,”国王说。“这很有道理。那么,我能对数字做些什么呢?”
“各种各样的操作,”鲁本说。他推开斯卡雷特,走到计算设备前打字:
>> **100 + 17**
=> 117
>> **50 - 20**
=> 30
>> **10 * 10**
=> 100
>> **40 / 20**
=> 2
“是的,是的,”国王说,“我可以用 + 把它们加在一起,用 - 减去,用 * 乘以,用 / 除以。”
“你们可能见过 ÷ 作为除法符号,”鲁本继续说,“但是在代码中我们可以直接使用 /。比如,4 ÷ 2 会变成 4 / 2。”
“但是我能做些什么有趣的事情呢?”国王抱怨道。
“那这个呢?”鲁本问道,一边继续打字。
>> **22.next**
=> 23
>> **22.pred**
=> 21
“啊哈!”国王说,“现在你说到点子上了。next 一定是告诉 Ruby 计算 下一个 数字,而 pred 是请求 Ruby 返回它的 前驱,即紧跟在它之前的数字。”
“正如雨水一样,”鲁本说。
“下雨!”国王惊呼道,猛地跳起,强劲到把他那把纯金的椅子给撞翻了。他以一个对这个年纪来说几乎不可能的速度跑出房间,鲁本和斯卡雷特紧跟其后。
在宫殿里穿行了好几分钟,经过一片混乱的内容(毕竟国王把整个宫殿弄得颠倒了),鲁本和斯卡雷特在国王的主浴室赶上了他。他又开始哭泣,不过这次是出于喜悦,而他手中紧紧握着的正是——他的字符串!
“下雨提醒我,我在吃完焦干燕麦早餐后洗了个清爽的澡!”国王呜咽着说。“然后它就在这里,挂着晾干,正如我离开时那样。我真是无以为报!”

“小心!”斯卡雷特说,“你的字符串还是有点湿,看看上面滑动的珠子。”
国王大声吸了吸鼻子,检查了他的字符串,结果字符串上的字符果然在四处滑动。国王想了想,然后将字符串的两端打了双结,以防字符滑掉:
"Property of His Royal Highness, the King"
“双引号!”斯卡雷特说,“你能在 Ruby 字符串中使用它们吗?”
“当然,”鲁本说,“单引号和双引号字符串几乎完全相同。”他打开国王的药柜,露出一台稍微不那么旧的计算设备,然后打出以下内容:
>> **double_quotes = "A string's the thing"**
=> "A string's the thing"
>> **single_quotes = 'for a springly King'**
=> "for a springly King"
“看见了吗?”鲁本说。“即使我们输入单引号,Ruby 也会回显双引号。两者都可以用!”
“虽然我听说过,”国王说,“你可以在双引号字符串中放入比单引号字符串更复杂的元素和小饰品。”
“那是真的,”鲁本说,“但我们会在适当的时候讲到那个。”然后他用金镶边的咔哒一声关上了国王的药箱。
你的小项目
现在你对字符串、数字和变量有了一些了解,让我们做一个小项目:编写一个程序,反射并回显国王的字符串。反射 是指将某物倒过来,所以你可能已经猜到我们会对一些字符串进行 reverse 操作。另一方面,回显 是指将某物重复几次,我们很快就会看到一种快速简便地重复字符串的方法。你会为它的简洁与易用感动得泪流满面,甚至会把这本书的页面撕下来擦干眼泪。
注意
对于一些较长的代码示例,我们将编写 Ruby 脚本,而不是使用 IRB!当你看到代码上方有文件名并用斜体显示时,比如 kings_string.rb 用于下一个示例,这意味着你可以将代码写入一个文件,文件名按给定名称,并使用 ruby 命令运行它。如果你不记得怎么做,或者需要帮助,回头看看第一章,或者请最近的成年人帮忙。你可以在 nostarch.com/rubywizardry/ 下载本书中所有的脚本。(但记住,如果你在学习编程,尽量自己敲代码,而不是仅仅阅读和运行代码!)*
接下来创建一个新文件,名为 kings_string.rb。然后,打开文件并输入以下内容。我们将编写一个简短的程序,展示通过赋值变量可以做的酷事,并展示 Ruby 如何处理字符串。
kings_string.rb
kings_string = "Property of His Royal Highness, the King"
string_reflection = kings_string.reverse
times_to_echo = 3
string_echo = kings_string * times_to_echo
puts kings_string
puts string_reflection
puts string_echo
前四行是在赋值变量。你可以通过等号看出来。
特别是第二行,非常酷:它定义了一个变量来保存 kings_string,但因为 reverse 方法会将字符串倒过来,所以 string_reflection 实际上会是 "gniK eht ,ssenhgiH layoR siH fo ytreporP"!
你可能还在想第四行代码是怎么回事:
string_echo = kings_string * times_to_echo
你想得对!* 是 Ruby 中表示“乘以”的符号。这意味着 2 * 2 等于 4,13 * 379 等于 4,927,依此类推。但是等等!你可能会进一步问,怎么能将一个字符串(它只是一些字母)与数字相乘呢? 答案是 Ruby 是个非常聪明的机器人。当它看到类似这样的内容时:
>> **"Hello!" * 3**
它是这样做的:
=> "Hello!Hello!Hello!"
所以这就是我们如何生成回显:kings_string * times_to_echo 会变成 "Property of His Royal Highness, the King" 重复三次!
puts 是 “put string” 的缩写,就像是“把那个字符串放到我能看到的地方”。正如我们所见,它只是将文本打印到屏幕上。你觉得运行程序时会看到什么?保存并关闭文件,然后使用 ruby kings_string.rb 运行它。你应该会看到如下输出:
Property of His Royal Highness, the King
gniK eht ,ssenhgiH layoR siH fo ytreporP
Property of His Royal Highness, the KingProperty of His Royal
Highness, the KingProperty of His Royal Highness, the King
干得好!
你知道这个!
让我们花点时间回顾一下你在过去几页中已经掌握的内容。
我们谈到了字符串,它们只是用引号括起来的单词或短语(单引号或双引号都可以)。实际上,由于组成字符串的元素不一定只能是字母——它们可以包括标点符号甚至数字,只要整个字符串都在引号之间——所以我们说字符串由字符组成,而不是字母。你可以把字符串看作是由字符组成的字面字符串,每个端点都用单引号或双引号系住。(你可以选择单引号或双引号,但两端必须匹配:"string' 或 'string" 是不行的!)
你还看到字符串有一些方便的方法,比如 length 和 reverse,这些方法是 Ruby 知道如何与字符串一起使用的命令。你总是先写下你想要操作的对象,后面跟一个点,再后面是命令,就像这样:
"gadzooks".length
我们稍微谈了一下关于数字的内容,这些数字是 Ruby 中的值,它们的工作方式和你想象的现实生活中的数字完全一样。数字有自己独特的方法,包括 next(用于进入下一个数字)和 pred(用于进入前一个数字):
>> **4.next**
=> 5
最后,我们谈到了变量,以及如何使用它们为 Ruby 的值起特殊的名字,例如 42 或 "chunky bacon"。你总是将变量名(不能包含空格)写在左边,后面跟一个等号,再后面是值:
>> **bacon_consistency = "chunky"**
=> "chunky"
>> **number_of_bacon_strips = 3**
=> 3
你只需输入名称,就可以拿回这个值:
>> **bacon_consistency**
=> "chunky"
根据你所知道的,如何在我们之前处理的那个小项目中进一步推进?例如,如果我们用 next 或 pred 改变了 times_to_echo 的数值会怎样?如果我们在存储在 kings_string 中的句子末尾加一个空格会发生什么?(提示:这可能会让我们的输出看起来更好。但不要直接在变量名 kings_string 上加空格——记住,Ruby 变量名不能有空格!)如果我们尝试用 + 把几个不同的字符串拼接在一起,而不是用数字乘它们,会发生什么?至于早餐中的“chunky bacon”,到底是什么东西?
第三章 管道梦想
学徒水管工的困境
国王、斯嘉丽和鲁本从皇家浴室返回,国王高兴地把他的字符串像一只大胡子猫一样挥舞着。
“为了一个淋浴里的字符串搞得这么复杂!”斯嘉丽对国王说。“希望你现在感觉好些了。”
“做很多事,”国王说着,转动着他字符串上的珠子和小饰品。
“说到水管,”鲁本说,“你听到那个声音了吗?”他们转过拐角重新进入国王的书房时,发现自己已经脚踝深陷在一个迷你湖中。到处都是水,水啊水!
“神秘管道!”国王喊道。“看!”他指着正在剧烈晃动并从狭窄顶部喷涌出大量水的神秘管道。

“看看这个 Flowmatic 某某东西!”国王说。
“这不是很形象,”鲁本说。
“不,那就是它的名字,”国王说。“Flowmatic 某某东西™。”
“找到了!”斯嘉丽说,抓起一个背面标有“陛下的 Flowmatic 某某东西™”的方形金属盒子。她撬开 Flowmatic 某某东西的盖子,发现里面有一个微型计算装置,上面显示着发光的>> IRB 提示符。
“我该做什么?”斯嘉丽问国王。
“我好像记得这个程序使用了flowmatic_on变量,”国王说。“试着关闭它。”他停顿了一下。“嘿!我想起了我们学过的关于变量的知识!”
斯嘉丽给国王竖起大拇指,输入命令并按下 ENTER:
>> **flowmatic_on = false**
=> false
神秘管道颤抖了一下,冒出了几声喷溅声,水流停止了。
“呼!”鲁本说。“干得好!”他从斯嘉丽肩膀上探头看着屏幕。“你是怎么做到的?false是什么?它不可能是字符串,那里没有引号。它也是一个变量吗?”
“不行!”斯嘉丽说。“但它像数字、字符串和变量一样内置在 Ruby 里。它叫做布尔值,其实有两个:true和false。看起来神秘管道在flowmatic_on为true时工作,false时停止。”
“那么,flowmatic_on true之前是怎么回事?”鲁本问道。
“我不知道!”斯嘉丽说。“一定是某个人或某物创建了那个变量。”
“嗯,水不再漏了,”国王说,“但是问题并没有完全解决。它应该在flowmatic_on为true时也能正常工作!毕竟,Flowmatic 负责向城堡提供所有水源;没有它,就没有皇家浴室、皇家刷牙,甚至没有皇家水气球大战!我们需要让神秘管道和它的 Flowmatic 保持开启状态,而且不漏水。”
“那这个呢?”鲁本指着计算装置上 Flowmatic 的开/关控制下方的一行说。
Warning! flow_rate is above 50!
“水一定流进神秘管道太快了,”斯嘉丽说。
“天哪!”国王说。“流量一定得超过 50!”
“我们该怎么办?”鲁本问道。
国王想了想。“我认为我们最好做些在这种情况下总该做的事情,”他说。“我们应该请专业人士来处理。这个情况下,是皇家管道工!”
编写和运行 Ruby 脚本
当国王去叫皇家管道工时,我趁机再给你讲讲 Ruby 的一些魔法。别担心,不会占用你太多时间。

你看,其实你并不总是需要在 IRB 中逐行输入命令。正如在第一章中提到的,你可以写一大段 Ruby 代码并将其保存为 Ruby 脚本。然后,你可以在 IRB 中运行这个 Ruby 脚本!(这就像在终端中使用 ruby 命令运行你的代码一样,就像我们在第一章中所做的那样,只是 IRB 会一直保持开启。)只需在包含 Ruby 脚本的文件夹中启动 IRB,然后输入 load 'filename.rb'。这和将文件中的所有内容逐行输入到 IRB 中完全相同——但这样做便于修改和重新尝试!
我们来试试这个小例子。将以下代码输入到你最喜欢的文本编辑器中,并保存为名为 flow.rb 的文件。(如果你需要提醒如何操作,可以回顾一下第一章,不用担心——我们会在短短的时间内讲解新的 #{} 语法。)
flow.rb
flow_rate = 100
puts "The flow rate is currently #{flow_rate}."
flow_rate = 100 / 2
puts "Now the flow rate is #{flow_rate}!"
如果你打开 IRB,输入 load 'flow.rb',然后按回车键,你应该会看到:
>> **load 'flow.rb'**
The flow rate is currently 100.
Now the flow rate is 50!
=> true
让我们逐行解析一下。
首先,load 'flow.rb'(你在这里使用单引号或双引号都可以)告诉 Ruby 查找当前目录下的一个名为 flow.rb 的文件(目录只是你计算机中文件夹的另一个名称)。如果 Ruby 找到 flow.rb 且文件中的代码没有问题,Ruby 会像你逐行输入代码到 IRB 一样运行它。接下来,你知道 flow_rate = 100 和 puts 是做什么的:前者将 flow_rate 变量设置为值 100,而 puts 打印出你给它的字符串。(你还会看到一个额外的 => true,这表示加载文件成功。)但你可能想知道:这个看起来很奇怪的 #{flow_rate} 是什么?
好吧,字符串和变量是不同的东西,但有时你可能想把它们结合起来——比如,打印出一个显示不同 flow_rate 变量值的消息。Ruby 让我们可以使用 #{} 来直接把变量的值插入到字符串中,而不必每次想用它时都查找变量值并手动输入。所以,当你有以下代码时:
flow_rate = 100
puts "The flow rate is currently #{flow_rate}."
你得到:
The flow rate is currently 100.
还有一件事:记得在第二章中,鲁本说过带双引号(")的字符串和带单引号(')的字符串略有不同吗?嗯,#{}魔法(如果你想显得很高大上,这叫做字符串插值)只能在双引号字符串中使用;单引号字符串无法实现这一点。(这正是国王在第二章中所说的,你可以在双引号字符串上做比在单引号字符串上更多复杂操作的意思。)
这就是我想给你们看的所有内容。顺便说一句,提到国王……
他的威严的流量控制
“喂?”国王说。(他已经等待了一段时间。)“这是皇家水管工吗?”
“咳!咳!咳!”皇家水管工说。

“哎呀,”国王说。“听起来皇家水管工好像得了重度的咳嗽症。”
“咳?”斯嘉丽说。
“咳!”皇家水管工说。
“有点像感冒,但更咳嗽和喘不过气,”国王说。“皇家水管工,能派你的学徒下来帮我们处理那个神秘的管道吗?它已经溢出来好久了。”
“咳!”她说,挂了电话。
“我想这是个肯定的回答,”国王说。
“我也这么觉得,”鲁本说。“看起来学徒已经在这里了!”
皇家水管工的学徒提着一个大红色工具箱走进国王的书房。鲁本和斯嘉丽发现他戴着深色的矩形太阳镜和浓密的黑胡子,难以读出他的表情。哈尔多的名字用红色线绣在他的工作服前面。
“哈尔多!”国王说。
“就是我,”哈尔多说。“我听说那个神秘的管道坏了。”
“当然,”斯嘉丽说。“你能帮我们修好吗?”

“我想是的,”哈尔多说,“但我只是个学徒,所以可能需要一些时间。我们来看看吧。”他说着走到 Flowmatic 某某机器前,盯着屏幕看了片刻。“我好像记得这里面有个instructions.rb文件。”他输入了load 'instructions.rb',然后出现了以下内容:
|~~ |~~
| |
:$: HIS MAJESTY'S FLOWMATIC SOMETHING-OR-OTHER :$:
`'''''''''''''''''''''''''''''''''''''''''''''`
~= Instructions =~
1\. Water should flow if flowmatic_on is true and
water_available is true.
2\. If flowmatic_on is false, the message
"Flowmatic is off!" should appear.
3\. If water_available is false, the message
"No water!" should appear.
4\. If the flow_rate is above 50, the warning
"Warning! flow_rate is above 50!" should
appear, along with the current flow rate.
5\. If the flow_rate is below 50, the warning
"Warning! flow_rate is below 50!" should
appear, along with the current flow rate.
=> true
“哈!”鲁本说。“问题是,如果流量太高或太低,我们只能收到消息。Ruby 不会自动修正流量,因此我们可能会遭遇洪水。”
“我们能修好这个!”斯嘉丽说。“我们写一个 Ruby 程序来检查流量。如果流量太高,我们就降低它,如果流量太低,我们就增加它!”
哈尔多挠了挠头。“嗯,问题是,”他说。“我想我知道我们需要做什么,但我还没学够 Ruby,无法输入正确的命令。不过,如果你们小孩能帮我一把,我觉得我们就能搞定了。”
“没问题,”鲁本说,“让一个 Ruby 程序根据不同条件执行不同操作是斯嘉丽和我非常熟悉的事情。”
“这叫做 控制流,”斯嘉丽说,“其实一点也不难。看看吧!”她在计算装置上的文本编辑器中打开了一个新文件,将其保存为 flowmatic_rules.rb,并输入了以下内容:
flowmatic_on = true
water_available = true
if flowmatic_on && water_available
flow_rate = 50
end
“我完全不懂了,”国王说。
“我们慢慢来,”斯嘉丽说,“首先,我们将 flowmatic_on 和 water_available 这两个变量赋值为 true。然后,在第二行,我们有 if,它是一个 条件语句。它的意思是:如果后面的代码是 true,那么在 end 之前的所有代码都会被执行。”
“&& 只是 Ruby 表达 and 的方式,”鲁本说,“我们已经知道第四行将流量设置为 50,所以整个意思是:‘如果 flowmatic_on 为 true 且 water_available 也为 true,这个程序就会将 flow_rate 变量设为 50。end 只是告诉 Ruby,如果我们没有将流量设置为 50,就什么都不做——至少现在不做。”
“我明白了,”哈尔多说,“这只是第一条指令!干得好。不过如果 Flowmatic 没有开启,或者没有水可用会怎么样?”
“嗯,暂时什么都没有,”鲁本说,“不过我们可以解决这个问题。”他伸手过去,在文本编辑器中修改了flowmatic_rules.rb代码:
flowmatic_on = true
water_available = true
➊ if flowmatic_on && water_available
flow_rate = 50
➋ elsif !flowmatic_on
puts "Flowmatic is off!"
➌ else
puts "No water!"
end
“我想我开始明白了,”国王说,“➊ 就是我们之前所做的。然后在 ➋ 处,我们尝试了一些新的东西:elsif!elsif是不是意味着‘如果第一部分没有执行,就试试下一步’?”
“正是这样,”斯嘉丽说,“别担心奇怪的拼写!那只是‘else, if’的简写方式。而 ! 只是 Ruby 表达 not 的方式。所以如果 flowmatic_on 是 false,那么 !flowmatic_on 就是 true,反之亦然。”
“而且既然只剩下一个条件——如果 Flowmatic 开启 了但没有水——程序就在 ➌ 处用 else 输出‘没有水!’的信息,这意味着:‘如果没有运行其他代码,那就运行后面的代码,’”鲁本说。

“而且这一切后面都会跟着一个 end,就像之前一样,”斯嘉丽说。
“你需要在 if、elsif 和 else 后面的行前加两个空格吗?”国王问。
“缩进?”斯嘉丽问。“不是的,但看起来确实挺不错。”
“这就处理了前面的三条指令!”哈尔多说,“我想我已经掌握了。看看我能不能用 Ruby 重写最后两条指令。”他在自己的 flowmatic_rules.rb 脚本中添加了以下几行:
➍ if flow_rate > 50
puts "Warning! flow_rate is above 50! It's #{flow_rate}."
flow_rate = 50
puts "The flow_rate's been reset to #{flow_rate}."
➎ elsif flow_rate < 50
puts "Warning! flow_rate is below 50! It's #{flow_rate}."
flow_rate = 50
puts "The flow_rate's been reset to #{flow_rate}."
➏ else
puts "The flow_rate is #{flow_rate} (thank goodness)."
end
“好吧,这个我明白了,”国王说。“>表示大于,<表示小于,所以➍处的第一部分表示:如果流量大于 50,我们显示一个‘太高’的警告,然后将变量flow_rate赋值为 50。程序接着使用字符串插值输出新的flow_rate值,就像我们之前看到的那样。”
“但是在➎处,程序检查flow_rate是否小于 50。如果是的话,我们会显示一个‘太低’的警告,并将其重置为 50。
“在➏处,我们有else。如果flow_rate既不大于 50 也不小于 50,那意味着它恰好是 50。所以,我们只需显示流量值,而不更改变量,并用puts输出它(谢天谢地)。”国王微笑着,显然为自己感到高兴。
“完美!”Ruben 说。“你还可以使用<=表示小于或等于,>=表示大于或等于,但我觉得我们现在还不需要这些。”
用更复杂的逻辑运算符改进 flow_rate.rb
Ruben 研究了屏幕片刻。“你知道,”他说,“我觉得你可以用更少的代码来替换从➍到➏的部分。看看这个!”
if flow_rate < 50 || flow_rate > 50
puts "Warning! flow_rate is not 50! It's #{flow_rate}."
flow_rate = 50
puts "The flow_rate's been reset to #{flow_rate}."
else
puts "The flow_rate is #{flow_rate} (thank goodness)."
end
“那两个竖线是什么意思?”Haldo 问。“我以前没见过这些。”
“就像&&表示与,!表示不是,||表示或,”Scarlet 说。“所以我们的意思是,‘如果流量小于 50或大于 50,显示警告并重置为 50;否则,直接告诉我们它是 50(谢天谢地)。’
“效果相当不错,”她接着说,“不过我们可以做得更简洁。”
if flow_rate != 50
puts "Warning! flow_rate is not 50! It's #{flow_rate}."
flow_rate = 50
puts "The flow_rate's been reset to #{flow_rate}."
else
puts "The flow_rate is #{flow_rate} (thank goodness)."
end
“我知道!表示不是,”国王说,“那么可以猜测!=表示不等于吗?”
“不仅是合理的,还是正确的!”Ruben 说。“你可以用!=表示不等于,用==表示等于。但要非常小心,不要混淆=和==。第一个是用来给变量赋值的,而第二个是用来检查两个事物是否相等的。”
“这太神奇了,”Haldo 说。“我觉得我真的掌握了 Ruby 的控制流了。还有什么我应该知道的吗?”
“还有一件事,”Scarlet 说。“因为程序中经常出现if后跟负条件的情况,Ruby 想出了另一种写法。你不必每次都输入类似以下内容:
if flow_rate != 50
puts "Warning! flow_rate is not 50! It's #{flow_rate}."
end
你可以改为输入unless:
unless flow_rate == 50
puts "Warning! flow_rate is not 50! It's #{flow_rate}."
end
“这两个例子完全一样,”Scarlet 结束道。“但如果你有elsif和else,有时候直接使用if看起来会更美观。”
当 Scarlet 在说话时,Haldo 保存了他们完成的flowmatic_rules.rb文件,并在 IRB 提示符下输入了load 'flowmatic_rules.rb'。当他按下 ENTER 键时,神秘的管道震动了一下,随后开始轻微地震动。Ruben 和 Scarlet 听到了水流穿过城堡墙壁的声音,哪里也没有水溢出。
“好极了!”国王说。“我真是太感谢你们了!不过我还是有个疑问,”他接着说,“最初是怎么把流量设置为 100 的?”
“这个我不太确定,”哈尔多说。“可能是城堡里有另一个 Ruby 程序,访问了flow_rate变量并改变了它。”他翻开了自己的红色工具箱,拿出了一个手电筒。“我马上调查一下,”他说。
“你不打算摘掉太阳镜吗?”斯卡利特问道。
“不需要,”哈尔多说,然后他打开了与神秘管道同一侧墙上的一扇小门,消失在城堡的深处,边走边吹着口哨。
一个更大的项目给你
在过去的几页中,你已经学到了很多,现在是时候将你新学到的知识付诸实践了!(别担心,我对你充满信心。) 哈尔多——现在是由于鲁本和斯卡利特的帮助,晋升为皇家水管工的高级学徒——需要你的帮助。虽然他还没找到神秘管道溢出的具体原因,但他曾短暂地进入过一个小而复杂的迷宫。他请求你记录他在迷宫中的冒险经历,因此让我们从创建一个名为 maze.rb 的新文件开始。(如果你不记得怎么做,看看第一章,或者向附近的成年人求助。)在你的文件中输入以下内容。
maze.rb
puts "Holy giraffes! You fell into a maze!"
print "Where to? (N, E, S, W): "
direction = gets.chomp
puts "#{direction}, you say? A fine choice!"
if direction == "N"
puts "You are in a maze of twisty little passages, all alike."
elsif direction == "E"
puts "An elf! And his pet ham!"
elsif direction == "S"
puts "A minotaur! Wait, no, that's just your reflection."
elsif direction == "W"
puts "You're here, wherever here is."
else
puts "Wait, is that even a direction?"
end
通过在终端输入ruby maze.rb并按下回车键来运行程序。你应该看到像这样的输出(尽管你的输出会根据你选择的方向有所不同):
Holy giraffes! You fell into a maze!
Where to? (N, E, S, W): **E**
E, you say? A fine choice!
An elf! And his pet ham!
print命令是新的,但别担心:它几乎和puts一样,唯一的区别是它打印文本后不会自动添加一个新的空行。
这部分也是新的:
direction = gets.chomp
我们在做的是设置一个变量direction,它等于调用gets方法后再调用chomp方法。这是一种说法,意味着我们正在“吃掉”gets的输入。gets是一个内建方法(你可以把它当作一个 Ruby 命令),它获取用户刚刚输入的内容;chomp则去除输入内容末尾的多余部分,比如空格或空行。这意味着我们现在已经把用户输入的内容(来自gets.chomp)存储在了direction变量中。
之后的一切就很顺利了!你已经看过#{}的字符串插值,现在的所有内容只是检查用户输入的字母是否与==(相等)比较,并使用if、elsif和else来控制用户看到的消息。
你可以通过从命令行输入ruby maze.rb来测试你的迷宫程序,或者在启动 IRB 后输入load 'maze.rb'。你可以不断重新运行它,并尝试不同的输入,看看每次会发生什么!
你还可以走得更远一点。(别担心,这真的是一个非常小的迷宫。)这里有一些想法:
-
你如何添加更多的方向,比如 NW、SW、NE、SE、上或下?
-
你如何处理接受小写字母作为方向的输入?
-
圆形有 360 度,向右转等于转 90 度。如果你想让用户输入一个数字,让他们转动相应的度数怎么办?你可以如何使用
<、<=、>、>=、==或!=来实现这个功能?(这有点超出了我们之前讨论的内容,但你可以做到!如果你不是冒险类型的人,你也不会在城堡下面的迷宫里徘徊。)
你知道这个!
控制流是个复杂的东西,但做完那个较大的项目证明你已经掌握了它。让我们回顾一下在过程中学到的一些东西。
我们讨论了布尔值,它可以是true或false。它们和字符串、数字、变量一样是 Ruby 的一部分,但它们绝对不是字符串!不要给它们加上引号,否则它们就不能正常工作。
我们介绍了脚本,以及如何在 IRB 中使用load 'script_name.rb'运行它们。(你也可以完全不在 IRB 中运行 Ruby 程序,只需在命令行输入ruby script_name.rb。)记住:如果你在 IRB 中加载文件,需要加引号,但如果你在命令行输入,就不需要引号!(计算机很笨,且非常挑剔。)
我们解释了字符串插值,使用#{},以及如何将变量的值直接插入到字符串中。这在很多情况下都很有用,记住:你只能在双引号(")字符串中使用字符串插值,它在单引号(')字符串中不起作用!
最后,我们学习了如何使用if、elsif、else和unless进行控制流,并了解如何将这些与逻辑运算符&&(和)、!(非)和||(或),以及比较运算符<(小于)、>(大于)、<=(小于或等于)、>=(大于或等于)、==(等于)和!=(不等于)结合使用。将这些一起使用,我们可以看到(例如)如果某件事和另一件事是true,判断如果某件事小于另一件事,或者说我们应该做某件事除非某事不等于其他事。(哇!)
难以置信,但这几乎就是计算机程序所做的一切:比较值并测试什么是真的,什么是不是真的。让我们花点时间放松一下,享受一下所有 Ruby 知识的光辉吧。(下一章会让你有点迷惑。)
第四章 留在环线上
红宝石单轨列车
“好了,”国王说道,“这一场冒险让我饿得像个伐木工。自从早餐吃了些干烤燕麦后,我就什么都没吃了!”
“快到午饭时间了,”鲁本说道,“有什么吃的吗?”
“这里什么都没有,”国王沮丧地说道,“我怕是把皇家厨房和皇家储藏室弄得一团糟了,毕竟我把整个宫殿翻了一遍找我的线,厨师们大概还没有把一切恢复到原来的样子。”
“我们可以出去了!”斯卡雷特说道,“我肯定宫殿外的王国有很多好地方可以吃饭。”
国王用力点了点头,“当然!”他说,“我们坐环线去哈舍里吧。那是我最喜欢的餐厅!”
“什么是环线?”斯卡雷特问道。
“很高兴你问了,”国王一边忙着穿上自己最好的旅行披风和裤子一边说道,“环线就是那种单轨列车,遍布王国各地,能带着我的臣民去任何他们想去的地方。从这里到哈舍里只有几个站!”
“我们不能坐个皇家马车之类的吗?”鲁本问道。
“这样有什么好玩的吗?”国王回答道,“赶快吧——下一趟环线列车应该马上就会抵达宫殿外面。”
国王、斯卡雷特和鲁本离开了国王的书房,穿过一个又一个走廊,绕过忙碌的厨师、女佣、管家、杂工和其他一大批宫殿工作人员,他们正在忙着把国王在疯狂寻找自己的线时翻转的所有东西恢复原状。最终,他们来到了宫殿的大木门前,两个看起来非常强壮的侍卫礼貌地行礼并为三人拉开了门。
“环线的站在哪里?”鲁本问,突然被阳光晃得睁不开眼。
“就在那儿,”国王指着宫殿入口附近一个小山丘顶上的大金属平台说道,“看到那条铁轨了吗?环线列车就是在上面运行的。几分钟后,它就会到达平台,然后驶向王国的东边。”
“真高啊!”斯卡雷特说道,“这样安全吧?”
“绝对安全!”国王说道,“你们很快就知道了。”
走了几分钟后,国王、斯卡雷特和鲁本到达了平台。就在鲁本准备问列车到达需要多长时间时,一辆明亮的红色金属列车飞速驶到平台,车门发出轻柔的嗖的一声开了。“哈哈!我们到了,”国王说道,“全员登车!”
门在他们身后迅速关上,几乎没有任何声音,环线列车便迅速驶离了宫殿站。鲁本四下张望,“这里一个人都没有!”他说,“我们有一辆车!自己一个人!”他在车厢的一侧塑料长椅上伸展开来。
“一个人都没有,”斯卡雷特说道,“连个指挥员都没有。怎么可能?”
“不需要指挥员!”国王说道,“环线是完全自动化的。它完全靠红宝石驱动!”
“在红宝石上铺铁轨?”鲁本说道,“太棒了!”
“我可不这么认为,”斯嘉丽说道。“我们看到没有人盯着的情况下,Flowmatic Something-or-Other 是如何工作的。”
“哦,我觉得没什么好担心的,”国王说道。“环线已经运行了好多年,从来没有出过问题。”
鲁本把鼻子贴在玻璃上。“我们很快就到了!”他说,“看起来环线正不停地朝哈舍里驶去。”
“你是什么意思?”国王问道。
“我们已经错过了两个车站—这太棒了!嗯,也许对那些在其他车站的人不太好,但,你懂的,对我们来说,更多的哈舍里。”
国王的眼睛瞪大了。“如果有人在等车,环线应该在每个车站停靠!”他说,“如果我们跳过任何一个站,肯定是出了问题!”
“没什么好担心的,是吧?”斯嘉丽说道。“我们现在被困在一辆失控的火车上!”
“太棒了!”鲁本说道。

“好了,好了,”国王说道。“如果今天早上的情况有任何指示的话,我相信这里肯定有某个计算装置,我们可以打开它看看发生了什么。”三人很快扫视着火车,寻找隐藏的隔间或神秘设备。不久后,鲁本发现了一个金属网格的方块,旁边有一个小红按钮。当他按下按钮时,金属网格轻微地吱吱作响,滑了起来,露出了 IRB >> 提示符的欢快光芒。
“找到了!”鲁本挥手让斯嘉丽和国王过来。
“太好了!”斯嘉丽说道。“让我们看看能不能找出一个方法来让这个东西停下来。”
“快点!”国王说道。“我们可不想错过我们的站。哈舍里全天供应早餐,但如果你晚到,有时候他们会卖完最好的菜肴。比如哈希!”
斯嘉丽正忙着检查计算装置屏幕上的 Ruby 代码。“哦不!”她说道。“看起来我们陷入了无限循环!”
“真是美味的早餐肉汁!”国王叫道。“那是什么?”
while 循环
“无限循环是一个永远不会结束的 Ruby 指令,”鲁本说道。“在 Ruby 中,循环是一段会反复执行的代码,它会按照指令执行,直到应该停止为止。但如果给它设置一个永远不会发生的停止条件,代码就会永远运行!”
“看看这个,”斯嘉丽说道。“看起来驱动火车的代码永远不会停止运行!”当国王眯起眼睛看屏幕时,他看到了这样的画面。
注意
只需阅读接下来的几个例子——不要在 IRB 中尝试它们。这些小段代码(以灰色显示)只能作为更长程序的一部分运行。
while true
drive_train_forward
end
“我想我听说过这个,”国王说道。“这个环线是一个while 循环,是一段在某个条件为真时会重复执行的代码。但由于这个循环以while true开始,而true总是true,所以这个循环会永远调用drive_train_forward方法!”
“没错,”斯嘉丽说道。“我们需要一种方式告诉环线停下来。”
“那这个怎么样?”鲁本指着一张夹在计算机装置屏幕旁边的泛黄纸张说。国王低下头去读。“‘关于循环及其机制的简短指南’,”他引用道。“这看起来很有前景!”

“这里写着不仅有drive_train_forward方法,还有stop_train方法,它应该能让列车停下来,”鲁本说。“试试这个!”
“没问题!”斯嘉丽说。她迅速修改了计算机装置中的代码:
while true
stop_train
end
当她按下回车键的瞬间,列车发出了一个深沉而悲伤的boooooop声,声音不到一秒钟就消失了,随着声音的渐隐,列车开始减速。没过多久,他们的列车车厢在单轨道上完全停了下来。
“干得好!”鲁本说。
“嗯,你确实让列车停下来了,”国王说。“但是看看窗外。”鲁本和斯嘉丽跑到国王指的地方,朝车厢前方的窗户望去。看到这一幕,他们的心一下子沉了下来。“我们卡在两个站台之间!”斯嘉丽说。“我甚至看不到前方轨道上的下一个站台。”
“我们再看看那本简短指南,”鲁本说。“如果设计循环程序的人内建了drive_train_forward和stop_train方法,也许她还设计了一个方法来判断列车是否在站台上。”
斯嘉丽和鲁本回到计算机装置旁,再次翻阅《关于循环及其机制的简短指南》。与此同时,国王若有所思地说道:“如果这个循环是一个无限循环,为什么列车竟然会为我们停下来?难道它不应该像其他站点那样呼啸而过吗?”
“我不知道,”鲁本说。“但是记得哈尔多说过王国里可能有另一个程序导致神秘管道溢出吗?也许某个地方有代码在运行,它告诉列车为我们停下。”
“也许吧,”国王说,“但是什么代码,为什么?还有,谁写的?这一切越来越奇怪了。”
“我想我找到可以用的东西了,”斯嘉丽说。“这里写着循环程序里也有一个at_stop?方法。如果我们用对了方法调用它,应该能在我们处于两个站点之间时前进,然后在到达站台时停下来!”
“太好了!”鲁本说。“我想我知道怎么做了。”他说着走向计算机装置并开始敲字。
“别忘了给你的while循环加上end,”斯嘉丽说。“就像if/elsif/else一样,循环也需要end。”
“我知道,我知道,”鲁本说。“好了,我想这就能解决了。”
while true
if at_stop?
stop_train
break
else
drive_train_forward
break
end
end
“等一下,”国王说。“那个break是什么意思?”
“那会让while循环立即停止,”鲁本说。“否则,我们就会一直stop_train或drive_train_forward下去!”
“看来我们需要一种方法来修复这个问题,”国王抱怨道。
“我想这段新代码会解决问题,”鲁本说道。他按下了 ENTER,火车开始启动。在不到一分钟的时间里,火车驶入下一个平台并缓缓停下。
“我们成功了!”国王说道,“而且我们到了东部 Bumpspark 车站!The Hashery 离这里只有两个站,下一站就是 New Mixico 平台。”

“太棒了!我们很快就能到达了,”鲁本说道。但是火车只是在东部 Bumpspark 车站停着,车门开着,平台上一个人也没有。国王、斯嘉丽和鲁本尴尬地站了几分钟,直到国王清了清嗓子打破了沉默。
“嗯,”他说,“看起来我们已经弄明白了如何在一个平台上停下火车,但它没有重新启动,似乎出了点问题。我们要不要再看一遍代码?”
“早就想到了,”斯嘉丽说道,“我想我知道问题出在哪里了——在我们的while循环中,我们给 Loop 程序的指令是:如果在车站就停下,如果不在车站就继续前进。嗯,我们现在在一个车站,Loop 正完全按照我们告诉它做的事情——它停下了!我们在循环中从来没有写任何东西告诉火车停下来后如何重新启动。”
“你说得对!”鲁本说道,“我们需要重写程序,也许像这样?”他打字道:
while !at_stop?
drive_train_forward
end
“那个!at_stop?对我来说有点丑,”斯嘉丽说道,“而 Ruby 的目标就是写出漂亮的代码。也许像这样?”她轮到使用计算装置:
until at_stop?
drive_train_forward
end
“就像if有unless,while也有until,”斯嘉丽说道,“这意味着直到我们到达停靠点之前,我们应该继续推动火车前进。”
“那确实看起来好看多了,”国王说道,“但是我们仍然有个问题:我们目前在一个停靠点,所以程序不会让我们继续前进!即使它能前进,我们也只会到下一个车站停下,而程序中没有任何指令告诉火车如何重新启动。”
“你完全正确,”斯嘉丽说道,“我们需要某种方式让 Ruby 从一个停靠点到另一个停靠点,直到线路上没有更多停靠点。鲁本,你看到清单上有什么能告诉火车从一个车站到下一个车站继续前进的东西吗?”
“嗯,”鲁本说道,“这里说 Ruby 的next方法可以在 Loop 程序中用来从一个车站移动到另一个车站,但我不完全确定我们该怎么做。‘Loop 与其机制简明指南’中有一个例子,但里面有一些看起来奇怪的方括号。你以前见过这些吗?”
数组
在斯嘉丽向鲁本解释那些看起来奇怪的括号时,我会先花点时间给你们解释一下它们。(斯嘉丽也可以解释得很好,但我有点着急了。)
鲁本描述的情况看起来是这样的:
["East Bumpspark", "Endertromb Avenue", "New Mixico", "Mal Abochny"]
一堆用方括号([])括起来并由逗号(,)分隔的 Ruby 对象叫做数组。数组基本上就是列表!比如,你可以用数组来创建一个 Ruby 购物清单,如下所示:
grocery_list = ["cheese", "bread", "grapes", "a festive hat for all
occasions"]
你可以将任何东西放入 Ruby 数组中:字符串、数字、布尔值,甚至是其他列表!这是一种方便的方式,将一个变量设置为多个值的集合。我们将在下一章进一步讨论数组,但现在需要知道的重点是,数组可以与非常方便的方法一起使用(这些方法叫做迭代器,不过不用担心现在就记住这个词),让你可以遍历——也就是说,逐个处理——数组中的每个元素。例子是学习的最好方式,现在就试试在 IRB 中运行这段代码,看看结果:
>> **grocery_list = ["cheese", "bread", "grapes", "a festive hat for**
**all occasions"]**
>> **for item in grocery_list**
>> **next if item.length.odd?**
>> **puts item**
>> **end**
这将会输出:
cheese
grapes
=> ["cheese", "bread", "grapes", "a festive hat for all occasions"]
你在最后看到整个数组,因为即使for只会打印你要求的内容,它仍然会返回整个数组,以防你做了什么改变它的操作。(我们并没有做什么。)

next方法是 Ruby 内置的,正如它的名字所示:它立即跳到数组中的下一个项,而不调用其他代码。在这个例子中,由于字符串"bread"的长度是 5,"a festive hat for all occasions"的长度是 31(这两个都是奇数),因此会调用next,这些列表项就不会被打印出来(记住,next会直接跳到列表中的下一个项,跳过end之前的其他代码)。由于"cheese"和"grapes"的长度都是 6——偶数——并且next只有在字母数是奇数时才会被调用,因此会调用puts语句,打印出项名称。
至于你刚才看到的全新for/in部分,我就交给 Scarlet 和 Ruben 来解释了。看起来 Ruben 已经掌握了数组和迭代器的使用,所以让我们看看他在“Loop”程序中正在做的例子。
将数组和循环付诸实践
“我想我明白了,”Ruben 说,“所以数组只是事物的列表——字符串、数字、任何我们喜欢的东西——我们可以将它们设置为一个单独的变量名。如果我们想的话。不仅如此,我们还可以使用循环和迭代器遍历整个数组,这样我们就能对数组中的每一项,或者说元素,做某些操作。”
“没错,”Scarlet 说,“我能看看你在 IRB 中输入的内容吗?”Ruben 点点头,把计算装置的显示屏转向 Scarlet。这是她看到的内容:
stops = ["East Bumpspark", "Endertromb Avenue", "New Mixico", "Mal
Abochny"]
for stop in stops
next if stop.empty?
end
“很好!”Scarlet 说,“但是那个for/in部分是怎么回事?那是像while那样的循环吗?”
“算是吧,”Ruben 说,“基本上,它告诉 Ruby,‘嘿!对于这个数组中的每个元素,执行end之前的指令。所以,在这个例子中,对于stops列表中的每一个停靠点,如果那里没有人等候,就跳到下一个停靠点。’”
“好的,”Scarlet 说,“还有一个问题——我看到你定义了stops变量并将其设置为数组,但我没有看到你在哪里赋值给stop变量。为什么?”
“这只是 Ruby 给你提供的一个很酷的快捷方式。你看,当你遍历数组时,Ruby 会从一个项移动到下一个项,如果你能在处理时为每个项临时起个名字,那会方便很多。由于这个‘临时’变量只在 for 循环内有效,所以你不需要声明它——你只需要写类似 for stop in stops 的代码,Ruby 就知道 stop 会依次取 stops 数组中每个项的值。事实上,你可以给这个变量任何名字,比如 item 或 thingy 或 elf_with_a_pet_ham,但是我觉得 stop 是最有意义的。”
“我也这么觉得,”Scarlet 说。“不过我总觉得那个 for 循环有点奇怪。我已经读了很多 Ruby 代码了,可是我不常看到 for 循环。可是我倒是看到很多这样的!”然后她开始在计算机上打字:
stops = ["East Bumpspark", "Endertromb Avenue", "New Mixico",
"Mal Abochny"]
stops.each do |stop|
next if stop.empty?
end
“哇!”Ruben 说。“那是什么?它跟我的 for 循环做的是不是一样的事?”
“没错!”Scarlet 说。“虽然它只有一点点不同,但看起来更漂亮。我们可以直接在 stops 变量上调用 each 方法,而不是使用 for/in 部分。这样我们就得到了和之前完全一样的代码,只不过它是放在 do 和 end 之间,而不是 for/in 和 end 之间。do/end 这一部分在 Ruby 中其实很常见,它被称为 块。”
“明白了,”Ruben 说,“这很有道理。那 stop 在两个竖线之间是什么意思?它是不是像我 for 循环中的那个‘临时’ stop 变量?”
“没错,”Scarlet 说。“你可以把那些竖线看作是一个小窗口的边界,我们将其沿着数组移动:当我们把窗口覆盖到数组中的每个元素时,stop 就临时被设置为该元素的值。”
“事实上,”她继续说道,“你甚至可以把它写得简短一些。Ruby 允许你使用大括号代替 do/end,而且因为我们在代码块中只有一行代码,用大括号看起来更简洁。”她在 IRB 中输入:
stops = ["East Bumpspark", "Endertromb Avenue", "New Mixico",
"Mal Abochny"]
stops.each { |stop| next if stop.empty? }
“这一切都很迷人,”国王说,“但是代码 能不能 工作?我们能在饿死之前赶到 New Mixico 站,或者——天哪!——Hashery 会不会没了哈希?”
“我想我们都准备好了!”Ruben 说。“我感觉我们在吃过一顿好饭后,还会继续讨论数组和块。准备好了吗,Scarlet?”Scarlet 点了点头,他们在三秒钟倒数后同时按下了 ENTER。Loop 列车开始震动,车门 嗖 一声关上,列车继续开向 Endertromb Avenue 站。三人屏住呼吸,车门打开,列车停了一会儿……然后车门再次滑动关闭!国王开始鼓掌,直到他们离开列车,走下从月台到街道的楼梯,朝着 Hashery 的樱桃红色门走去。
您的项目,如果您选择接受
经过 Loop 权威委员会审查,委员会成员们确认 Loop确实需要一位列车长(即便是仅仅为了照看程序并确保它不再进入无限循环)。令人惊讶的是,没有人自愿担任这个职位,所以我决定自告奋勇让你来担任!这就是我这个人。
操作列车是个大生意,但我认为从小做起是安全的。我们先来做一个程序,用来报告 Loop 是否会停在一个请求的车站,如果会,它会列出所有在请求站之前的站点,乘客就可以知道他们的车站前还要经过多少个站。我们从创建一个名为 loop_the_loop.rb 的新文件开始。(如果你不记得怎么做,可以回去看看第一章,或者请你的当地成人帮忙。)然后打开文件,输入以下代码。
loop_the_loop.rb
➊ we_wanna_ride = true
stops = ["East Bumpspark", "Endertromb Avenue", "New Mixico",
"Mal Abochny"]
➋ while we_wanna_ride
print "Where ya headin', friend?: "
➌ destination = gets.chomp
➍ if stops.include? destination
puts "I know how to get to #{destination}! Here's the station list:"
➎ stops.each do |stop|
puts stop
break if stop == destination
end
else
puts "Sorry, we don't stop at that station. Maybe another time!"
we_wanna_ride = false
end
end
这里有一些新的内容,但没有什么是你不能应付的!
首先,我们设置了几个变量:we_wanna_ride为true,stops设置为一个字符串数组 ➊。接着,我们创建了一个while循环,以we_wanna_ride(它开始时为true)作为条件 ➋。在循环内,我们使用print在屏幕上打印一些文字,并用gets.chomp获取用户的回答 ➌。
include?方法 ➍是新的!它会返回true,如果数组中有一个元素与destination匹配,否则返回false。(这个方法对于快速检查你想要的对象是否在给定数组中非常有用。)
接下来的部分在➎稍微有点复杂:
stops.each do |stop|
puts stop
break if stop == destination
end
你已经见过stops.each do |stop| ... end这一部分,而break if stop == destination部分的作用正如你所猜的那样:它在 Loop 到达乘客想要的目的地站时,跳出循环。不过,在执行这个检查之前,它会先打印出每个元素,所以如果那个站点在数组中,它至少会打印出一个站点。
你可以通过在命令行输入ruby loop_the_loop.rb并按下 ENTER 来测试你的列车程序。你应该会看到类似这样的输出(当然,你可能会选择与我不同的站点):
Where ya headin', friend?: Mal Abochny
I know how to get to Mal Abochny! Here's the station list:
East Bumpspark
Endertromb Avenue
New Mixico
Mal Abochny
Where ya headin', friend?: New Mixico
I know how to get to New Mixico! Here's the station list:
East Bumpspark
Endertromb Avenue
New Mixico
Where ya headin', friend?: Detroit
Sorry, we don't stop at that station. Maybe another time!
你可以通过不同的输入反复运行程序,看看每次输出是如何变化的!
如果你想让程序更加复杂一点,这里有一些其他的想法:
-
现在,程序会一直提示用户输入,只要用户继续输入
stops数组中的车站。你怎么更新程序,让它即使识别到车站,也只运行一次呢? -
你怎么处理接受小写字母作为目的地的问题呢?(提示:这将与上一个项目中你可能采取的某个额外步骤类似。)
-
如果一个乘客正在乘坐相反方向的火车(例如,从 Mal Abochny 到 East Bumpspark)怎么办?你如何更新你的程序使其能在两个方向上都工作?更棘手的是,如果火车路线是一个大圆圈(意味着,如果一个乘客从 East Bumpspark 去 Mal Abochny,那么在 Mal Abochny 后的下一站应该再次是 East Bumpspark)呢?你如何更新你的程序,打印出正确的火车停靠列表,如果乘客想要绕整个圆圈走一圈?
你知道这个!
我在这一章中给了你很多信息,但如果你在读完它后还能驾驭火车,我敢肯定你已经掌握了要领。让我们回顾一下我们所学的内容。
我们讲解了while 循环,它是包含一些代码的循环,位于while和end之间,只要while条件为true,代码就会继续执行。(小心——如果条件永远无法变为false,循环将永远执行下去,造成无限循环!)
我们看到,就像if有unless,while也有until。如果你能写出:
while something_is_the_case
# Do something!
end
然后你也可以写:
until !something_is_the_case
# Do something!
end
我们还看到,当使用循环或迭代器(它只是 Ruby 代码,用于遍历列表中的项目)时,我们可以调用next方法,根据if/elsif/else或unless语句跳过某些元素。
我们简要讨论了数组,它基本上就是 Ruby 列表,以及如何将任何我们想要的东西放入其中。一个数组看起来是这样的:
my_hobbies = ["Ruby", "eating things", "cat videos"]
我们学习了如何使用for 循环或each方法来迭代(或遍历)一个数组,虽然它们的工作方式完全相同,但each方法在 Ruby 中更为常见。
一个for循环是这样的:
# Assuming we have an array called todo_list
for task in todo_list
puts task
end
使用each迭代看起来是这样的:
# Using do/end
todo_list.each do |task|
puts task
end
# Or, using curly brackets
todo_list.each { |task| puts task }
最后,我们学习了一些关于块的内容。Ruby 块就是常规的 Ruby 代码,夹在do/end或花括号({})之间。像each这样的某些方法会接收块,我们将在深入学习 Ruby 的 Hashery 课程后,进一步了解这些内容。
第五章 阳光与炉灶上的哈希
大汉克的哈舍里
“早安,陛下!”哈舍里里深处传来了一个宏亮的声音。
“早上好,大汉克!”国王说道。
“大汉克?”鲁本问,“谁是大汉克?”
一位庞大的男人,光头和卷曲的黑色胡须,从餐馆的后面走了出来。“我就是!”他说。

国王用力地摇了摇汉克的手。“很高兴见到你,汉克!我们一路过来可费了不少劲——环路出了点问题——但我迫不及待想坐下来好好享受一顿你做的最棒的哈希。”
大汉克皱起了眉头,胡须也显得有些下垂。“环路出了问题?”他问,“真希望我能说我很惊讶。这里的事情也有点乱。”
国王倒吸了一口气。“你不会是——”
大汉克点了点头。“我们的炉灶坏了,”他说,“在修好之前,我做不出任何东西:没有鸡蛋,没有早餐肉汁,当然也做不出我闻名的哈希。”
国王瘫坐在哈舍里里一张长长的橡木长凳上。“没有哈希!还有什么比这更糟糕的?”
“这哈希一定非常好,”斯卡雷特说道。
“太棒了!”国王喊道,眼看就要流泪了。“但是没有一个能用的炉灶,我们就做不成任何事。而我们已经走了这么远!”
“等一下,”大汉克说,“这可不是我和吱吱叫的吉姆第一次在哈舍里出问题,也不会是最后一次。我们一定会弄明白的。”
“谁是吱吱叫的吉姆?”鲁本问。
“他是我的炸菜厨师,”汉克说道,“他可不算是厨房技术方面的天才——我们一切都靠红宝石(Ruby)来运行——不过他可是个了不起的厨师。做的煎蛋非常好,他几乎已经掌握了我的哈希食谱。”
“红宝石!”鲁本和斯卡雷特同时喊道。
大汉克抬起了浓黑的眉毛。“你们孩子懂不懂红宝石(Ruby)?”他问道,“如果懂的话,那会大有帮助。”
“当然!”斯卡雷特说道,“带我们去厨房,我们接手。”
“太棒了!”国王说道,“这些孩子可聪明了,汉克,”他补充道,“他们会很快让你的厨房恢复运转。”
大汉克点了点头。“听起来不错!实际上,我让吱吱叫的吉姆在后面用旧煎锅,但我想他需要点红宝石(Ruby)方面的帮助。我会再试试炉灶,但如果你们孩子和吉姆在我修好之前就把订单做好了,告诉我,我们可以一起想办法。”
“没问题,”斯卡雷特说道,“你带路!”
大汉克示意他们跟着他走,摇摇晃晃地穿过一排排木凳,向哈舍里的后方走去。他停在一扇顶部有小窗户的红色金属门前,轻轻敲了两下,肩膀顶着推开了门。“吉姆!”他喊道,“国王和他的朋友们来了!”
他们听到厨房角落里传来一阵短促的摩擦声,接着是一堆锅碗瓢盆撞击的声音。
“没事,没事——我明白了!” 吱吱吉姆喊道,声音颤抖了两次。他从一堆大土豆袋后面跌跌撞撞地走出来,手里拿着一个平底锅,另一个则歪歪斜斜地顶在头上。
“我知道为什么他们叫他吱吱吉姆了,”鲁本低声对斯卡利特说道。
“轻松点,吉姆,”大汉说着,从吉姆手中接过锅具。“国王和他的朋友们对 Ruby 有一些了解,所以我请他们帮你,而我则去调整主灶。”
吱吱吉姆急忙向国王行了个礼。“陛下,”他说。
“吱吱吉姆,”国王说道。

大汉指了指吉姆刚刚从中走出来的厨房角落。“计算装置就在那边,”他说。“我在炉子那边,厨房的另一边。”他转过身,双臂各夹着一袋土豆。“如果需要我,大声喊我——厨房很大,”他头也不回地喊道。说完,他就走了。
吱吱吉姆清了清嗓子。“大汉可能告诉过你,我不是 Ruby 专家,”他尖声说道,“但是如果你能帮我重新启动我的煎锅,我可以像无人能敌一样做好顾客的订单。”他从围裙口袋里拿出一叠订单。“大多数是土豆饼和今天的特别菜单——阳光数组,”他说。“三颗顺序排列的荷包蛋!是王国里最棒的早午餐。”
“好吧,”鲁本说。“我们其实刚刚帮国王解决了循环问题,而且我们还得用了数组。这应该是小菜一碟!”
“鸡蛋,” 吱吱吉姆纠正道。
“哦,是的,一块……鸡蛋,” 鲁本说。
“我们开始工作吧,”斯卡利特说。“计算装置我已经打开了!”
数组中的数组
“太棒了!”吉姆说。“既然你们了解数组,能帮我创建一个吗?第一个订单是一个阳光数组;就是三颗 'sunny_side_up_egg' 按顺序排列。”

“当然!”斯卡利特说。“它应该像这样。”她开始在计算装置上打字:
>> **order_one = ['sunny_side_up_egg', 'sunny_side_up_egg', 'sunny_**
**side_up_egg']**
=> ["sunny_side_up_egg", "sunny_side_up_egg", "sunny_side_up_egg"]
当斯卡利特按下回车键时,炉灶上的小金属轨道开始震动。一个接一个的三个鸡蛋沿轨道滚下来,撞上一个小锤子,摔开并落在炉灶上,做成了荷包蛋。
“太完美了!”吉姆说。“不过看起来要打很多字,而且我们还会有很多订单。”他的声音再次颤抖。“有没有什么办法能用更少的输入做到一样的事?”
“没问题!”斯卡利特说。“你也可以这样创建一个新数组:
>> **order_two = Array.new(3, 'sunny_side_up_egg')**
=> ["sunny_side_up_egg", "sunny_side_up_egg", "sunny_side_up_egg"]
“这里,我们调用了 Array 的 new 方法,它创建了一个项目列表。括号里的下一个部分意味着数组应该包含三个项目,”斯卡利特解释道,“最后一个部分意味着每个项目应该是一个 'sunny_side_up_egg'。这和我们为 order_one 所做的完全一样。”
“我记得在循环中用方括号创建数组,”Ruben 说,“但我从没见过 Array.new。那是做什么的?”
“记得 Ruby 有像 String 这样的数据类型吗?”Scarlet 问道。嗯,Array 是另一种数据类型。你可以使用 数组字面量 语法创建一个数组,方法就是将一个变量名赋值给一个用方括号括起来的列表。你也可以通过在 Array 类 上调用 new 方法来创建一个数组。”
“什么是 Ruby 类?”Squeaky Jim 问道。
“我们稍后会讲到,”Scarlet 说,“但重要的是,类就像 Ruby 中的对象组,调用类名上的 new 方法会创建该类的一个新的 实例,或者说是一个例子。”
“好吧,这样说得通,”Ruben 说,“我们可以把变量放进数组里,之前我们也看到过你可以把字符串放进去。还有什么可以放进数组里?”
“任何东西!”Scarlet 说,“而且数组里的项甚至可以是不同的东西。看看这个!”
>> **random_array = [1, 'two', 'sunny_side_up_egg', true]**
=> [1, "two", "sunny_side_up_egg", true]
“一个数字,一个字符串,一个变量,和一个布尔值,全部在同一个数组里,”Ruben 说,“真棒!”
“太好了,”Squeaky Jim 说,“第一份阳光数组就快好了。但我有种预感,我们可能要做很多这种东西——有没有什么方法可以让我们把所有的订单都放进一个数组里?就像一个列表中的列表?”
“绝对可以,”Ruben 说,Scarlet 让开了,他开始打字:
>> **order_three = ['hash']**
=> ["hash"]
>> **order_four = ['egg', 'hash']**
=> ["egg", "hash"]
>> **todays_orders = [order_one, order_two, order_three, order_four]**
=> [["sunny_side_up_egg", "sunny_side_up_egg", "sunny_side_up_egg"],
["sunny_side_up_egg", "sunny_side_up_egg", "sunny_side_up_egg"],
["hash"], ["egg", "hash"]]
“太棒了!todays_orders 是一个包含四个其他数组的数组:order_one、order_two、order_three 和 order_four,”Squeaky Jim 说,“我们很快就能完成了。不过,如果我们的订单已经打包成一个数组了,怎么再取出来呢?”
更多数组方法!
“我们可以做几件事,”Ruben 说,“数组有很多很酷的内建方法我们可以使用。比如说,我们可以用 first 方法获取数组中的第一个 项 或 元素,就像这样:
>> **todays_orders.first**
=> ["sunny_side_up_egg", "sunny_side_up_egg", "sunny_side_up_egg"]
“我明白了,”Scarlet 说,“first 方法给我们数组中的第一个元素!而且既然我们在谈 first,我们还可以用 last 获取数组中的最后一个元素!”
>> **todays_orders.last**
=> ['egg', 'hash']
“那是 order_four,”Jim 说,“马上就来!”
“不过等等,”Ruben 说,他迅速在计算机装置上打字:
>> **todays_orders.empty?**
=> false
>> **todays_orders.length**
=> 4
“哇,那是什么?”Jim 问道,把他的纸质煎炸厨师帽推回去,挠了挠头。“我以前没见过 empty? 或 length。”
“我们之前在字符串上看到过 length,”Scarlet 说,“当我们对字符串使用这个方法时,它会告诉我们字符串包含了多少个字符。那对数组来说,它会告诉我们数组里有多少项吗?”
“没错,”Ruben 说,“我们在循环列车站看到过 empty?,它只是返回一个布尔值——如果站点没人等车就是 true,如果至少有一个人就是 false。这个 empty? 适用于数组,但它的工作方式完全一样。”
然后 Ruben 皱起了眉头。“但列表里还有四个订单!我们可以用 first 和 last 获取其中一些,但怎么获取剩下的呢?我们在做这些的时候怎么从列表中移除它们?”
移位!弹出!插入!
“我想我可以帮忙,”斯嘉丽说。“不过我们需要用到一些新的数组方法。”她越过鲁本,操作计算机装置,开始打字:
>> **todays_orders**
=> [["sunny_side_up_egg", "sunny_side_up_egg", "sunny_side_up_egg"],
["sunny_side_up_egg", "sunny_side_up_egg", "sunny_side_up_egg"],
["hash"], ["egg", "hash"]]
>> **current_order = todays_orders.shift**
=> ["sunny_side_up_egg", "sunny_side_up_egg", "sunny_side_up_egg"]
>> **todays_orders**
=> [["sunny_side_up_egg", "sunny_side_up_egg", "sunny_side_up_egg"],
["hash"], ["egg", "hash"]]
“太完美了!”鲁本说。“你是怎么把 todays_orders 中的第一个订单提取出来并放入 current_order 变量的?”
“使用 shift 方法,”斯嘉丽说。“它同时做两件事:它从你调用它的数组中删除第一个元素,并且返回,或者说吐出那个元素!”
“所以,如果你把一个新变量设为调用 shift 方法后的结果,”国王插话说,“你基本上是把数组中的一个元素移到新的变量里了!”
鲁本和斯嘉丽转向国王,国王刚刚结束了对厨房里散乱的许多土豆袋的研究。
“那…其实是完全正确的,”斯嘉丽说。
“太棒了!”国王说。“但是如果我想把东西添加到数组的前面呢?或者添加到后面呢?甚至——敢我说——把东西从后面移除呢?”
“那么我们有适合你的方法!”斯嘉丽说。“我不想弄乱订单,所以我会在我自己创建的名为 breakfast_items 的数组上给你展示。看一下!”她在计算机装置上输入了以下内容:
>> **breakfast_items = ['egg', 'hash', 'gravy', 'biscuit', 'sausage',**
**'jam']**
>> **current_food = breakfast_items.shift**
=> egg
>> **breakfast_items**
=> ['hash', 'gravy', 'biscuit', 'sausage', 'jam']
>> **current_food = breakfast_items.pop**
=> jam
>> **breakfast_items**
=> ['hash', 'gravy', 'biscuit', 'sausage']
“天哪!”国王说。“这正是我想要的——pop 删除并返回数组中的最后一个元素,而 shift 对第一个元素做了相同的操作!”他看着斯嘉丽继续输入。
>> **breakfast_items.push('egg')**
=> ['hash', 'gravy', 'biscuit', 'sausage', 'egg']
>> **breakfast_items.unshift('jam')**
=> ['jam', 'hash', 'gravy', 'biscuit', 'sausage', 'egg']
“啊哈!我明白了:push 会把一个元素添加到数组的末尾,而 unshift 会把一个元素添加到数组的开头,”国王继续说道。
“没问题!只要确保你从左到右读数组,”斯嘉丽说。“第一个元素是最左边的那个,最后一个元素是最右边的那个。”
“如果我想把东西添加到中间呢?”国王问。
斯嘉丽什么也没说,只是继续输入:
>> **breakfast_items**
=> ['jam', 'hash', 'gravy', 'biscuit', 'sausage', 'egg']
>> **breakfast_items.insert(2, 'tea')**
=> ['jam', 'hash', 'tea', 'gravy', 'biscuit', 'sausage', 'egg']
“太神奇了!”鲁本说。“但是等一下,为什么 tea 是数组中的第三个元素?”他问。“你调用 insert 方法时传入的是数字 2,而不是数字 3!”
“这是计算机的一大怪异之处,”斯嘉丽说。“它们不像你我那样从 1 开始计数。它们是从零开始的。如果你从 1 开始计数,tea 会是位置 3,但如果从零开始,它会比那个少一个位置。所以如果你希望它成为第三个元素,你必须告诉 Ruby 在位置 2 调用 insert,而不是位置 3。”
“我比塑料花厂里的大黄蜂还要迷惑,”国王沮丧地说。“就在我以为我开始理解 Ruby 的时候。”
“等一下,我想我明白了,”斯奎基·吉姆说,他的声音只轻微沙哑。“这样对吗?”他在一张沾着哈希酱的餐巾纸背面画了个图:

“数组就像一排盒子,”吉姆说。“第一个编号是零,后面的编号依次增大。你可以用shift从前面取东西,用unshift往前面添加东西,用push往后面添加东西,用pop从后面取东西。”他从鲁本看向斯卡雷特,不确定地问:“这样对吗?”
“没错!”鲁本和斯卡雷特异口同声地说。
“做得好,我的孩子!”国王说。“你学得真快。”
“事实上,”斯卡雷特说,“数组就像一排盒子,你甚至可以通过请求数组获取某个盒子编号的元素!看到了吗?”她在计算机装置上输入:
>> **breakfast_items**
=> ['jam', 'hash', 'tea', 'gravy', 'biscuit', 'sausage', 'egg']
>> **breakfast_items[2]**
=> "tea"
“就像是你告诉数组要抓取哪个盒子的编号,”鲁本解释道。“通过说你想要breakfast_items[2],你其实是在告诉 Ruby,你想要数组中第 2 号位置的元素,也就是第三个元素。”
咯吱作响的吉姆微笑着说:“太好了!”他说。“不过我在想……”
“奇怪什么?”鲁本问。
“嗯,”吉姆一边翻着最后一个煎蛋,一边把它放进纸篮里,“从数组中添加和移除东西没问题,我觉得厨房的软件在这方面做得足够好。但如果我想先了解所有订单的信息呢?有没有办法可以遍历所有订单并逐个打印出来?”
遍历数组
“确实,”国王说。“我们在循环里见过这个——叫什么来着?”
“迭代,”鲁本说。“这样做!”他伸手操作计算机装置,开始快速打字:
>> **todays_orders.each do |order|**
>> **puts "#{order}"**
>> **end**
["sunny_side_up_egg", "sunny_side_up_egg", "sunny_side_up_egg"]
["hash"]
["egg", "hash"]
“是的,这看起来很熟悉!”国王说。“这会打印出todays_orders数组中的每一个订单吗?”
“你答对了,”斯卡雷特说。“但是记住,有一种写法比do/end块更简洁。”她迅速输入:
>> **todays_orders.each { |order| puts "#{order}" }**
["sunny_side_up_egg", "sunny_side_up_egg", "sunny_side_up_egg"]
["hash"]
["egg", "hash"]
“没错!”国王说。“当块中只有一行代码时,你可以用大括号代替do/end。”他挠了挠浓密的白胡子。“不过这些块还是让我有点困惑。”
“我们很快会再谈这些!”斯卡雷特说。“现在,我们应该确保在哈舍餐馆里,关于数组和顾客订单的部分都没问题。”
咯吱作响的吉姆点点头。“我想我已经掌握了数组的用法,我们现在对订单的处理也没问题了,”他说,扔掉最后一张订单票。“不过这些关于迭代的讨论让我在想,或许我们还能解决另一个问题。”
“那是什么?”斯卡雷特问。
“嗯,”吉姆吱吱地说,“大汉克和我一直在想,怎样才能最好地把哈舍餐馆的菜单打印出来给顾客。你认为迭代数组可能是个好方法吗?”
“菜单由什么组成?”鲁本问。
“用 Ruby 术语来说,就是字符串和数字,”吉姆说。“每个字符串就是菜单上的一项,每一项都会有一个表示价格的数字。我想既然我们可以在数组中混合使用字符串和数字,那应该没问题。”
“嗯,”Scarlet 说。“我不这么认为。你怎么将菜单项和它们的价格配对呢?即使你只是交替列出它们,每当菜单更改时,你不断地进行推入、弹出、移动和移出操作,也许会搞乱它们。”
“你说得对,”Jim 承认道。“那么,也许是一个数组的数组?每个数组元素可以是它自己的一小段数组,每个小数组就可以包含一个菜单项的名称和它的价格。”
“这稍微好点,”Ruben 说。“至少这样你的菜单名称和价格会在一起。Scarlet,你觉得怎么样?”
Scarlet 想了想,最后说道:“不,我想我们应该用哈希,而不是数组。”
哈希工坊中的哈希
我敢打赌你现在在想:“好吧,我们现在在哈希工坊。哈希已经上桌了。难道 Ruby 有一个内置的东西叫做哈希,这不是个大笑话吧?”

其实不是。完全没有笑点。哈希是 Ruby 中最酷的部分之一,所以在 Scarlet、国王、Ruben 和 Squeaky Jim 弄清楚早餐哈希和 Ruby 哈希之间的区别时,我来花一点时间给你们解释它们。
数组就像是一排排的盒子,对吧?每个元素都有自己编号的槽位,就像购物清单上的物品。这很好,只要清单上每行的所有东西——也就是说,数组中的每个元素——各自独立,做自己的事。但如果你想显示两个元素之间有某种关系怎么办?
想想字典:在字典中,你有一个单词和它的定义。不同于购物清单,你不会说所有的单词都单独列在一行,所有的定义也单独列在另一行,因为那样会忽略字典中最重要的一部分:单词与其含义之间的关系。Squeaky Jim 的订单就像一个列表,而一个订单并不真正影响任何其他订单,所以数组很适合。但对于他的菜单,他需要将菜单项与价格关联起来,因此他需要像字典一样的东西。而 Ruby 用哈希来实现这个功能。
哈希比起解释更容易展示(难道不是所有东西都是这样吗?),所以看一下下面的代码。它将我们的英雄(以及 Squeaky Jim 和 Big Hank)与他们的描述配对。快在 IRB 中输入它,并注意我们使用了花括号({})而不是数组中使用的方括号([]):
>> **our_heroes = {**
>> **:the_king => 'the ruler of the kingdom',**
>> **:ruben => 'a Ruby wizard in training',**
>> **:scarlet => 'a Ruby wizard in training',**
>> **:big_hank => 'the owner of the Hashery',**
>> **:squeaky_jim => 'a fry cook at the Hashery'**
>> **}**
这段代码接收一个变量our_heroes,并将一个哈希值存储在其中。不要被花括号搞混——这不是一个代码块!哈希值不是命令;它们只是我们所说的键值对。一个单词及其定义是键值对的好例子:单词是键,而单词的定义是值。就像在字典中一样,你使用哈希键来查找哈希值。
每一组键值对之间用逗号分隔,这让它们有点像数组。相似之处不止于此!例如,如果您有上面的哈希,您可以这样写:
>> **our_heroes[:the_king]**
然后您会得到:
=> "the ruler of the kingdom"
这有点像查找数组值,只不过您不需要在方括号中提供元素的编号,而是写上哈希键。
不用担心哈希键现在看起来奇怪;那些看起来像变量、前面有冒号的东西叫做符号,我们将在下一章介绍它们。
和数组一样,您可以使用字面语法或 new 方法创建哈希。这两行代码的意思是相同的:
>> **hashery_menu = {}**
=> {}
>> **hashery_menu = Hash.new**
=> {}
有时您会看到另一种写哈希的方式。不是使用小小的哈希箭头(=>),而是有些人在符号名称后加上冒号,这样 our_heroes 哈希看起来就像这样:
>> **our_heroes = {**
>> **the_king: 'the ruler of the kingdom',**
>> **ruben: 'a Ruby wizard in training',**
>> **scarlet: 'a Ruby wizard in training',**
>> **big_hank: 'the owner of the Hashery',**
>> **squeaky_jim: 'a fry cook at the Hashery'**
>> **}**
=> {:the_king=>"the ruler of the kingdom", :ruben=>"a Ruby wizard in
training", :scarlet=>"a Ruby wizard in training", :big_hank=>"the
owner of the Hashery", :squeaky_jim=>"a fry cook at the Hashery"}
这两个例子都是完全正确的 Ruby 代码,您可以选择任何一个,哪个更容易记住就用哪个。(我喜欢冒号,因为它们打起来更快。)
最后,您可以使用一些简洁的方法在哈希中提取键、值或键值对组合。例如,调用哈希的keys方法会返回一个包含键的数组:
>> **our_heroes.keys**
=> [:the_king, :ruben, :scarlet, :big_hank, :squeaky_jim]
您还可以在哈希上调用 values 方法,获取其值的数组:
>> **our_heroes.values**
=> ['the ruler of the kingdom', 'a Ruby wizard in training', 'a Ruby
wizard in training', 'the owner of the Hashery', 'a fry cook at the
Hashery']
还有一些其他值得了解的哈希方法。就像 empty? 可以告诉您数组是否为空一样,empty? 也可以告诉您哈希是否没有键值对:
>> **our_heroes.empty?**
=> false
>> **empty_hash = {}**
>> **empty_hash.empty?**
=> true
您还可以使用 length 方法来查找哈希中有多少组键值对:
>> **our_heroes.length**
=> 5
最后但同样重要的是,您可以使用一些全新的哈希方法 has_key? 和 has_value? 来检查哈希是否包含某个键或值:
>> **our_heroes.has_key?(:ruben)**
=> true
>> **our_heroes.has_key?(:trady_blix)**
=> false
>> **our_heroes.has_value?('a fry cook at the Hashery')**
=> true
然而,您可能想问:“我怎样才能一起获取哈希的所有键和值?”好吧,最好的方法就是遍历哈希。这个过程看起来和遍历数组非常相似——事实上,只有一个小小的不同!
>> **our_heroes.each do |hero, role|**
>> **puts "#{hero} is #{role}."**
>> **end**
继续尝试吧。(确保在 puts 中使用双引号——记得,如果您要在字符串中插入变量,需要使用双引号。)你看到了我提到的那个小差别吗?在管道符号(||)之间,您需要同时放入 hero 和 role。在数组中,我们只需要一个变量放在管道符号之间,但哈希包含键和值,所以我们需要告诉 Ruby 块这两者。如果一切顺利,您将获得一个包含所有勇敢英雄及其人生站位的列表:
the_king is the ruler of the kingdom.
ruben is a Ruby wizard in training.
scarlet is a Ruby wizard in training.
big_hank is the owner of the Hashery.
squeaky_jim is a fry cook at the Hashery.
=> {:the_king=>"the ruler of the kingdom", :ruben=>"a Ruby wizard in
training", :scarlet=>"a Ruby wizard in training", :big_hank=>"the
owner of the Hashery", :squeaky_jim=>"a fry cook at the Hashery"}
说到我们的英雄,听起来 Scarlett 和 Ruben 已经向国王和 Squeaky Jim 解释完了哈希。(我的听力非常敏锐。)让我们看看他们是否已经弄清楚如何使用哈希来遍历 Hashery 菜单。
欢快的范围
“我有一个完美的主意,适用于遍历 Hashery 菜单,” squeaky Jim 说道,这次他的声音一点也不破音。“我们只需要做的是——”
就在那一刻,大汉 Hank 从厨房的远端笨重地走了过来。
“我不想打断你,”他大声说,“但我在使用这个范围时遇到了麻烦。事实上,我只需要解决 Ruby 中一个小细节就能让它工作,但如果我能弄明白,我愿意做猴子的税务律师。你能帮帮我吗?”
“没问题!”斯卡雷特说。“有什么问题?”
“跟我来,”大汉克说,他们穿过了那座巨大的厨房,经过堆满鸡蛋、面粉、土豆和其他食材的台面,经过烤箱、铲子和那些只有三根叉的叉子,直到他们来到厨房另一侧那台闪闪发光的新炉灶前。
“她不是很美吗?”大汉克问。“真希望我能弄明白她的原理。这里是我卡住的地方。”他指着范围控制台上发光的 IRB >> 提示符。它显示了:
>> **current_temperature = (300..400)[0]**
NoMethodError: undefined method `[]' for 300..400:Range
“我听到你说的关于数组的事了,”汉克说,“而范围的使用手册说它会从 300 度加热到 400 度。所以我想我可以用方括号来获得零位置的温度,那应该是 300。”
“哦,我明白问题所在了,”鲁本说。“这个范围并没有使用数组来表示温度!它使用的是范围。”
“范围?”大汉克问。
“这就是 Ruby 给你一堆不同的值紧挨着放在一起的方式,”鲁本说。“范围(ranges)并不像数组那样能做所有的事情,但我们可以很容易地把它们转成数组。看看这个!”他开始输入:
>> **('a'..'f').to_a**
=> ["a", "b", "c", "d", "e", "f"]
>> **('a'...'f').to_a**
=> ["a", "b", "c", "d", "e"]
>> **(1..9).to_a**
=> [1, 2, 3, 4, 5, 6, 7, 8, 9]
>> **(1...9).to_a**
=> [1, 2, 3, 4, 5, 6, 7, 8]
>> **(1..9).first**
=> 1
>> **(1..9).last**
=> 9
“我简直要变成圣诞鹅了!”汉克大喊道。“太神奇了!不过我有几个问题。首先,那个 to_a 是做什么的?”
“to_a 方法将范围转换为数组,”斯卡雷特说。“由于范围的值是紧挨着的,Ruby 能够推测出数组的样子。看到没?它对字母和数字都能工作!”
“不仅如此,”鲁本补充道,“而且一旦范围变成了数组,你就可以像处理任何数组一样对它进行迭代。”
“明白了,”汉克说,转动着他的胡子。“但告诉我这个:为什么有些范围用两个点,有些用三个?”
“那就是你告诉 Ruby 是否包含范围中的最后一个元素的方式,”鲁本说。“两个点意味着‘包含第一个元素,直到最后一个元素,并且范围内的最后一个元素’,而三个点意味着‘包含第一个元素,并且包含直到但不包括范围中的最后一个元素’。”
“听起来有点混乱,”吉姆尖叫道。
“有可能,”斯卡雷特承认道。“这就是为什么我通常只使用两个点的范围。把两个数都放在范围里更有意义。”
“明白了,”大汉克说。“最后一个问题。如果我想要范围中的第一个元素,我可以用 to_a 将它转换成数组,然后用 [0] 抓取第一个元素。但我也可以用你刚刚给我展示的 first 方法吗?”
“当然!”鲁本说,然后开始输入:
>> **current_temperature = (300..400).first**
=> 300
随着一声愉快的哔,范围迅速加热至 300 度。新鲜的香肠味开始弥漫在空气中。
“你们做到了!我真是无法感谢你们这些孩子。”大汉 Hank 笑着,像个欢快的餐厅牛仔一样把哈希菜肴甩到烤架上。“不过,我忍不住觉得有点傻。明明是那么小的事!”
“编程总是给人这种感觉,”Scarlet 说道。“但做得越多,你就越明白,总是一些小问题,而你会变得越来越擅长迅速解决问题。”
“说到快速,”大汉 Hank 说道,“午餐高峰马上就要来了。”他环顾四周,厨房里堆满了未擦过的土豆和未煎的鸡蛋。“怎么样——要不要先吃点东西,顺便帮我一把?”他笑了笑,那副黑色的大胡子在脸上晃动。“当然,吃的全是免费的。国王和他的朋友们想吃什么就吃什么!”
Ruben 和 Scarlet 互相看了看,然后看向国王。国王点了点头。“我们已经走了这么远,”他说。“不如再待一会儿!”
点餐时间!
既然 Hashery 已经恢复到百分之百的状态,大汉 Hank 和吱吱 Jim(现在他对 Ruby 更有信心了,所以吱吱声少了许多)需要你的帮助来准备菜单给顾客。Jim 没来得及告诉我们他的计划,但我敢肯定你能搞定这个。简单得像派……呃,鸡蛋,对吧?
让我们从创建一个名为 hashery_menu.rb 的新文件开始。(如果你不记得如何做,可以回去看看第一章,或者请你身边的大人帮忙。)然后打开你的文件,输入以下代码。
hashery_menu.rb
hashery_menu = {
eggs: 2,
hash: 3,
jam: 1,
sausage: 2,
biscuit: (1..3)
}
hashery_menu.keys.each do |item|
puts "Today we're serving: #{item}!"
end
hashery_menu.each do |item, price|
puts "We've got #{item} for $#{price}. What a deal!"
end
puts "Here's what a biscuit'll run ya, depending on how much butter
you want:"
hashery_menu[:biscuit].to_a.each do |price|
puts "$#{price}"
end
这就是你和 Jim 之前已经整理过的内容,所以这里没有什么新鲜的或令人害怕的东西!
这是你运行代码时会看到的输出,使用命令 ruby hashery_menu.rb:
Today we're serving: eggs!
Today we're serving: hash!
Today we're serving: jam!
Today we're serving: sausage!
Today we're serving: biscuit!
We've got eggs for $2\. What a deal!
We've got hash for $3\. What a deal!
We've got jam for $1\. What a deal!
We've got sausage for $2\. What a deal!
We've got biscuit for $1..3\. What a deal!
Here's what a biscuit'll run ya, depending on how much butter you
want:
$1
$2
$3
但是,确实有几个新的想法组合,我们可以逐一查看它们是如何工作的。看看这里:
hashery_menu = {
eggs: 2,
hash: 3,
jam: 1,
sausage: 2,
biscuit: (1..3)
}
在这里,我们只是创建了一个名为 hashery_menu 的哈希。它有像 :eggs 和 :hash 这样的键,每个键都有一个与之对应的值,比如 :eggs 的值是 2,:hash 的值是 3。这是我们为菜单上的每个项目收取的价格。
接下来是这一部分:
hashery_menu.keys.each do |item|
puts "Today we're serving: #{item}!"
end
我们使用 keys 方法来获取哈希中所有键的列表,然后将这个列表(或者说是 数组)传递给 each 方法。对于每个键,我们会打印出字符串:Today we're serving: #{item}! 比如,当我们遍历到键 :eggs 时,我们会输出:Today we're serving: eggs! 代码会遍历哈希中的每个项目,然后 puts 每个菜单项到屏幕上。
hashery_menu.each do |item, price|
puts "We've got #{item} for $#{price}. What a deal!"
end
这里事情有点变得复杂了。当我们在哈希上调用 each 方法时,我们将哈希的 键(item)和该键的 值(price)传递给 do 块。例如,当我们遍历到 :eggs(其对应的值是 2)时,我们会输出:We've got eggs for $2\. What a deal!
puts "Here's what a biscuit'll run ya, depending on how much butter
you want:"
hashery_menu[:biscuit].to_a.each do |price|
puts "$#{price}"
end
最后,我们将用一点 Ruby 魔法操作一下我们的hashery_menu中的:biscuit。首先,我们用hashery_menu[:biscuit]来访问它的值。然后,由于这个值是一个范围,我们可以在它上面调用to_a方法,将它转换成数组,然后像之前一样使用each方法遍历它的所有项。我们将打印出我们的消息,告诉大家一个饼干的价格是多少,然后do块将打印出可能的价格:$1、$2和$3,每个价格占一行。
你可以通过在命令行输入ruby hashery_menu.rb来测试整个菜单程序,它应该显示出我刚才给你展示的输出结果。
你这里有一个很棒的菜单,但如果你想让 Squeaky Jim 和 Big Hank 欣喜得泪流满面,可以尝试以下的创意。
你的菜单里有一个非常棒的范围,甚至还将它转换成了数组!不过我没有看到你菜单里有普通的数组,而你完全可以将数组作为哈希的值。为什么不添加一个:random_special键(即每日特别推荐),并将一个价格数组作为它的值呢?如果我告诉你可以在数组上调用sample方法,让 Ruby 从数组中随机挑选一个元素,你会怎么用它呢?
你可以非常花哨地使用shift、unshift、push或pop方法,将值添加到或从:random_special数组中删除。看看你已经写的代码,你会如何在:random_special键的数组值上调用这些方法呢?
说到push方法,Ruby 有一个很酷的快捷方式。它叫做铲子操作符,它的用法是这样的。包含<<的那一行和bagel_types.push('cinnamon raisin')完全相同:
>> **bagel_types = ['plain', 'sesame', 'everything']**
=> ["plain", "sesame", "everything"]
>> **bagel_types << 'cinnamon raisin'**
=> ["plain", "sesame", "everything", "cinnamon raisin"]
尝试用<<替换你的push操作,然后去[www.ruby-doc.org/`](http://www.ruby-doc.org/)阅读更多关于铲子操作符的内容。提示:它如何帮助你在 Ruby 中构建字符串?
你已经知道了这个!
你可能会觉得大脑里充满了我们所学的所有新的数组、哈希和范围的魔法,但不用担心——我们将再次过一遍所有内容,确保你都掌握了。
让我们从数组开始,数组只是信息的列表。你有两种创建数组的方式。你可以使用数组字面量语法,像这样使用方括号:
>> **breakfast = ['chunky bacon, 'chunky bacon', 'chunky bacon']**
=> ["chunky bacon", "chunky bacon", "chunky bacon"]
或者你可以使用Array.new来做相同的事情:
>> **breakfast = Array.new(3, 'chunky bacon']**
=> ["chunky bacon", "chunky bacon", "chunky bacon"]
你发现数组可以包含任何东西,包括字符串、数字、变量、布尔值,甚至其他数组。
你学到了一堆数组方法,包括:
-
empty?,如果数组没有元素,则返回true,如果数组至少有一个元素,则返回false。 -
length或size,它们做的是相同的事情——返回数组中的元素个数。 -
first,它返回数组中的第一个元素,而不将其移除。 -
last,它返回数组中的最后一个元素而不移除它。 -
shift,它返回数组中的第一个元素并且将其从数组中移除。 -
unshift,它将元素添加到数组的开头。 -
push,它将元素添加到数组的末尾。 -
pop,用于移除并返回数组的最后一个元素 -
insert,可以在数组的任何位置添加一个元素
呼!
让我们通过一些更多的例子在 IRB 中做些练习,刷新一下记忆:
>> **empty_array = []**
=> []
>> **empty_array.empty?**
=> true
>> **not_empty_array = [1, 2, 3, 4, 'I declare a thumb war']**
=> [1, 2, 3, 4, "I declare a thumb war"]
>> **not_empty_array.empty?**
=> false
>> **not_empty_array.length**
=> 5
>> **not_empty_array.first**
=> 1
>> **not_empty_array.last**
=> 'I declare a thumb war'
>> **not_empty_array**
=> [1, 2, 3, 4, 'I declare a thumb war']
>> **first_item = not_empty_array.shift**
=> 1
>> **not_empty_array**
=> [2, 3, 4, 'I declare a thumb war']
>> **not_empty_array.unshift(first_item)**
=> [1, 2, 3, 4, 'I declare a thumb war']
>> **last_item = not_empty_array.pop**
=> 'I declare a thumb war'
>> **not_empty_array**
=> [1, 2, 3, 4]
# We could also do not_empty_array << last_item
>> **not_empty_array.push(last_item)**
=> [1, 2, 3, 4, 'I declare a thumb war']
# Insert the number 5 at position 4; remember, arrays start counting
at 0!
>> **not_empty_array.insert(4, 5)**
=> [1, 2, 3, 4, 5, 'I declare a thumb war']
我们讨论了如何使用方括号访问数组:
>> **junk_drawer = ['lightbulb', 'dead battery', 'some pens', 'old**
**penny']**
=> ["lightbulb", "dead battery", "some pens", "old penny"]
>> **junk_drawer[2]**
=> "some pens"
最后,我们回顾了如何遍历数组:
>> **junk_drawer.each do |thing|**
>> **puts thing**
>> **end**
lightbulb
dead battery
some pens
old penny
=> ["lightbulb", "dead battery", "some pens", "old penny"]
这与以下内容完全相同:
>> **junk_drawer.each { |thing| puts thing }**
lightbulb
dead battery
some pens
old penny
=> ["lightbulb", "dead battery", "some pens", "old penny"]
接下来:哈希。哈希与数组不同,因为它们不仅仅是列表。相反,它们将键与值关联起来(想象:字典中的单词与定义)。然而,和数组一样,哈希可以使用字面量语法或new方法来创建:
>> **hashery_menu = {}**
=> {}
>> **hashery_menu = Hash.new**
=> {}
而且,和数组一样,你可以使用方括号访问哈希值:
>> **hashery_menu = {**
>> **eggs: 2,**
>> **hash: 3,**
>> **jam: 1,**
>> **sausage: 2,**
>> **biscuit: (1..3)**
>> **}**
=> {:eggs=>2, :hash=>3, :jam=>1, :sausage=>2, :biscuit=>1..3}
>> **hashery_menu[:jam]**
=> 1
我们看到了一些哈希方法,包括empty?(如果哈希没有键值对,则返回true,如果哈希至少有一个键值对,则返回false),length(返回哈希中键值对的数量),keys(返回哈希中键的数组),values(返回哈希中值的数组),has_key?(如果哈希包含某个特定的键,则返回true,否则返回false),以及has_value?(如果哈希包含某个特定的值,则返回true,否则返回false)。
这就是它们,尽显风采:
>> **hashery_menu.empty?**
=> false
>> **empty_hash = {}**
=> **{}**
>> **empty_hash.empty?**
=> true
>> **hashery_menu.length**
=> 5
>> **hashery_menu.keys**
=> [:eggs, :hash, :jam, :sausage, :biscuit]
>> **hashery_menu.values**
=> [2, 3, 1, 2, 1..3]
>> **hashery_menu.has_key?(:jam)**
=> true
>> **hashery_menu.has_key?(:zebra)**
=> false
>> **hashery_menu.has_value?(3)**
=> true
>> **hashery_menu.has_value?(42)**
=> false
我们还学到了,我们可以像遍历数组一样遍历哈希,只不过我们需要在代码块中的管道符号之间放入键和值的变量:
>> **hashery_menu.each do |item, price|**
>> **puts "#{item} costs #{price}"**
>> **end**
eggs costs 2
hash costs 3
jam costs 1
sausage costs 2
biscuit costs 1..3
=> {:eggs=>2, :hash=>3, :jam=>1, :sausage=>2, :biscuit=>1..3}
最后(但同样重要),我们学习了范围。范围就是一堆相邻的 Ruby 值。我们看到,括号内的两个点包含范围的两个端点,而三个点包含第一个端点,但只到(不包括)第二个端点:
>> **(1..5).to_a**
=> [1, 2, 3, 4, 5]
>> **(1...5).to_a**
=> [1, 2, 3, 4]
我们还学习了一些范围方法,包括to_a(将范围转换为数组)、first(返回范围中的第一个项)和last(返回范围中的最后一项):
>> **('a'..'c').to_a**
=> ["a", "b", "c"]
>> **('a'..'c').first**
=> "a"
>> **('a'..'c').last**
=> "c"
好了!我们成功了。到目前为止做得很好——但不要太得意忘形。午餐高峰期快到了,下一章会有点疯狂。
第六章:Ruby 的哈希表中的(厚切)培根
符号!
“那个哈希表真棒!”斯嘉丽说。鲁本一边点头一边猛地将另一份鸡蛋和哈希菜肴塞进嘴里。
“很高兴你喜欢!”大汉克说。“不过上午的高峰期马上就要到了,我们得加紧准备了。”

斯嘉丽从凳子上跳了下来。“我们有鸡蛋要煎、土豆要擦、香肠要煮、早餐肉汁要做、饼干要烤。还有别的什么吗?”
汉克转动了他的胡子。“我不确定,”他说,“让我们看看你们和斯奎基·吉姆做的菜单吧。”
“好!”斯嘉丽说,她在厨房的计算机装置上调出了哈希餐厅的菜单:
>> **hashery_menu**
=> { :eggs => 2, :hash => 3,
:jam => 1,
:sausage => 2,
:biscuit => 1..3 }
“这看起来不错——每一道菜都与它的价格在哈希表中关联,”大汉克说,“但我们也应该把我们的早餐饮料加上。你能为我的菜单哈希加一个包含数组作为值的键吗?”
“当然,”斯嘉丽说。“那我们应该放哪些饮料?”
“我们有咖啡、橙汁和茶,”汉克说。
“好!”斯嘉丽说。她输入了:
>> **hashery_menu['drinks'] = ['coffee', 'orange juice', 'tea']**
=> ["coffee", "orange juice", "tea"]
“啊哈!原来这是往哈希表里添加一个键的方法,”汉克说。
“没错!”斯嘉丽回答说。“你只需输入哈希表的名字,然后在方括号中输入键名——这里,我们使用的是'drinks'——然后将整个表达式赋值为你喜欢的任何值。看,我们是怎么更新hashery_menu的?”
>> **hashery_menu**
=> {:eggs=>2, :hash=>3, :jam=>1, :sausage=>2, :biscuit=>1..3,
"drinks"=>["coffee", "orange juice", "tea"]}
“酷!”鲁本说,终于吃完了他的鸡蛋和哈希菜肴。“现在菜单上有了饮料的列表。”他凑近计算机装置发光的屏幕,“不过看起来drinks哈希键是一个字符串,而其余的都是符号。这样有区别吗?”
“哦,当然有区别!”斯奎基·吉姆说,他正打开土豆袋并清理哈希餐厅巨大的 Grate-O-Matic。他将纸帽子推得更高一点,靠在机器上。“你看——”他开始说,但在他讲话的同时,肘部不小心按下了机器的大开关,机器突然启动,发出咆哮声,吓得斯奎基·吉姆差点摔倒好几次,才拼命把它关掉。

“你看,”吉姆 squeaked(发出尖锐的声音)地说,在他终于关掉 Grate-O-Matic 后,“尽管我在 Ruby 方面不太擅长,但我确实时不时地会尝试编程厨房的计算机装置。有一天早晨,哈希餐厅超级忙——这是我见过的最忙的上午高峰期之一!”
“我记得那个,”大汉克说,一边从闪亮的红色冰箱里拿出一大把香肠。“我们不仅有大量顾客,而且那天还是‘自选菜单日’。”
“自选菜单日?”鲁本问,一边挠着头。
大汉克点了点头,开始把香肠从长长的链条上拉下来,丢进一个巨大的平底锅里。“没错。我们让顾客自己创建个人菜单,这样他们可以点任何自己想要的东西。开始的时候没问题——人们在做菜单、点餐、吃饭。但随着时间推移,程序变得越来越慢。在高峰期,我们几乎无法完成任何订单!我们不得不关闭厨房的计算装置,手动处理所有订单。简直是一片混乱。”
吱吱作响的吉姆点了点头。“我想我知道为什么了!”
汉克停下了拉香肠的动作。“你知道?”
“没错!”吉姆说,“我前几天在读 Ruby,试图提高一下自己在厨房里的操作技巧,发现 Ruby 符号比字符串占用更少的内存。在‘自己做菜单’那天,我们把所有哈希表的键都用了字符串,随着程序运行,内存越来越多,直到没有足够的内存去完成任务。”
“稍微退后一点,”国王边咀嚼着一块生土豆边说,“这些 Ruby 符号到底是什么?你说它们比字符串占用更少的内存是什么意思?”
符号的简要介绍
当吱吱作响的吉姆试图向大家解释 Ruby 符号时,我来给你简要总结一下。基本上,Ruby 符号就是一个 名称。例如,如果我在谈论国王,而斯卡利特也在谈论国王,那么我们讨论的就是同一个东西——国王!在 Ruby 中,当我们谈论符号(即名称)时,会在前面加上冒号,像 :the_king。你经常会看到符号名称中有 下划线 (_),因为和变量名一样,符号名称中不能有空格。
那么,符号和字符串(比如 'The_King')到底有什么不同呢?好吧,回想一下国王在第二章的字符串。现在,假设国王有 两个 上面挂着 完全相同 珠子和饰品的字符串。虽然它们的 内容 可能相同,但它们并不是 完全相同的东西。但是当我们谈论国王时,我们并不是在谈论两个长得一模一样的国王,而是在谈论 同一个 国王!
如果你还有点困惑,不用担心:我有几个代码示例可以帮助你完全搞清楚。启动 IRB,然后试试这个:
>> **string_one = 'The King'**
=> "The King"
>> **string_two = 'The King'**
=> "The King"
>> **string_one.object_id**
=> 2184370320
>> **string_two.object_id**
=> 2184365180
这里我们把两个 不同 的变量设置为相同的字符串值 'The King'。然后,当你对这两个变量使用 object_id 方法时,你是在要求 Ruby 提供它用来跟踪每个对象的唯一编号。这是 Ruby 用来区分对象的 ID 号,并且没有两个对象的 ID 是完全相同的。相反,如果两个变量有相同的对象 ID,它们 必须 指向同一个对象。
你在 IRB 中看到的对象 ID 数字可能不会和我的完全一样,但没关系!每次你启动一个新的 Ruby 程序时,对象 ID 都会重新分配。重要的是,string_one和string_two,尽管它们的值都等于 'The King',却是不同的对象。它们的内容完全相同,但就像我们之前提到的“国王的字符串”例子一样,我们讨论的是两个完全不同的字符串,只不过它们恰好包含相同的内容。
现在看看这个:
>> **symbol_one = :the_king**
=> :the_king
>> **symbol_two = :the_king**
=> :the_king
>> **symbol_one.object_id**
=> 466088
>> **symbol_two.object_id**
=> 466088
这里我们设置了两个不同的变量,symbol_one 和 symbol_two,都指向符号 :the_king。再次强调,你的对象 ID 可能不会和刚才显示的数字完全相同,但当你比较 symbol_one 和 symbol_two 的对象 ID 时,你会发现它们是完全相同的数字!就像我们谈论国王时,我们指的是同一个人一样,symbol_one 和 symbol_two 也指的是同一个对象,:the_king。
因为符号仅仅是你可以随便使用的名称,所以你不需要为它们赋值。虽然你可以明确地说:
>> **variable_name = :my_fancy_symbol**
=> :my_fancy_symbol
你不能说:
:my_fancy_symbol = some_value
如果你这样做,你会得到一个SyntaxError。就像你不能通过把字符串或数字放到等号的左边来给它们赋不同的值一样,你也不能给符号赋不同的值。
唯一会把符号放在等号左边的情况,是当你在哈希中使用它们时,像这样:
>> **fancy_words = { bloviate: 'To talk at length' }**
=> {:bloviate=>"To talk at length"}
记住,如果我们使用更新的哈希语法,就不需要在 bloviate 键前加冒号。如果我们想使用旧的哈希箭头语法(=>),我们就需要在符号前加冒号:
>> **fancy_words = { :bloviate => 'To talk at length' }**
=> {:bloviate=>"To talk at length"}
但的确!我确实还在继续。那么你可能会想知道:符号到底有什么用?它们为什么比字符串占用更少的内存?
因为符号总是只有一个对象 ID,它只会在每个 Ruby 程序中创建一次。这意味着你可以创建成千上万的变量,它们都指向相同的符号对象,而只有一个符号对象被创建。如果你用字符串做这个操作,它们的对象 ID 会不同,这样你就会得到成千上万的不同字符串。就像你一样,Ruby 也只有有限的内存,不能同时跟踪太多的东西。如果你创建大量字符串,Ruby 在试图管理它们时就会耗尽内存,程序会变得非常慢,甚至崩溃!如果你使用符号,Ruby 会创建更少的对象,使用更少的内存,因此,使用符号的程序(比如作为哈希键的符号)会比使用字符串的等效程序运行得更快。这就引出了那个“千百万亿”级的问题:你什么时候应该利用符号带来的内存节省呢?
基本上,每当你需要重复使用一个名称,但又不想每次都创建一个全新的字符串时,符号是最好的选择。它们非常适合用作哈希键,也可以用来引用方法名。我们很快就会讨论如何将符号用作方法名!

说到“马上”,我敢肯定吱吱作响的吉姆快要结束他对 Ruby 符号的解释了。让我们看看鲁本、斯嘉丽、国王和大汉是否对符号有和你一样好的理解!
符号与哈希,终于在一起了
“我想我明白了,”鲁本说。“符号只是 Ruby 用来指代特定对象的名称,所以如果我们在哈希中使用符号作为键,我们实际上是在反复引用同一个对象。”
“完全正确!”吱吱作响的吉姆说。“现在你明白为什么我们在用字符串而不是符号做‘自建菜单’哈希时遇到了那么大的麻烦了。”
“当然!”斯嘉丽说。“每次顾客制作新菜单时,都会生成一堆新的字符串。”
“我们有成百上千的顾客,”大汉叹了口气。“难怪我们的 Ruby 程序内存不够用了!”
“嗯,我当然不想现在就开始把字符串加到菜单里,”斯嘉丽说。“我们怎么才能把字符串键改成符号呢?”她在计算装置中输入hashery_menu来调出哈希菜单的内容:
>> **hashery_menu**
=> { :eggs => 2, :hash => 3,
:jam => 1,
:sausage => 2,
:biscuit => 1..3,
"drinks" => ["coffee", "orange juice", "tea"] }
“嗯,”国王说。“我们能不能直接把字符串键改成符号键?”
“我不这么认为,”吱吱作响的吉姆说。“根据我读到的内容,我觉得我们能做的最好的办法是删除字符串键,然后用符号键替代它。”
“你说得对,”鲁本说,“但编程就是要不断试验。我听说 Ruby 有一个to_sym方法,可以把字符串转换成符号。我们要不要试试?”
“没问题,”斯嘉丽说,她打字:
>> **hashery_menu.keys.last.to_sym**
=> :drinks
“看起来成功了!”鲁本说。“你能再调出哈希看看确认一下吗?”
斯嘉丽点了点头,再次调出了哈希菜单。
>> **hashery_menu**
=> { :eggs => 2, :hash => 3,
:jam => 1,
:sausage => 2,
:biscuit => 1..3,
"drinks" => ["coffee", "orange juice", "tea"] }
“糟了!”鲁本说。“Ruby 返回了字符串 'drinks' 的符号版本,但它并没有真正改变哈希中的键。”
“这倒也好,”大汉说。“我一直在想我们的早餐饮品,突然意识到我们根本没在数组里放价格!”
斯嘉丽拍了拍额头。“没错!”她说。“我们需要把饮料和价格都放进去。”她想了想。“等等——如果我们在关联饮料和它们的价格,那不就像是将每个食物项和它的价格关联起来吗?我们能不能把哈希放到另一个哈希里面?”
“没别的办法了,只有实验!”国王说。“你不如按照吉姆建议的,先删除"drinks"键,然后试试加一个符号键,把哈希作为值放进去?”
“好的!”斯嘉丽说。“吉姆,你知道怎么从哈希中删除一个键吗?”
“我想是的,”吉姆说,他伸手过来开始在计算装置上打字:
>> **hashery_menu.delete('drinks')**
=> ["coffee", "orange juice", "tea"]
“哇,那是什么?”鲁本说。“当你删除了键时,它居然把值还给你了!”
吉姆点点头。“那就是delete方法的作用!”他说。“这样,如果我们想用删除的键的值做点什么,我们就可以把它保存到变量里,像这样:
menu_drinks = hashery_menu.delete('drinks')
“不过,”Jim 说,“现在不能这么做,因为'drinks'键已经没了。看到了吧?”他再次输入:
>> **hashery_menu**
=> { :eggs => 2, :hash => 3,
:jam => 1,
:sausage => 2,
:biscuit => 1..3 }
“干得漂亮!”Scarlet 说。“现在我们只需要测试一下是否能将哈希放在哈希里。Big Hank,饮料的价格是多少?”
“咖啡一美元,橙汁两美元,茶一美元。” Hank 说。Scarlet 在计算装置中输入:
>> **hashery_menu[:drinks] = { :coffee => 1, :orange_juice => 2,**
**:tea => 1 }**
=> { :coffee => 1, :orange_juice => 2, :tea => 1 }
“成功了!”国王大声喊道。“大家干得好!”
“就在千钧一发之际!” Big Hank 大声说道。大家都忙着围绕计算装置,致力于使 Hashery 菜单完美无缺,根本没有注意到周围愈发喧闹的声音。顾客们涌入 Hashery,空气中充满了声音,甚至 Big Hank 都得大声喊叫,才能让大家听见:“启动 Grate-O-Matic!照看好煎锅!像你们的命运依赖一样烤饼干!上午的高峰期来了,他们饿了!”
“是的,Hank!” Squeaky Jim 说道,他不仅没有发出尖锐的声音,还启动了 Grate-O-Matic,开始像做了一辈子一样翻制哈希。“把新的菜单送到所有顾客手中!”
“菜单!差点忘了,”Hank 说。“今天的特别菜单上还有一个新添加。”然后他输入了:
>> **hashery_menu[:chunky_bacon] = 1**
=> 1
“ chunky bacon?” Scarlet 和 Ruben 一起问道。

Hank 微笑着耸耸肩。“我有个朋友以前常来点这个,”他说。“有段时间没见到他了,所以我没有把它放进菜单里。但谁知道呢?”他望向越来越多的饥饿的 Hashery 顾客。“也许今天是他回来的一天。”
上午的高峰期
现在你已经了解了符号,你可以应付任何规模的上午高峰期,不用担心会让 Ruby 程序变慢或内存不足。事实上,你已经成为了 Ruby 符号的高手,以至于 Big Hank 和 Squeaky Jim 给了你一个他们到目前为止觉得不可能完成的任务:将他们的老式“自定义菜单”转变为使用符号作为键而不是字符串!
这一想法一开始听起来可能有点奇怪,但它只是为了确保你能够舒适地使用 Ruby 哈希表;每次使用它们时,你不必每次都将所有的键都转换成符号。哈希表非常适合存储像我们 Hashery 菜单这样的信息,你会在编写 Ruby 代码时反复使用它们——不仅仅是本书中的代码。
之前,我们看到不能仅仅对哈希表的键调用to_sym并期待它神奇地改变;相反,我们必须删除键并替换它。
对于单个字符串键来说,这样的做法还行,但 Hank 和 Jim 讨论的是成千上万的字符串,遍布数百个顾客的菜单——即使你想,也不可能一个一个地转换它们!但是,如果我们能自动遍历一个哈希表,做精确的操作:抓取每个字符串键,删除它,保存键的值,并将该值赋给一个新的符号键呢?
我们创建一个新文件,命名为 strings_to_symbols.rb。(如果你不记得如何做,可以参考第一章,或者向最近的成年人寻求帮助。)然后用你的文本编辑器打开文件,并输入以下内容:
my_own_menu = { 'tater_tots' => 2,
'fancy_toast' => 3,
'omelette' => 3,
'tiny_burger' => 4,
'chunky_bacon' => 1,
'root_beer_float' => 2,
'egg_nog' => 2
}
在这里,我们创建了一个全新的哈希表,名为my_own_menu,并将一些值(价格,数值类型)分配给一些键(菜单项,字符串类型)。继续往程序中添加内容,我们还没有完成!
puts "Object ID before: #{my_own_menu.object_id}"
接下来,我们将打印出我们菜单哈希的对象 ID。这是为了之后确认,尽管我们对哈希做了一些修改,但它仍然是同一个对象;在我们修改了键和值之后,如果对象 ID 与之前相同,那么我们就可以确定它是同一个哈希,只不过其中的信息不同了。
继续在strings_to_symbols.rb中添加内容。现在我们有了哈希键的字符串,但我们真正想要的是符号!我们需要添加一点代码,将字符串键转换为符号键。
my_own_menu.keys.each do |key|
my_own_menu[key.to_sym] = my_own_menu.delete(key)
end
puts "Object ID after: #{my_own_menu.object_id}"
puts my_own_menu
好了,就到这里。我们对my_own_menu哈希调用keys方法来获取键,然后立即对这些键调用each方法进行迭代。(还记得第五章吗?如果需要提示,可以回头看看。)
这里是非常酷的部分:对于哈希中的每个键,我们对键调用delete(它会将键从哈希中移除),但因为delete返回的是被删除的键所关联的值,我们立刻将其赋值为对该键调用to_sym。这是一个惊人的双重效果:它不仅删除了哈希中的原始键,还立刻将值添加到了一个新的键上,而这个新键就是原始键转换成的符号。结果是什么?你将哈希表中的所有键从字符串转换成了符号!
我们甚至可以证明这就是同一个哈希,而不是它的副本:我们在遍历哈希之前和之后打印哈希的对象 ID,你会看到输出中两次显示的对象 ID 是完全一样的。没错——每个对象在 Ruby 中都有一个对象 ID,包括哈希本身!
你的完整代码应该是这样的:
strings_to_symbols.rb
my_own_menu = { 'tater_tots' => 2,
'fancy_toast' => 3,
'omelette' => 3,
'tiny_burger' => 4,
'chunky_bacon' => 1,
'root_beer_float' => 2,
'egg_nog' => 2
}
puts "Object ID before: #{my_own_menu.object_id}"
my_own_menu.keys.each do |key|
my_own_menu[key.to_sym] = my_own_menu.delete(key)
end
puts "Object ID after: #{my_own_menu.object_id}"
puts my_own_menu
运行你的代码吧——输入ruby strings_to_symbols.rb并按下 ENTER 键。输出应该如下所示:
Object ID before: 2174149520
Object ID after: 2174149520
{:tater_tots=>2, :fancy_toast=>3, :omelette=>3, :tiny_burger=>4,
:chunky_bacon=>1, :root_beer_float=>2, :egg_nog=>2}
你应该看到相同的对象 ID 打印两次,然后是你的哈希表的漂亮输出,键变成了符号而不是字符串。
符号还能做什么?
现在你可以轻松解决大汉克和吱吱吉姆的菜单问题,你可能会想知道还能做些什么。如同鲁本所说,实验是编程的一个重要部分,你可以用哈希和符号做很多实验。例如,如果你对一个包含空格的字符串调用to_sym,会发生什么呢?(你仍然会得到一个符号,但它看起来会很奇怪——试试看吧!)
我们还可以探索哈希嵌套哈希。记住,我们可以像这样访问哈希中的值:
>> **hash_name[:key]**
=> value
那么,你会如何访问嵌套哈希中的哈希值呢?这里有个提示——对于我们的原始菜单:
>> **hashery_menu**
=> { :eggs => 2, :hash => 3,
:jam => 1,
:sausage => 2,
:biscuit => (1..3),
:drinks => { :coffee => 1, :orange_juice => 2, :tea => 1 } }
你觉得hashery_menu[:drinks][:orange_juice]会返回什么?
最后,字符串有一个to_sym方法,可以将其转换为符号,但符号也有一个to_s方法(即“to string”的缩写),可以将其转换为字符串。你会如何更新这个程序,将符号键转换为字符串?
你知道这个!
本章我们只讨论了哈希和符号,但由于它们不像数字、字符串(甚至数组)那样容易理解,因此值得再讲一遍。(天哪,我已经写了多年的 Ruby 了,仍然觉得符号很奇怪!)
首先,我们看了如何往哈希中添加键值对,这就像用方括号([])将键设置为一个值一样简单:
my_hash[:key] = value
接下来,我们介绍了符号,它们基本上就是名字;你不需要给它们赋值,但如果需要的话,你可以将它们存储在变量中。
例如,这样是可以的:
my_variable = :my_symbol
但这样做会导致错误:
:my_symbol = some_value
符号唯一可以出现在左边的情况是当我们把它们当作哈希键使用时,如下所示:
>> **my_hash = { ninjas: 'awesome',**
>> **wizards: 'pretty rad',**
>> **warrior_princesses: 'super tough'**
>> **}**
=> {:ninjas=>"awesome", :wizards=>"pretty rad", :warrior_
princesses=>"super tough"}
当你在谈论国王、老师或亚伯拉罕·林肯时,你说的总是完全相同的人;同样,符号总是指向完全相同的对象。这意味着它们比字符串占用更少的内存,因为每次你创建一个新的字符串——即使它和另一个字符串的所有字符都一样——它仍然是一个独立的对象,并拥有自己的对象 ID:
>> **symbol_one = :the_king**
=> :the_king
>> **symbol_two = :the_king**
=> :the_king
>> **symbol_one.object_id**
=> 466088
>> **symbol_two.object_id**
=> 466088 # The same!
>> **string_one = 'The King'**
=> "The King"
>> **string_two = 'The King'**
=> "The King"
>> **string_one.object_id**
=> 2184370320
>> **string_two.object_id**
=> 2184365180 # Different!
一般来说,符号适用于你需要反复使用一个名字的情况,主要用于哈希键以及其他一些巧妙的小技巧(我们将在后续章节中进一步讨论)。当你关心某个事物的内容时,你应该使用字符串;当你关心某个事物的名称时,你应该使用符号。
如果你不确定两个对象是否相同,你可以使用object_id方法(它适用于任何 Ruby 对象)来获取对象的 ID 号。每个对象都有一个唯一的 ID 号,这就是 Ruby 在程序中区分对象的方法:
>> **'The King'.object_id**
=> 2187090900
>> **{ :eggs => 2, :hash => 3 }.object_id**
=> 2187097060
>> **['eeny', 'meeny', 'miny', 'moe'].object_id**
=> 2187104080
记住,你的对象 ID 不会和这里显示的完全相同,但它们应该在你的计算机上彼此不同。
在符号和字符串之间转换非常简单!你可以使用to_sym方法将字符串转换为符号:
>> **'drinks'.to_sym**
=> :drinks
你也可以使用to_s方法将符号转换为字符串:
>> :**drinks.to_s**
=> "drinks"
关于从哈希中删除键值对,你不仅知道可以通过delete方法来实现,还学到了delete不仅会从哈希中移除键值对并且返回该值,这样你就可以将其保存在变量中:
>> **simple_hash = { :one => 1, :two => 2 }**
=> { :one => 1, :two => 2 }
>> **saved_value_from_hash = simple_hash.delete(:two)**
=> 2
>> **simple_hash**
=> { :one => 1 }
>> **saved_value_from_hash**
=> 2
最后,你学到了一件事:百分之百允许将哈希存储在另一个哈希中,如下所示:
>> **fancy_hash = { :number_key => 42,**
>> **:hash_key => { :first_value => 1,**
>> **:second_value => 2**
>> **}**
>> **}**
=> {:number_key=>42, :hash_key=>{:first_value=>1, :second_value=>2}}
你现在已经进入了 Ruby 的核心部分!好消息是,从这里开始基本上都是顺风顺水。虽然接下来确实有一些难度较大的概念,但一旦你掌握了基本的 Ruby 对象(如数字、字符串、数组和哈希),学会了如何使用它们的一些方法,并且对控制流等主题(使用if/elsif/else、循环和迭代器)变得得心应手,你就已经涵盖了大部分语言内容。如果你还不完全熟悉 Ruby,不用担心;虽然学习基础知识并不需要太长时间,但你可以根据自己的节奏深入探索。这也是我们接下来要做的:深入 Ruby 的核心,那里有一些听起来奇怪(但非常强大!)的“生物”在等待你。
第七章 方法与区块的魔力
疯狂中的方法
“我想这就是最后一份了!”嘎吱吉姆说,一边把一堆散乱的哈希倒进了簸箕。“真是个忙碌的上午高峰!”
“我同意,”大汉克说。他把一个巨大的绿色堆肥袋扛到肩上,朝国王、斯卡莱特和鲁本笑了笑。“感谢今天所有的帮助!要不是你们在这里帮忙,我们可真麻烦了。”
“我们乐意效劳!”鲁本说,他正打包三盒哈希蛋。 “这里的食物真棒!”
“当然见过!”斯卡莱特说。“午餐高峰时你们还好吗?我们玩得很开心,如果你们需要帮助,我们还可以再帮忙。”
汉克笑着把一袋又一袋的东西扔进厨房那巨大的堆肥桶。“哈希瑞更像是一个早餐和早午餐的地方,”他说。“午餐高峰时客人通常不多。我想我们会没问题的。”他擦了擦手,看了看四周。“今天剩下的时间你打算做什么?”
“嗯,”国王说,“既然我们已经来到这里,我在想我们可以——”
“等一下。这是什么?”斯卡莱特打断了他,一边从嘎吱吉姆的簸箕里拿出一把看似散落的哈希。
嘎吱吉姆弯下腰去看。“大部分是泥土,”他说。
“但是这些小红针是什么?”她问。“还有这个闪亮的绿色东西?”
国王从他的王袍里拿出一只小放大镜,凑近了点,想看得更清楚。
“啊哈!我见过这些红色的针刺,”国王说。“它们来自卡尔迈恩松。”他仔细观察了那块闪亮的绿色物体。“不过这个,”他说,一边抚摸着自己蓬松的白胡子,“这个是我多年来做国王从未遇见过的。”
“它看起来像是鳞片,”鲁本说。“像是鱼或者蜥蜴的!”
“甜玉米松饼!”国王说。“你说得对!但它太大,不能来自任何普通的鱼类或蜥蜴。它到底来自哪里呢?”
“一步一步来,”斯卡莱特说。“卡尔迈恩松是什么?”
“卡尔迈恩松是王国东部的一片广袤的红色森林,”国王回答道。“事实上,就在这里走几分钟就能到了。”
斯卡莱特把鳞片翻转在手心里。“你们见过这样的东西吗,汉克?吉姆?”
“永远不会,”吉姆说,汉克摇了摇头。
斯卡莱特想了想。“如果这可能是哈希瑞计算机故障的线索,我们应该调查一下,”她说。“越早越好!”
国王用力点了点头。“这边,”他说。他转向大汉克和嘎吱吉姆。“再次感谢你们美味的午餐,伙计们!”他说。“明天我还会再来的!”
“我们乐意效劳,陛下!”汉克笑着说,他和嘎吱吉姆鞠了个躬。
国王、鲁本和斯卡莱特挥手告别了汉克和吉姆,走出哈希瑞,走进明亮的上午光线中。
“就在那里!”国王踮起脚尖指着说。卡尔迈恩松的红色树顶在不到一英里的地方可见。
“好吧,我们出发吧,”鲁本说道。“还有大把时间,只需要几分钟。”说完,他们三人朝森林出发。
“你知道,”Scarlet 过了一会儿说,“我觉得这些 Ruby 故障根本不是偶然发生的。”
“真的吗?”国王问。
“想一想!”Scarlet 说道。“你的字符串丢了,神秘管道溢出,循环出了问题,而 Hashery 的计算装置也坏了——这一切竟然是在同一天发生的?”
“嗯,那个绳子的部分可能是我搞错了,”国王不好意思地说道。
“不管怎样,我觉得 Scarlet 说的有道理,”鲁本说。“我觉得这……是破坏行为!”
“天哪!破坏?”国王说道。“谁会做出这样的事?”
“我不知道,”Scarlet 回答,“但不管怎么说,我们会弄明白的!”
又走了几分钟,三人终于到达了卡尔曼松树林的边缘。巨大的松树高耸入云,红色的针叶在阳光下闪闪发光。

Scarlet 伸手抓起一把针。她用另一只手翻找口袋,从中拿出了她在 Jim 的簸箕里找到的针。国王拿着放大镜检查了这两根针,足足看了快一分钟。
“完全一样,”他最后说道。“这些针确实是松树的针!”
“你说得对!”鲁本说。“但是既然我们已经到了这里,接下来该做什么?”
“我想我们会向一个了解这里情况的人求助,”国王说。
“怎么回事?”Scarlet 和鲁本异口同声地问道。
“当然,借助计算装置!”国王说道。
Scarlet 四下看看。“但是我们现在在森林里!”她说道。“这里到 Hashery 之间根本没有计算装置。”
“计算装置在王国的每个角落都有,”国王说道。“你只需要知道该在哪里找。”他伸手拉了拉一棵附近树的最低枝条,一台巧妙隐藏在树干里的计算装置随之从树里摇了出来。
“哇!”鲁本说。“那现在怎么办?”
“嗯,我想我们得用 Ruby 来查找住在松树林里的人,”国王说道。“即使我们找不到我们那根闪闪发光的绿色鳞片的主人,至少我们可能会找到可以帮忙的人。”
“完美!”Scarlet 说道。“那么每个计算装置里都有一个储存着王国居民目录的数据库?”
“嗯,是的,”国王揉了揉头。“但是有个问题,我对 Ruby 不太了解,但我确实记得曾经听说过,实际上没有内建方法来获取所有那些人的列表。”
鲁本坐在一块平坦的岩石上。“没有方法!”他说。“如果 Ruby 没有内建方法,我们怎么能找到人来帮忙呢?”
Scarlet 思考了一会儿。“嗯,”她说,“我认为我们是可以自己编写 Ruby 方法的,不过我以前从没见过这样做。”
“编写我们自己的 Ruby 方法?”国王问。“那太棒了!你确定这可能吗?”
“当然有可能!”一个附近的声音喊道。国王和斯嘉丽都吓了一跳,鲁本差点从岩石上掉下来。他们都转过身,朝着声音的方向看去,看到离他们只有几步远的地方站着……一个骑士,拔出了剑!
“啊!”鲁本喊道,试图躲到岩石后面。
“这到底是怎么回事,午夜小吃马尔济潘有什么关系?”国王质问道。
骑士停住了,随即匆忙把头盔的面甲推了上去。
“陛下!”她喊道,并深深鞠了一躬。“非常抱歉!我没认出您来,因为我的面甲盖住了脸。”她迅速将剑收回剑鞘。
“一位女骑士!”斯嘉丽说道。
“不,只是个骑士。”国王说道。“毕竟,如果她是个男的,你不会说‘一个男骑士’吧?”
“我想不是。”斯嘉丽承认道。
“你是谁?”鲁本问。

骑士挺直了腰背,自豪地把手放在臀部。“我是 Off-White 骑士!”她回答道。
“Off-white?”国王问道。“我觉得你的盔甲更像是蛋壳色。”
“也许是米色。”斯嘉丽眯着眼说。
“我觉得那是一只大鸟。”国王说。
“够了,别再闹了!”骑士说道。“我是Off-White骑士,现在是你们自卫的时候了!”
“啊!”鲁本又喊道,双手抱头。
骑士试图挠头,却不小心刮到了头盔的外部。“你为什么那样缩着身子?”她问道。
“你们不打算把我们杀掉吗?”鲁本问道。
那位 Off-White 骑士笑了。“天哪,绝对不是!”她说。“事实上,作为骑士,我的职责就是帮助卡门松树林中需要帮助的人,所以我来教你们如何编写自己的 Ruby 方法。”
“但这是白天。”国王说道。
鲁本和斯嘉丽交换了一个心照不宣的眼神。
定义你自己的方法
Off-White 骑士清了清嗓子。“是的。嗯,”她说,“我想说的是,你完全可以定义你自己的 Ruby 方法。你只需要使用特殊的词汇def和end。”她走到那个巧妙伪装的计算装置旁,开始打字。
>> **def simon_says(phrase)**
>> **return phrase**
>> **end**
“你先输入def,这是define的缩写,因为你在定义一个全新的方法。接下来,输入方法的名字,在这个例子中是simon_says。然后在括号之间输入参数,这个方法只有一个参数:phrase。”
“什么?”国王一边揉着头,一边问道。
“参数。”Off-White 骑士说道。“它们就像占位符或你在调用方法时给方法传递的信息的昵称。”
“让我弄明白。”国王说道。“当你用def和end写出一个方法做什么的时候,那叫做定义这个方法。”
“没错。”骑士说。
“当你在某个地方实际使用方法时,那就是调用该方法。”
“的确!”骑士说,“有时候我们说调用而不是调用,但它们的意思完全一样。你定义一个方法,让 Ruby 知道它做什么,而你在需要使用时调用这个方法。调用一个方法看起来是这样的,”她继续说,并输入更多内容:
>> **simon_says('Prepare for battle!')**
=> "Prepare for battle!"
“我现在有点模糊了,”国王说。
“你总是有点模糊,”灰白骑士说,盯着国王蓬松的胡子。
“是的,是的,”国王说,“但是我仍然有些困惑。你能再讲讲如何调用方法吗?”
“当然!”骑士说,“当我们之前定义print_sum方法时,我们只是告诉 Ruby 每当我们使用simon_says这个名字时运行什么代码。然后我们可以通过写方法名并插入自己的信息——字符串'Prepare for battle!'——来使用这段代码,就像之前phrase参数一样。正如我所说,phrase就像一个占位符,直到我们准备好使用带有'Prepare for battle!'的方法时,它才出现在括号之间。”
“那'Prepare for battle!'周围的括号呢?”Ruben 问,“我之前看到过没有括号的 Ruby 方法调用。”
“你说得对!”骑士说,“括号是可选的;你通常在定义方法时使用它们,但在调用方法时,你可以选择使用或省略它们。对 Ruby 来说都是一样的!”
return 与 puts
“好的,我现在明白定义和调用了,”国王说,“但这个return是怎么回事,它和puts有什么不同呢?它们不都是把东西打印到屏幕上吗?”
“啊哈!”灰白骑士说,“很多人觉得这很混淆,但我想我可以通过几个例子向你展示return和puts之间的区别。”
“这里我们定义了一个叫做print_sum的方法,它用puts打印两个数字的和,”她说:
>> **def print_sum(a, b)**
>> **puts a + b**
>> **end**
“接下来,我们将定义第二个方法,它返回和。”
>> **def return_sum(a, b)**
>> **return a + b**
>> **end**
“你看我们定义的print_sum和return_sum方法的区别了吗?”骑士问,“一个是puts,另一个是return。”Scarlet、Ruben 和国王都点了点头。
“完美!”灰白骑士说,“让我们看看这对我们 Ruby 代码的实际意义。首先,我们调用我们的print_sum方法。”
>> **sum = print_sum(2, 3)**
5
=> nil
>> **sum**
=> nil
“看到了吗?”骑士说,“puts会在屏幕上打印某些东西——在这种情况下,它加了2和3,并把结果5打印到屏幕上——但它不会做任何事与值5:它在打印后产生nil!当我们检查sum的值时,我们看到它是nil。”
“现在让我们调用我们的return_sum方法。”她继续输入:
>> **sum = return_sum(2, 3)**
=> 5
>> **sum**
=> 5

“现在我明白了,”国王说,“打印某个东西只是让该值出现在屏幕上,但返回它则让你将该值存储在变量中,就像我们对sum所做的那样。”
“你明白了!”骑士说。“一个方法就像一个小机器。东西进入它,东西从它出来。进入方法的东西在你调用时是它的参数,出来的东西是它的返回值。”
“如果一个方法没有特定的返回值,它会返回nil。你知道每次你puts或print一些东西时,看到=> nil吗?那是因为尽管puts和print方法在屏幕上写入文本,它们没有返回值,所以它们返回nil。”
“等一下,”国王说。“如果一个方法在没有其他返回值时能自动返回nil,为什么我们不能自动返回其他值呢?”
理解方法参数
“我们可以!”白骑士说。“每当你在方法定义中,Ruby 会自动返回最后执行的 Ruby 代码。如果你想省点打字,可以省略return关键字,只要方法的最后一行是返回值,Ruby 会自动为你返回它。”
“太棒了!”鲁本说。“任何能节省我们打字的方式都很好。现在,稍微回顾一下参数和参数之间的区别:参数是在方法定义中括号之间的便捷名称,用来告诉方法它将获得哪种类型的信息,而参数则是你在调用方法时实际传递给它的信息。”鲁本说道。
“完全正确!”白骑士说道。“等一下,让我再给你一个例子。”她愤怒地敲击着计算机装置,同时叙述着。“我们来定义一个叫做add_things的方法,它有thing_one和thing_two两个参数,并返回它们的和。它看起来应该是这样的:
>> **def add_things(thing_one, thing_two)**
>> **thing_one + thing_two**
>> **end**
“接下来,我们将调用这个方法,传入参数3和7。返回值是10。”
>> **add_things(3, 7)**
=> 10
“太好了,”斯嘉丽说,“但是如果你想要有时候传递一个参数给方法,有时候又不传,怎么办?如果你没有传递正确数量的参数,Ruby 会抛出一个错误!”她在计算机装置上敲打:
>> **def plus_one(number)**
>> **number + 1**
>> **end**
>> **plus_one 2**
=> 3
>> **plus_one()**
ArgumentError: wrong number of arguments (0 for 1)
“是的!”鲁本说。“这里,Ruby 说它得到了零个参数,但它期望得到一个。”
“很好的观点!”白骑士说。“在这种情况下,你可以使用可选或默认参数。那些是带有占位符值的特殊参数,如果你在调用方法时没有给 Ruby 传递这些参数,Ruby 会插入占位符。让我为你定义一个这样的函数。”她说道,开始再次在计算机装置上敲击键盘:
>> **def declare_name(name='The Off-White Knight!')**
>> **puts name**
>> **end**
“看到等号了吗?”她说。“这告诉方法,如果没有其他指示,就使用这个字符串。现在,如果没有任何参数,方法将使用默认的名称,”她说。“我们来试试调用它!”她继续敲击键盘:
>> **declare_name()**
The Off-White Knight!
=> nil
“哇!”鲁本说。“你根本没传任何参数,所以默认值被自动使用了。”
“没错,”骑士说道。“而且,因为 Ruby 非常灵活,你甚至不需要括号就能表明你在调用一个方法!”她又输入了更多代码:
>> **declare_name**
The Off-White Knight!
=> nil
“这看起来有点太神奇了,”国王说道。“如果有很多代码在运行,我怎么才能立刻区分没有括号的方法和普通的变量?”
“这个观点很不错,”米白骑士说道。她试图擦去额头上的汗水,却不小心把面罩拉下来了。“我经常使用括号,因为它能清楚地表明我在使用一个方法,而不是其他东西,比如一个变量。”
“现在,假设你确实想用你自己的名字,”她继续说道,努力把面罩推回去。“你只需要传递它——带括号或不带括号——像这样。”她又输入了几行代码:
>> **declare_name('Lady Scarlet the Bold!')**
Lady Scarlet the Bold!
=> nil
>> **declare_name 'Sir Ruben the Fearless!'**
Sir Ruben the Fearless!
=> nil
“呼!”米白骑士说道。“让我休息一下,手套太累了。”
nil 是什么?
“当然,”国王说道。“不过,我还是有点困惑 nil,”他说。“它到底是什么?”

“我想我知道答案了,”鲁本说道。“nil 是 Ruby 表达‘什么都没有’的一种方式。当 Ruby 想表达‘什么都没有’或者‘没有值’时,它使用 nil。”
“nil 是 true 还是 false?”国王问道。
“没有!”鲁本说。“它是它自己的东西。但它确实是 Ruby 中两个假值之一——另一个是 false。”
“你说的‘falsey’是什么意思?”国王问道。
“我的意思是,如果你在 if 语句中使用 nil,它将是 true 的反面,”鲁本说道。“这应该很熟悉。”他在计算装置中输入了代码:
>> **if nil**
>> **puts "This text won't be printed!"**
>> **end**
“你的代码没有在屏幕上打印任何东西!”国王说道。
鲁本点了点头。“这是因为 Ruby 永远不会将 if nil 当作一个 true 条件。对 Ruby 来说,说‘if nothing’和‘if false’是一样的,所以它永远不会执行 if nil 和 end 之间的代码。”他想了想。“记得 if 语句吗?”他问道。
国王用力点了点头。“就像是昨天一样!”他说。
“是今天,”鲁本说道。
“番茄,番茄,”国王说道,两次发音完全一样。鲁本和斯嘉丽互相看了看,然后耸了耸肩。
“总之,”鲁本说道,“if 语句会获取一段 Ruby 代码,如果这段代码是 true 就做一件事,如果是 false 就做另一件事。nil 在 if 语句中总是被当作 false 来处理,因此,如果你想在一个值是 nil 时执行某些操作,你可能会认为需要这样做:
>> **if !nil**
>> **puts "But I will get printed!"**
>> **end**
But I will get printed!
=> nil
“不过我们已经见过一段内置的 Ruby 代码,它和‘if not’的意思完全一样:unless!”鲁本又输入了一些代码:
>> **unless nil**
>> **puts "But I will get printed!"**
>> **end**
But I will get printed!
=> nil
“unless 和 ‘if not’ 的含义完全相同。当我们说‘如果不困就熬夜’,这和‘除非困了就熬夜’的意思是一样的,”鲁本解释道。
“我以前见过这个!”国王说道。“我们在 Ruby 中使用 unless,每当我们本该使用 if 和 ! 时。”
“对!”鲁本说。“false 和 nil 在 if 或 unless 语句中表现相同,但记住 nil 和 false 并不完全一样。”他继续输入:
>> **nil == false**
=> false
“你看到 unless 示例末尾的 => nil 吗?”白色骑士问。“那就是我说的。nil 是 puts 的返回值。看一下!”她伸手越过鲁本的头,继续输入:
>> **puts 'Prepare for nil!'**
Prepare for nil!
=> nil
“关于 nil 还有最后一件事,”骑士说。“它不仅 不是 和 false 一样,它 也不是 和零一样!零是一个数字;nil 只是根本什么也不是。”
“我现在明白了,”国王说。
Splat 参数
“对,”骑士说,“那么我们进入更多方法魔法!我已经告诉你如何让一个方法接收一个可选参数,但 Ruby 还允许你告诉方法接收 任意数量 的参数。Splat 参数 就是告诉 Ruby 方法:‘嘿,我会传给你一个完整的东西列表。我不知道有多少个,所以你就处理我传给你的任何数量吧!’”骑士活动了几下手指。“它们是这么工作的,”她说,然后开始输入:
>> **def declare_knights(*knights)**
>> **puts knights**
>> **end**
>> **declare_knights('Lady Scarlet', 'Sir Ruben', 'The Off-White**
**Knight')**
Lady Scarlet
Sir Ruben
The Off-White Knight
=> nil
“你可以把我们第一行中的星号(*)看作是一个小的 splat 标记,它告诉方法接收所有的参数,无论有多少,并对它们做一些操作。”白色骑士说。
“我看到了参数名旁边的小星号,”国王说,“但在方法体内看不到,也没有在调用方法时看到。* 是不是只在定义方法时使用,并且只在圆括号之间?”
“正是如此,”骑士说。“Ruby 很聪明——你只需要告诉它一次!正如我提到的,”白色骑士继续道,“Ruby 会自动意识到方法体内最后出现的内容可能就是你想返回的东西。所以,如我之前提到的,如果你想省略一些输入,当方法的最后一行是返回值时,你可以省略 return 关键字。Ruby 会自动处理!这意味着,像这样:
>> **def add(a, b)**
>> **return a + b**
>> **end**
>> **add(1, 3)**
=> 4
完全等同于这个!
>> **def add(a, b)**
>> **a + b**
>> **end**
>> **add(1, 3)**
=> 4
“太棒了!”斯嘉丽说,“我肯定会记住这个技巧,等写我自己的方法时用上。”
“现在,”白色骑士说着,拔出剑,“是时候让你执行 YIELD 了!”
“啊!”鲁本喊道,再次用手捂住了头。
块方法
白色骑士费力地将剑重新插回鞘中。“你真的应该改掉那个畏缩的习惯,”她说。“我刚才说的是,当你写自己的 Ruby 方法并且接收块时,你需要使用 yield 关键字。”
“哦,”鲁本说,慢慢地又把手放了下来。
“等一下,你可以写自己的 Ruby 方法来接收块吗?”斯嘉丽问。
“当然!”骑士说。“你会像这样写,”她说着,在计算装置中输入:
>> **def my_block_method**
>> **yield**
>> **end**
=> nil
“首先,”骑士说道,“我们定义一个方法,my_block_method,使用 def。接下来,我们使用 yield 关键字告诉方法让块中的代码运行;当你调用这个方法并传入一个块时,块中的内容就是方法执行的内容!让我们来看几个例子。”
>> **my_block_method { 1 + 1 }**
=> 2
“这里,我们调用 my_block_method 并传入一个块,块中只是加了 1 + 1,所以 my_block_method 返回 2。我们还可以做其他的事情,比如打印文本:”
>> **my_block_method { puts 'Hello from the block!' }**
Hello from the block!
=> nil
“my_block_method 让块中的代码运行,所以它打印出了 Hello from the block! 我们看到 nil 是因为 puts 在屏幕上打印文本,但它返回的值是 nil,”骑士解释道。
“块是什么来着?”国王问。
“块只是一些 Ruby 代码,放在大括号之间,或者放在 do 和 end 之间,”骑士说道,“你可能见过像 each 这样的内建方法使用块,但现在你也可以创建你自己的方法来使用块了!”
“太棒了!”Scarlet 说道,“但你能把 splat 参数和块一起使用吗?”
“你可以的!”骑士说道,“你可以将常规参数、默认参数、和 splat 参数传递给你写的任何方法,并且它们可以按任何顺序出现。”
骑士伸了伸手指,打了个响指,开始在计算装置上输入,并一边解释:“让我们来构建一个我们可以用来快速轻松自我介绍的小工具,”她说道,“毕竟,我们骑士总是得到处去新城镇时介绍自己。”
>> **def all_about_me(name, age=100, *pets)**
>> **puts "My name is #{name}"**
>> **puts "I'm #{age} years old"**
>> **if block_given?**
>> **yield pets**
>> **else**
>> **puts pets**
>> **end**
>> **end**
=> nil
“我们还没完,”骑士说道,“但让我们来回顾一下。首先,我定义了 all_about_me 方法,接受三个参数。我们看到一个普通的 name 参数和一个 age 参数,如果没有传入年龄,默认值是 100。”
“但是你本来可以先写上带默认值的 age 参数,再写 name 参数,”Ruben 说道。
“你明白了,”骑士说道,“终于,*pets splat 参数!它可以与常规或默认参数按任何顺序出现,不过我们正好把它放在了最后。”
“我明白这部分了,”国王说道,“但是 block_given? 是怎么回事?”
“那是一个 Ruby 内建方法,”骑士说道,“如果方法接收到了块作为参数,它会返回 true,否则返回 false。我写了 all_about_me 方法,它会在传入块时 yield 给块,传递宠物列表给块;否则,它只是用 puts 打印出宠物列表。如果 block_given? 现在还不完全明白也没关系——我们稍后会看到更多。”
“为什么是 yield pets 而不是仅仅 yield?”Scarlet 问道。
“这是个很好的问题,”骑士说道,“之前我们只是想让块处理传给我们方法的所有内容,所以我们简单地写了 yield。但现在,我们希望块只关注 pets,所以我们特意只将宠物列表传给块。”
骑士的手指再次在计算装置上移动,试验着他们新定义的方法。
>> **all_about_me('Ruben', 12, 'Smalls', 'Chucky Jim')**
My name is Ruben.
I'm 12 years old.
Smalls
Chucky Jim
=> nil
“那是我!”鲁本说道,“太棒了。我们还能做什么?”
“嗯,”白色骑士说道,“我们可以使用我之前提到的那个块!看看如果我们把一个块传递给我们的方法,然后通过 pets 引用我们传递的宠物会发生什么:”
>> **all_about_me('Ruben', 12, 'Smalls', 'Chucky Jim') { |pets| puts**
**pets.join(' and ') }**
My name is Ruben.
I'm 12 years old.
Smalls and Chucky Jim
=> nil
“这看起来和上一个例子很像,”红衣骑士说道,“但是这个块挺复杂的。它在做什么?”
“我来给你们讲解!首先,{ |pets| puts pets} 只是告诉 Ruby:‘嘿,块!我要传给你一个变量 pets,你应该把它打印出来。’”
“但是然后,all_about_me 只会打印出数组的元素,看起来不太好看,”红衣骑士说道。
“没错!”骑士说道,“所以我也在使用一个内建的 Ruby 方法,join。Ruby 数组的元素可以通过 join 方法连接成字符串,所以我正在用 join 把宠物数组转化成一个字符串,中间用 ' and ' 连接。”
“你能给我们看一个别的例子吗?”鲁本问道。
“当然,”骑士说道,“这里有一个例子,我们可以用 join 把一个数字数组转换成一个字符串,每个数字之间加上 'plus':”
>> **[1, 2, 3].join(' plus ')**
=> "1 plus 2 plus 3"
“Ruby 总是有新东西可以发现,”国王惊叹道。
进入达戈龙的巢穴
“说到发现,”红衣骑士说道,“这让我想起来了!”她在口袋里翻找。“你以前见过这样的东西吗?”她拿出了那片闪闪发光的绿色鳞片,让白色骑士检查。
“天啊!”白色骑士说道,“这片鳞片看起来像是达戈龙的!”
“你是说那只 龙?”鲁本猜测。
“不,达戈龙,”白色骑士说道,“那是她的名字。虽然她确实是一条龙。”
“一只母龙!”鲁本说道。
“不,只是一条龙,”白色骑士说道,“如果她是个男人,你会称她为‘男人龙’吗?”
“我猜不是,”鲁本承认道。他紧张地四处张望。“但是在卡尔敏松林里真有龙吗?”
“别担心,”骑士说道,“达戈龙是一只非常强大的龙,但她也非常聪明且有礼貌。事实上,我很惊讶你在麻烦发生的地方找到了她的一片鳞片。这听起来不像是我认识的达戈龙。”
“嗯,听起来我们最好去找她问问发生了什么,”国王说道,“带路吧,女士骑士!”
“去找达戈龙!”白色骑士说着,把面罩拉下来遮住了眼睛。“这边!”骑士朝森林深处走去,国王、红衣骑士和鲁本跟在后面。
走了几分钟后,他们开始听到低沉而有节奏的 呼呼声,就像有人在用风箱给火加油。
“那是什么?”鲁本低声问。
“达戈龙!”骑士低声说。“她在这里!”他们还没反应过来,便发现自己站在一只庞大的龙面前,这只龙蜷缩在一个闪闪发光的绿色圈子里,正在睡觉。

“达戈龙!”骑士喊道,把面罩推上她的头盔。
达戈龙没有睁开眼睛。“是吗?”她吼道,一股细细的烟雾从她的右鼻孔冒了出来。
“我带来了一些客人来看您,包括国王!”
达格龙的眼睛猛地睁开,立刻集中在他们四个人身上。达格龙展开身体,竖起身来,直立到了她的最大高度;她的头几乎碰到了周围松树的顶部。
“陛下!”达格龙说道,她低头行礼,差点让头碰到地面。
“达格龙女士,”国王说道。“我们带来一个稍微有点……不寻常的问题。”国王朝斯卡雷特点了点头。“这个是你的吗?”
斯卡雷特从口袋里拿出一片鳞片,递给了达格龙。达格龙盯着它看了几秒钟,然后慢慢摇了摇头。
“我不认为那是我的,”达格龙说道,“但我确实有很多鳞片。如果你们愿意,可以检查一下看看我是不是缺少了某一片。”
一行人花了将近一个小时检查达格龙,寻找松动或丢失的鳞片。斯卡雷特和鲁本四处搜寻。乳白骑士也努力地探寻。国王则在一棵高大的红松下小憩片刻。
“好吧,毫无疑问,”斯卡雷特最终说道,把那片神秘的鳞片靠近达格龙尾巴的尖端。“这绝对不是达格龙的。”她把鳞片放进口袋,沮丧地坐在鲁本旁边的岩石上。
“虽然我很高兴自己不是这些奇怪事件中的嫌疑人,”达格龙说道,“但我确实感到很抱歉让你们失望。”她想了想。“虽然我不能确定能帮上什么忙,但我可能知道能帮忙的人。”
斯卡雷特抬起了头。“谁?”她问道。
“至于游吟诗人,”达格龙说道。“他在猩红松树林中嬉戏歌唱,几乎走遍了王国的每个角落。如果有任何奇怪的事情发生,我确信他会知道。”
鲁本从岩石上跳了下来。“你能带我们去见他吗?”他问道。
“当然,”达格龙说道。“不过我们可能需要一段时间才能找到他。”
斯卡雷特站起来,拍了拍身上的灰尘。“实际上,我打赌我们可以很快找到他,”她说道。“既然我们现在知道如何定义自己的 Ruby 方法,我可能知道正是那个方法!”
你知道这个!
在定义自己的方法、创建使用代码块的方法以及了解像展开参数和默认参数这样的内容之后,你的大脑可能已经感觉非常充实了!我们花点时间回顾一下本章讨论的内容吧。
首先,你学会了如何编写自己的方法。我们用def开始方法定义,接着是方法的名字,然后是括号中的参数列表。最后,输入我们希望方法执行的代码,最后用end结束,像这样:
>> **def multiply_by_three(number)**
>> **number * 3**
>> **end**
>> **multiply_by_three(2)**
=> 6
你还学会了方法可以有默认或可选参数。如果我们为一个接受可选参数的方法提供了参数,方法就会使用这个参数;否则,它会使用默认值:
>> **def multiply_by_three(number=2)**
>> **number * 3**
>> **end**
>> **multiply_by_three**
=> 6
>> **multiply_by_three 3**
=> 9
如果我们想要一个可以接受任意数量参数的方法,我们可以通过在参数名前加星号(*)来使用展开参数:
>> **def print_all_the_names(*names)**
>> **puts names**
>> **end**
>> **print_all_the_names('Larry', 'Curly', 'Moe')**
Larry
Curly
Moe
=> nil
说到nil,你已经学到没有显式返回值的方法会返回nil,这是 Ruby 表示“什么都没有”的方式。记住,返回一个值与仅仅在屏幕上打印它是不同的!
>> **puts 'Beware the Dagron!'**
Beware the Dagron!
=> nil
事实上,谈到返回值时,更常见的做法是省略return,让 Ruby 自动返回它运行的最后一段代码的结果。所以虽然你可以写成这样:
>> **def just_return_two**
>> **return 2**
>> **end**
>> **just_return_two**
=> 2
用这种方式写会更符合 Ruby 风格:
>> **def also_returns_two**
>> **2**
>> **end**
>> **also_returns_two**
=> 2
最后,我们看到如果要定义一个接受块的方法,只需要使用方便的yield关键字。我们可以不带参数地yield,将控制权交给块,或者传递参数,以便给块提供可操作的参数。
>> **def block_party**
>> **yield**
>> **end**
>> **block_party { puts 'Hello from the block!' }**
Hello from the block!
=> nil
>> **def block_party_part_two(name)**
>> **yield name**
>> **end**
>> **block_party_part_two('Haldo') { |name| puts "This is #{name}'s**
**party!" }**
This is Haldo's party!
=> nil
你在这一章学到了很多东西,但请记住:如果你对某个方法的功能或它期望的参数有疑问,你随时可以查阅 Ruby 文档,地址是 ruby-doc.org/。只要上网之前请确保先问问身边的大人!
说到查找新内容,我们将在下一章介绍一些新的 Ruby 代码——具体来说,如何组织、创建和控制我们自己的 Ruby 对象。
第八章 一切都是对象(几乎)
我们故事的主题是一个对象
斯嘉丽跑到计算装置前。“你知道那个王国中每个人的目录名吗?”她对国王喊道。“它是一个哈希表,将每个人的名字和地址关联起来。”
“让我们看看,”国王说。“啊,是的!我很确定它叫做citizens。”
斯嘉丽点点头,开始在 IRB 中输入。当她按下回车键时,她看到的是:
>> **citizens**
=> {
:aaron_a_aardvark => 'A van down by the river',
:alice_b_abracadabra => 'The green house with two chimneys',
:trady_blix => 'Mal Abochny',
# ...and so on and so forth
国王从她肩膀上探头看。“就是它!”他说。“不过哇!王国里肯定有成千上万的人!我们怎么找到 Wherefore?”
斯嘉丽继续敲打键盘:
>> **citizens.size**
=> 24042
“是的,哈希表肯定太大,不能手动查找,”斯嘉丽说,“不过我敢打赌我们可以写一个方法来找到他!”
鲁本仔细研究了citizens哈希表。“记得我们可以通过输入哈希名,然后在方括号里加上键,来获取哈希值吗?”他问。
“是的,”斯嘉丽说。
“嗯,”鲁本说,“如果我们写一个方法,传入一个人的名字和citizens哈希表,然后试图在哈希表中查找这个名字,怎么样?”
“鲁本,你真是个天才!”斯嘉丽说。她迅速打字:
>> **def find_person(name, people)**
>> **if people[name]**
>> **puts people[name]**
>> **else**
>> **puts 'Not found!'**
>> **end**
>> **end**
“等等,等等。这是什么?”国王问。“只是我随便写的一个方法,”斯嘉丽说。“看吧?它叫做find_person,它接受一个人的name作为符号和一个people的哈希表作为参数。如果它在哈希表中找到了名字,就打印出来;否则,它就说名字没找到!”她继续输入:
>> **find_person(:wherefore, citizens)**
=> One half mile due east!
“找到了!”斯嘉丽说。“它在citizens哈希表中找到了:wherefore键。”
“往东走半英里!”褪色骑士说。“应该只要几分钟,东边就在那儿。走吧!”
达格龙挺直了身子,瞬间遮住了太阳。“我也去,”她说,“我和 Wherefore 是老朋友,我们已经有一段时间没见面了。能再次见到他真好。”
“那好,”国王说,“带路吧!”
褪色骑士和达格龙转身,朝着上午晚些时候的太阳走去,国王、斯嘉丽和鲁本紧随其后。他们走着走着,树木越来越高,彼此也越来越密集,几分钟后,太阳只从卡尔梅松松树的树梢间透出一抹温暖的红光。
“等一下,”鲁本说,他停下脚步,转过头去。“你听到了吗?”
他们都停了下来。国王捂住耳朵,摇了摇头,把小指头插进耳朵里转了转,然后又捂住耳朵。“我什么也听不见,”他说。
“我也听到了,”达格龙说。“是——”
“音乐!”鲁本喊道,“它是从那边传来的!”他指向他们之前前进方向的右侧。
“走吧!”斯嘉丽说道,大家继续向松树林走去。
音乐声渐渐增大,在穿过一片特别密集的树林后,大家发现自己站在一片小草地的边缘。在草地中央,树桩上坐着一个身穿红色长袍、戴着带有长白羽毛的弓箭手帽的男人。他正在弹奏一把粉色的曼陀林,偶尔停下来用羽毛笔在一卷长纸上急促地涂写,而这支羽毛笔与他帽子上的白羽毛一模一样。
“为什么!”达格龙轰然回应。
站在树桩上的男人停下了涂鸦,抬起头来看。他的脸上露出了灿烂的笑容。“达格龙!”他喊道,“真高兴见到你!进来,进来,进来。”

在达格龙的带领下,大家穿过草地,围绕着为什么转了一圈。为什么灵活地跳下树桩,脱下帽子,深深地鞠了一躬。
“朋友们,”他说,“欢迎来到我的森林根据地!”他指着树桩。“现在看起来不怎么样,但我一直对修缮旧物有一种情结。而我,”他说,“是你们谦卑的主人,流浪的吟游诗人为什么。”为什么把帽子戴回了头上。“我当然认识达格龙,我之前也见过那位灰白骑士。”他看着国王,双手合十。“陛下,”他说,“我之前没有荣幸见过您,但现在确实是荣幸。”
“同样的,”国王说道,“我们听说过很多关于你的事情!”
为什么转向鲁本和斯卡利特。“那就剩下你们这些可爱的无赖了。你们叫什么名字?”
“我是斯卡利特,”斯卡利特说道,“这是鲁本。”
“嗨!”鲁本说道。
“你好,你好!”为什么说道,“很高兴见到你。不过恐怕你们来得不是时候。”他叹了口气。“我整个上午都在写一首民谣,但才写了一半。如果晚上之前想写完,我得立刻回去继续。”
“一首民谣?”斯卡利特说道。
“哦,是的,”为什么说道,“你看,我算是个商人。我经营一个小型的民谣配送服务,拥有几十个客户。唯一的难题是,”他说,“这意味着我确实有几十个客户,而每首民谣都需要我花上好几个小时才能完成。我简直忙不过来了!”他从长袍口袋里拿出一条手帕,擦了擦额头上的汗水。
达格龙若有所思地哼着,吐出几缕烟雾。“你知道,”她说道,“我想我可以帮上忙。”她环顾四周,几乎空旷的草地。“不过我需要一点鲁比魔法。你们附近有计算装置吗?”
为什么笑了。“我有计算装置!”他说着,踩上了树桩上最大的根部。树桩震动了一下,然后从地面上升起几英尺。它缓慢旋转着升起,露出了熟悉的计算装置屏幕的光芒!
类与对象
“太好了,”Dagron 说着,绕过树墩,紧挨着屏幕靠了过去。“那么!你 Ruby 程序中的每个对象都有一个唯一的 ID 号,”她说。“你会发现,你创建的对象通常比 Ruby 创建的对象有更高的 ID 号。看?”她用爪子触摸了计算设备的屏幕,说道:“Ruby 有一些非常熟悉的对象,比如0或true。Ruby 中的每个对象都有自己的 ID 号,这就是 Ruby 用来追踪它们的方式。看看!”
>> **0.object_id**
=> 1
>> **true.object_id**
=> 20
“像这样的内建 Ruby 对象在 IRB 启动或脚本加载时,Ruby 会自动分配 ID 号,”Dagron 继续说道。“Ruby 也会给我们在程序中创建的 Ruby 对象分配 ID 号,但这些 ID 号通常是非常高的。这是因为 Ruby 为我们提供了很多内建对象!”她再次触摸计算设备,屏幕上出现了更多文字:
>> **:baloney.object_id**
=> 1238088
>> **"The Ballad of Wherefore the Wand'ring Minstrel".object_id**
=> 2174481360
“她是怎么做到的?”Ruben 悄声对 Off-White 骑士说道。“她甚至什么都没打!”
“她不需要,”骑士低声回应。“龙是神奇的生物,而 Dagron 是所有龙中最具魔力的一个。”
“但是这些对象都来自哪里?”Dagron 问道。Wherefore 盘腿坐在地上,期待地仰望着她。
“来自类,”Dagron 说,回答了她自己提的问题。“你可以把 Ruby 类看作是制造特定类型对象的小机器,每个 Ruby 对象都知道自己属于哪个类。我们可以使用class方法来询问对象属于哪个类。首先,Ruby 的数字来自Fixnum类。看!”她说着,屏幕上出现了更多代码:
>> **7.class**
=> Fixnum
“一个字符串的类自然是……String!”她继续说道:
>> **'Odelay!'.class**
=> String
“知道这些很不错,”国王插话道,“但是这对我们有什么好处呢?”
“我正要说这个,”Dagron 说道。“当你知道一个 Ruby 对象属于哪个类时,你可以使用new方法来创建该类的一个新对象。你以前见过这个,对吧?”她指着屏幕上的新代码说道:
>> **greeting = 'Hello!'**
“是的!”Ruben 说道。
“好吧,现在你可以做这个!”Dagron 说着,再次触摸了计算设备。
>> **greeting = String.new('Hello!')**
=> "Hello!"
>> **greeting**
=> "Hello!"
>> **greeting.class**
=> String
“你看到了吗?”Dagron 说,折叠起她的爪子。“Ruby 中的每个对象都有一个类,我们可以用class方法找到它。更重要的是,每个对象都是通过类的new方法创建的,类的工作就是生成特定类型的对象!”
“所以这个类就像一个饼干模具,压制出特定种类的饼干,”Wherefore 说着,用闭拳拍打着掌心做出压制的动作。“姜饼人、巧克力碎片饼干、雪花形状的糖饼干。而对象就是这些饼干!”
“这是一个非常好的思考方式,”Dagron 说。
“什么时候吃午餐?”Wherefore 问道。

“恐怕我还是不太明白,”国王打断道。“我仍然有点困惑,类到底有什么重要的?”
“我想我可以帮忙解释这个,”斯嘉丽说道。“当我们处理数字或字符串时,类所做的有用事情可能不太明显。但是如果我们要创建我们自己的对象,并且有自己的新类,类就成了从模板创建一堆对象的方式。例如,如果我们有一个Minstrel类,我们就可以创造一堆吟游诗人!”
“怎么做?”国王问道。
创建我们的第一个类:Minstrel
“很高兴你问了!我们来试试吧,”达格龙说。她触摸了计算装置,更多代码出现在屏幕上。
注
对于这些较长的代码示例,我们将编写 Ruby 脚本!每当你看到代码上方的文件名以斜体显示时,比如接下来例子中的 minstrel.rb ,这意味着你可以将代码输入到文本编辑器中,并将其保存为给定名称的文件。
minstrel.rb
class Minstrel
def initialize(name)
@name = name
end
def introduce
puts "My name is #{@name}!"
end
def sing
puts 'Tralala!'
end
end
“那么,”达格龙清了清嗓子,说道,“我们来看看。class关键字告诉 Ruby 你想创建一个新类,”她说。“就像你使用def和end告诉 Ruby 你在定义一个新方法一样,你使用class和end告诉 Ruby 你想创建一个新类。”
“在class之后,你输入类的名称,可以是你喜欢的任何名字,”达格龙解释道。“不过,类名总是以大写字母开头,比如Minstrel。”Wherefore 已经把羊皮纸翻过来,正在尽快地在他的歌谣背面做笔记。“我们正在创建Minstrel类,这样我们就可以创建很多新的吟游诗人。”
“在class和最后的end之间,你可以添加任何你想要的方法,就像在类外定义方法一样,”达格龙继续说道。“在Minstrel类中,我定义了三个方法:initialize、introduce和sing。”
鲁本凑近计算装置的屏幕。“为什么那个@name变量前面有一个@呢?”他问。
“一切都在合适的时候,”达格龙说道。
注
为了跟随达格龙,我们需要将她的脚本加载到 IRB 中。当我们想从文件中在 IRB 中使用代码时,只需在包含 Ruby 脚本的文件夹中启动 IRB,然后使用load命令加载文件。像这样加载 minstrel.rb :
>> **load 'minstrel.rb'**
=> true
现在让我们试试达格龙的代码!
“首先,让我们看看Minstrel类的initialize方法。每当我们使用new方法创建类的新实例时,这个方法就会被调用。看看!”达格龙在屏幕上添加了更多代码。
>> **wherefore = Minstrel.new('Wherefore')**
=> #<Minstrel:0x000001052c77b0 @name="Wherefore">
“当我们调用Minstrel.new时,我们创建了一个新的吟游诗人。因为initialize方法只接受一个参数name,所以我们在调用new方法时传入了一个名字。你看到@name="Wherefore"那部分了吗?这意味着wherefore的名字是'Wherefore'!”达格龙深思了一下,“所以如果你想在创建类的新实例时立刻执行某些代码,就把它放在类的initialize方法定义中。”
“明白了,”国王说。
“现在所有的 Proc.new 相关内容更有意义了!”Ruben 说道。“我们只是每次调用 new 时创建新的 proc!”
“没错!”Off-White Knight 说道。“Proc 是一个内置的 Ruby 类,每当我们调用 new 时,就会创建一个新的实例。我们基本上有一个小工厂,每当我们想要时就生成新的 proc。类就是这个:小工厂,生产对象!”
“正是如此,”Dagron 喘着气说,她几乎露出了笑容。
“你添加的另外两个方法怎么样?”Scarlet 问道。
“啊,是的,”Dagron 说道。“我们的 wherefore 是一个 Minstrel,因此他可以自动访问那些方法。”
>> **wherefore.introduce**
My name is Wherefore!
=> nil
“看到了吗?”她说。“introduce 方法打印一个包含乐师名字的字符串,在这个例子中是 Wherefore。而且他不仅可以自我介绍,还能唱歌!”
>> **wherefore.sing**
Tralala!
=> nil
“我们已经讨论过类是如何生成某种类型的对象的,”Dagron 说道,“但我们其实还没有真正提到对象是什么。其实很简单:对象就是一小堆值!你可以把它们想象成信息的容器——可能包含一个名字、一个大小,或者一个颜色。每个对象从它的类那里继承方法,允许我们访问它的名字、大小或颜色,这就构成了我们的 Ruby 代码。”
“好的,”国王说道,“现在我明白为什么类如此重要了:它们让你可以重用代码来处理多个对象,而不需要每次都重写所有的信息和方法,Ruby 代码就是由对象组成的。但让我们回到 Ruben 的问题——我们看到的那个奇怪的螺旋形图案在 wherefore 的 @name 上是怎么回事?”
“@符号 (@) 只是告诉 Ruby 这是一个特殊类型的变量——一种描述对象值的变量,像是对象的名称、大小或颜色!我稍后会详细解释一下。让我们通过使用巫师(weezards)来试试这个例子,”Wherefore 说道。
“你是说巫师,”Scarlet 说道。
“不,是 weezards,”Wherefore 说道。“短巫师。小巫师。Wee 的东西。Weezards。”

“很好,”Dagron 说道。“但为了讲明白这个问题,我需要解释一下 Ruby 中四种不同类型的变量。”
“四种!”国王惊呼道。“我以为只有一种!”
“你通常看到的变量叫做局部变量,”Dagron 说道。“它们非常适合创建你很快就会用到的变量。但一旦我们开始编写自己的方法和类时,就需要创建一些可以在这些方法和类的定义内定义,但会在稍后使用的变量——例如,当我们最终调用一个方法或创建一个类的实例时。”
“另外三种变量,”Dagron 接着说,“分别是全局变量、类变量和实例变量。虽然在不同地方使用不同类型的变量可能让人感到困惑,但一旦你掌握了其中的规律,就会发现其实非常简单。”
“你说的不同地方是什么意思?”Scarlet 问道。
“作用域,”Dagron 说道。
变量作用域
哇哦,这越来越有意思了。我们正在进入语言的真正核心!作用域是 Ruby 中一个非常重要的概念,我激动得不行,简直无法抑制自己的兴奋。我希望你不介意我在 Dagron 向我们的勇敢英雄们解释作用域时,借此机会也给你简单解释一下作用域。只需要一分钟。
这可能让你感到惊讶,但并不是所有变量都可以在 Ruby 程序中的任何时候随便使用。在程序中,有时候即使你定义了一个变量,如果你尝试使用它,Ruby 会抱怨并表示它不存在!这可能意味着什么呢?
这意味着什么呢:在程序的任何给定时刻,只有某些你定义的变量和方法可以被访问。程序中任何时刻可以访问的变量和方法集合定义了当前的作用域;你可以使用作用域内的任何内容,而无法使用作用域外的任何东西。
那么,是什么决定了 Ruby 中变量的作用域呢?目前,这里有一个很好的经验法则:新的作用域是在方法定义、类定义和代码块内部创建的。所以,如果你使用的是我们一直在使用的普通局部变量,这样做完全没问题:
>> **regular_old_variable = 'Hello!'**
=> "Hello!"
我们只是将一个regular_old_variable设置为字符串'Hello!'。这很标准。
接下来,我们将在方法内部定义一个变量:
>> **def fancy_greeting**
>> **greeting = 'Salutations!'**
>> **end**
=> nil
在这里,我们在名为 fancy_greeting 的方法内部定义了一个名为 greeting 的变量。你之前已经见过方法定义,所以这里也没有什么新鲜的东西!
接下来,我们将重新回顾代码块:
>> **3.times { |number| puts number }**
0
1
2
=> 3
到这个阶段,你已经是一个块的专家了,所以你也掌握了这一点:我们在数字 3 上调用了 times 方法,并传递了一个代码块。在块内部,我们使用变量 number 来跟踪当前数字,并依次打印出 0 到 2 的每个数字。(别忘了,计算机从 0 开始计数,而不是从 1。)
这些变量错误将会让你震惊和惊讶!
不过,可能让你感到惊讶的是,这些代码中的某些部分会导致 Ruby 抛出错误!让我们一一看看。在下面的代码中,我们从定义一个变量开始。但这个regular_old_variable存在于 FancyThings 类定义之外(在外部作用域中),因此它在类定义内部不存在!
>> **regular_old_variable = 'Hello!'**
=> "Hello!"
>> **class FancyThings**
>> **puts regular_old_variable**
>> **end**
NameError: undefined local variable or method `regular_old_variable'
for FancyThings:Class
在类定义内部,你会获得一组全新的局部变量(你一直以来看到的那种变量),因此 Ruby 正确地告诉你,在类内部,你还没有一个叫做 regular_old_variable 的变量。
方法定义也是如此:它们也会获得自己的局部变量集,因此当你在方法内部定义 regular_old_variable 时,它在方法外部是不存在的:
>> **def fancy_greeting**
>> **puts regular_old_variable**
>> **end**
>> **fancy_greeting**
NameError: undefined local variable or method `regular_old_variable'
for main:Object
又一个错误!
而且,正如你可能已经猜到的那样,我们在块示例中的 number 变量是局部的,它在块结束后立即停止存在,所以如果我们在块结束后再次尝试使用它,就会出现错误!
>> **3.times { |number| puts number }**
0
1
2
=> 3
>> **puts number**
NameError: undefined local variable or method `number' for
main:Object
在这里,对于从 0 到 3 的每个数字,Ruby puts将传入块的number打印出来。现在,块变得有趣了:就像方法或类一样,在块中定义的变量在块结束时会停止存在。不过,不同于方法和类,块可以访问它们外部的变量和信息!在这种情况下,我们的块知道数字 3,因此知道变量number应该取 0 到 3 之间的每个数字。然而,一旦块结束,Ruby 就不再关心number,所以如果我们试图再次使用它,就会导致错误。
当我第一次了解到 Ruby 可以在程序的某些部分看到变量,而在其他部分看不到时,我好好挠了挠头,我相信你现在一定在问自己我当时问自己的一样问题:“如果是真的,那我到底怎么才能在程序的其他地方使用我在类或方法中创建的变量呢?”好吧,幸运的是,Dagron 就要告诉我们答案了!
全局变量
“让我们从全局变量开始,它可以在程序的任何地方被访问。举个例子可能会有帮助,”Dagron 说,她用爪子触碰了计算装置的屏幕:
>> **$location = 'The Carmine Pines!'**
>> **def where_are_we?**
>> **puts $location**
>> **end**
>> **where_are_we?**
The Carmine Pines!
=> nil
“这里,”Dagron 说,“我们创建了一个名为$location的变量,它的值是字符串'The Carmine Pines!'。然后我们创建了一个方法,where_are_we?,它尝试访问$location。通常情况下,这不会起作用,但因为$location是一个全局变量,我们在调用where_are_we?方法时会得到'The Carmine Pines!'!”
“啊哈!我以前见过这种变量,”Off-White Knight 说。“我能通过它前面的美元符号认出来!全局变量可以很有用,因为它们可以在 Ruby 程序的任何地方被访问。你可以在方法外定义全局变量,在方法内定义,在类中定义,随便你想在哪里定义,而且如果你在程序的其他地方使用它,它也能正常工作。但,”她举起一根手指说,“如果变量可以在程序的任何地方被访问,它也可以在程序的任何地方被更改,而且你不总是能明确知道何时或者如何发生了这种变化。”
Scarlet 点点头。“没错!”她说。“记得我们发现有东西正在改变 Flowmatic Something-or-Other 中的变量吗?想象一下,如果我们所有的变量都能在程序的任何地方随时被更改,那会有多糟糕!”
“想都别想!”国王打了个冷战说。“我们当然不想要那个。好吧,那如果可以避免,我们就不使用全局变量!那我们可以使用其他类型的变量吗?”
类变量
“明智的选择,陛下,”达格龙说道。“我们还可以使用另一种类型的变量,那就是类变量,它非常有用,特别是当我们希望一个类保存一些关于自己的信息时。就像所有全局变量都以$开头一样,所有类变量都以@@开头,且一个类可以有任意多个类变量。类变量可以被类内部和类的任何实例访问;所有实例共享同一个类变量。现在,韦尔福,我们来用你的巫师例子。”她对着计算装置吹了个烟圈,屏幕上出现了这段代码:
weezard.rb
class Weezard
@@spells = 5
def initialize(name, power='Flight')
@name = name
@power = power
end
def cast_spell(name)
if @@spells > 0
@@spells -= 1
puts "Cast #{name}! Spells left: #{@@spells}."
else
puts 'No more spells!'
end
end
end
“我们定义了一个Weezard类,其中有一个类变量@@spells,”达格龙说道,“还有两个方法:initialize,它为特定的巫师设置名字和能力;cast_spell,任何巫师都可以使用。现在,我们使用new来创建两个具有特殊能力的新巫师。别忘了先load你刚刚写的代码!”
>> **load 'weezard.rb'**
=> true
>> **merlin = Weezard.new('Merlin', 'Sees the future')**
=> #<Weezard:0x00000104949260 @name="Merlin", @power="Sees the
future">
>> **fumblesnore = Weezard.new('Fumblesnore', 'Naps')**
=> #<Weezard:0x0000010494c500 @name="Fumblesnore", @power="Naps">
“这就是我们这些巫师有趣的地方,”达格龙继续说道。“即便是Merlin和Fumblesnore有不同的能力,它们却在操作同一个变量@@spells!每当它们使用cast_spell时,法术变量就会减少一。看看这个。”
>> **merlin.cast_spell('Prophecy')**
Cast Prophecy! Spells left: 4.
=> nil
>> **fumblesnore.cast_spell('Nap')**
Cast Nap! Spells left: 3.
=> nil
“所以当你创建一个类变量时,整个类只有一个副本,而你创建的所有实例都共享这个类变量?”鲁本问道。
“没错,”达格龙说道。
“所有巫师共享固定的法术组,听起来有点奇怪,不是吗?”韦尔福问道。“是不是每个巫师都有自己的一套法术更合理?”
实例变量
达格龙点了点头。“有时候,创建对象的类需要跟踪某些信息,但并不是每次都这样,”她说。“因此,我们在 Ruby 中并不常使用类变量;我们更多使用的是实例变量和局部变量。事实上,通过实例变量,我们可以为每个巫师提供她自己的法术集,”达格龙继续说道,屏幕上出现了更多代码。“实例变量可以被类内部以及类的任何实例访问,就像类变量一样。大区别在于,每个实例都有自己独立的变量副本!”
weezard_2.rb
class Weezard
def initialize(name, power='Flight')
@name = name
@power = power
@spells = 5
end
def cast_spell(name)
if @spells > 0
@spells -= 1
puts "Cast #{name}! Spells left: #{@spells}."
else
puts 'No more spells!'
end
end
end
“看看我们是如何将@@spells变量从一个属于类的变量移到initialize方法中的@spells实例变量的吗?”达格龙问道。“以@开头的变量是实例变量。之所以称为实例变量,是因为每个实例,也就是 Ruby 中由类创建的对象,都有自己的副本。”
“所以当我们用new方法创建Weezard类的实例时,每个实例都会分配到自己的@spells变量吗?”斯嘉丽问道。
“正是如此,”达格龙说道。“事实上,我们现在就来做这个。我们将像之前一样创建我们的巫师。”
>> **load 'weezard_2.rb'**
=> true
>> **merlin = Weezard.new('Merlin', 'Sees the future')**
=> #<Weezard:0x0000010459e160 @name="Merlin", @power="Sees the
future", @spells=5>
>> **fumblesnore = Weezard.new('Fumblesnore', 'Naps')**
=> #<Weezard:0x000001045a13d8 @name="Fumblesnore", @power="Naps",
@spells=5>
“这看起来就像上次我们创建巫师时的样子!”国王抱怨道。
“非常相似,”达格龙承认,“但确实有一个重要的区别。看看每个巫师施法时发生了什么!”
>> **merlin.cast_spell('Prophecy')**
Cast Prophecy! Spells left: 4.
=> nil
>> **fumblesnore.cast_spell('Nap')**
Cast Nap! Spells left: 4.
=> nil
“它们每个都有自己的@spells变量!”斯嘉丽说。“这就是为什么fumblesnore的法术次数在merlin施法时没有受到影响。”
“完全正确,”达格龙说。“尽管它们的@spells变量有相同的名字,每个实例都有自己的一份,所以它们不会互相冲突。不仅如此,因为类的实例总是可以访问它们的实例变量,所以我们在类的initialize方法中定义的任何实例变量都可以被新创建的对象使用。”
“这就是为什么我们在initialize方法定义中做像@name = name这样的事,”象牙白骑士说。“它确保当我们传入name参数时,每个实例都会在@name中保存一份。”
局部变量
“说到局部变量,”达格龙说,“我们来看看这些吧,好吗?它们应该很熟悉,但值得再看一眼。局部变量只能在它当前的作用域内看到,这意味着它只能在定义它的方法或类内看到。”
计算机装置屏幕上出现了新的代码:
>> **class YeOldeClass**
>> **local_variable = 'I only exist inside the class!'**
>> **end**
>> **puts local_variable**
NameError: undefined local variable or method `local_variable' for
main:Object
>> **def yet_another_method**
>> **another_local = 'I only exist inside this method!'**
>> **end**
>> **puts another_local**
NameError: undefined local variable or method `another_local' for
main:Object
“所以实际上,局部变量只能在它们定义的类或方法内部看到,或者我们可以在所有类和方法定义之外使用它们,”斯嘉丽说道。
“没错,”达格龙说。“Ruby 中有一个特殊的作用域,叫做顶级作用域,所以如果你在任何方法或类定义之外定义局部变量,Ruby 就能看到它们。看看这个!”
>> **local_variable = "I'm the top-level local variable!"**
>> **def local_in_method**
>> **local_variable = "I'm the local variable in the method!"**
>> **puts local_variable**
>> **end**
>> **puts local_variable**
I'm the top-level local variable!
=> nil
>> **local_in_method**
I'm the local variable in the method!
=> nil
“你看到了吗?”达格龙说。“局部变量甚至可以有完全相同的变量名,只要它们在不同的作用域中!Ruby 知道方法定义会有自己的一组局部变量,所以它不会抱怨有两个同名的变量。”
“所以局部变量只能在我们定义它们的类或方法中,或者在这个特殊的顶级作用域中看到,”国王说。“但全局变量可以在任何地方看到,而且如果我们创建了一个类的实例,实例可以看到我们在定义类时创建的任何实例变量。”
“正是如此,”达格龙说。
“而类可以看到它自己的类变量,”国王继续说道。
“正确!”达格龙说。“事实上,不仅实例可以有像initialize、introduce和sing这样的的方法;类也可以有它们自己的方法!”
“就在我开始理解的时候!”国王抱怨道。“这是怎么可能的?”
“因为,”达格龙回答道,“Ruby 类也是对象!”
“我需要坐下了,”国王说道。
“你确实坐下了,”在场的问道。
“是的,”国王说道,他盘腿坐在象牙白骑士和漫游歌手之间。“继续吧,达格龙女士,”他说。“我们如何能直接将一个方法添加到类本身,而不仅仅是类的一个实例呢?”
对象和 self
“嗯,”达格龙说道,“Ruby 始终保持一个名为self的特殊内置变量,而self指代的是我们当前谈论的 Ruby 对象。”她开始快速讲解,嘴里冒出小小的火花。“所以我们所需要做的就是使用self来定义类中的方法,而不是将该方法添加到实例上,而是将它添加到类本身。”
“也许举个例子会更清楚些,”白色骑士说。她伸手过去,开始在计算机装置上打字:
monkey.rb
class Monkey
@@number_of_monkeys = 0
def initialize
@@number_of_monkeys += 1
end
def self.number_of_monkeys
@@number_of_monkeys
end
end
“这里我创建了一个Monkey类,”骑士说道。“它有一个@@number_of_monkeys类变量,用来跟踪我们创建了多少个猴子实例,还有我们在之前的类中看到的initialize方法。当我们对Monkey调用new来创建一个新猴子时,它会把@@number_of_monkeys加 1。”
“那那个self.number_of_monkeys方法呢?”鲁本问道。
“那是一个类方法!”骑士说道。“这是Monkey类本身的方法,当我们调用它时,它将返回@@number_of_monkeys。我们来看看吧!首先,我们加载那个脚本,然后创建几个猴子。”
>> **load 'monkey.rb'**
=> true
>> **monkey_1 = Monkey.new**
=> #<Monkey:0x000001048fccf8>
>> **monkey_2 = Monkey.new**
=> #<Monkey:0x00000104902310>
>> **monkey_3 = Monkey.new**
=> #<Monkey:0x00000104907900>
“很好!”白色骑士说。“现在我们有了猴子,让我们问问Monkey类有多少只猴子。”她在计算机装置上打字:
>> **Monkey.number_of_monkeys**
=> 3
“太棒了!”Wherefore 说道。“但是为什么不直接问一个猴子有多少只猴子呢?”
“嗯,”骑士说道,“首先,问一个猴子实例有多少其他实例是没有意义的——那是类的事情,而不是实例的!但更重要的是,因为我们在定义number_of_monkeys方法时用了self,它仅仅是类的方法,而不是实例的方法!看见了吗?”她继续打字:
>> **monkey_1.number_of_monkeys**
NoMethodError: undefined method `number_of_monkeys' for
#<Monkey:0x000001048fccf8>
“看!现在Monkey类有了自己的number_of_monkeys方法,但它只属于类本身;猴子实例没有这个方法。”

“事实上,”骑士说道,“向类添加方法是很常见的,Ruby 为此提供了更简洁的语法。它看起来像这样!”她继续打字:
monkey_2.rb
class Monkey
@@number_of_monkeys = 0
def initialize
@@number_of_monkeys += 1
end
class << self
def number_of_monkeys
@@number_of_monkeys
end
end
end
“看到吗?”她问道。“我没有在类中通过self.number_of_monkeys来定义number_of_monkeys方法,而是使用了class << self来告诉 Ruby:‘嘿!我定义的每个方法,直到我说end为止,都是类的方法,而不是实例的方法。’看看当我在Monkey上调用这个方法而没有创建任何实例时会发生什么。”
>> **load 'monkey_2.rb'**
=> true
>> **Monkey.number_of_monkeys**
=> 0
“现在看看,如果我创建一个实例并再次调用这个方法会发生什么,”骑士说道。
>> **monkey = Monkey.new**
=> #<Monkey:0x0000010490af60>
>> **Monkey.number_of_monkeys**
=> 1
“看到了吗?这就像使用self.number_of_monkeys一样,”白色骑士说,露出灿烂的笑容。
“真有趣,”达格龙说道。“我从没见过class << self。”
“真的?”Wherefore 问道。
“没人知道所有的事情,”达格龙说。“连我也不行!”
“许多人觉得def self.method_name语法更容易理解,”骑士说道,“所以每当你需要为一个类添加方法时,使用这个语法是完全没问题的。”
“当然,”斯嘉丽说,“现在self对我来说好多了!它只是指 Ruby 程序‘正在谈论’的对象。而在这种情况下,self就是我们所在的类!”
方法和实例变量
“完全正确,”达格龙说道。“有了这个,我还有一个技巧要展示给你们。你们看,虽然为我们的实例创建实例变量非常容易,但想要访问它们并不总是那么简单。明白我的意思了吗?”她说道,在她说话时,新的代码开始填满屏幕:
>> **class Minstrel**
>> **def initialize(name)**
>> **@name = name**
>> **end**
>> **end**
“我重新创建了我们之前的Minstrel类,但只包含一个initialize方法,”达格龙说道。“没有introduce或sing方法!让我们像之前一样创建一个实例。”
>> **wherefore = Minstrel.new('Wherefore')**
=> #<Minstrel:0x000001049637c8 @name="Wherefore">
“现在,”达格龙说道,“看我们的吟游诗人实例是如何拥有‘Wherefore’这个名字的?(你可以通过@name="Wherefore"这一部分看出来。)让我们试着去访问它。”
>> **wherefore.name**
NoMethodError: undefined method `name' for
#<Minstrel:0x000001049637c8 @name="Wherefore">
“你看,”达格龙说道,“虽然wherefore有一个@name实例变量,但它没有name方法。在 Ruby 中,所有重要的是方法。为了让wherefore.name真正起作用,我们需要写一个方法来访问@name实例变量。”
“那是不是意味着我们需要在Minstrel类中定义一个叫做name的方法?”斯嘉丽问。
“完全正确,”达格龙说道,屏幕上的代码在她的爪子下发生了变化:
another_minstrel.rb
class Minstrel
def initialize(name)
@name = name
end
def name
@name
end
end
“现在我们有了一个返回@name实例变量的name方法,”达格龙说道。“让我们看看当我们创建一个带有这个name方法的新吟游诗人并尝试使用它时会发生什么!”
>> **load 'another_minstrel.rb'**
=> true
>> **wherefore = Minstrel.new('Wherefore')**
=> #<Minstrel:0x000001049637c8 @name="Wherefore">
>> **wherefore.name**
=> "Wherefore"
“万岁!”国王喊道。“我们成功了!我们通过name方法改变了吟游诗人的名字。”
“真是太棒了,”Wherefore 说道,“但是如果我们想把吟游诗人的名字改成别的呢?”
“好吧,”达格龙说道,“让我们看看能不能用现在的代码做到这一点。”她在计算装置的发光屏幕上添加了更多代码:
>> **wherefore.name = 'Stinky Pete'**
NoMethodError: undefined method `name=' for
#<Minstrel:0x000001049637c8 @name="Wherefore">
“我们可以获取名字,”达格龙说道,“但我们不能改变它;Ruby 抱怨我们的实例没有方法可以改变名字。它在寻找一个我们还没写的方法!”
鲁本仔细研究了屏幕。“又是那个NoMethodError,”他说。“看起来 Ruby 想让Minstrel类有一个叫做name=的方法!”
达格龙点了点头。“如果我们想要改变@name,我们需要写一个名为name=的特殊方法,”她说。“如果你在方法名后面加上等号,Ruby 会理解为:‘我想让这个方法改变某个东西的值。’所以为了改变@name,”她补充道,“我们需要添加一些额外的代码。”
她将name=方法添加到剩余的代码中,大家都看到了:
another_minstrel_2.rb
class Minstrel
def initialize(name)
@name = name
end
def name
@name
end
def name=(new_name)
@name = new_name
end
end
“现在我们有了一个新的方法,name=,它接受一个参数,new_name,”达戈龙说道。“这应该告诉 Ruby,允许我们通过调用wherefore.name = 'some new name'来更改名字!我们来试试。首先,我们创建一个新的吟游诗人。”
>> **load 'another_minstrel_2.rb'**
=> true
>> **wherefore = Minstrel.new('Wherefore')**
=> #<Minstrel:0x000001049637c8 @name="Wherefore">
>> **wherefore.name**
=> "Wherefore"
“接下来,我们将尝试更改它的名字。”
>> **wherefore.name = 'Stinky Pete'**
=> "Stinky Pete"
>> **wherefore.name**
=> "Stinky Pete"
“太棒了!”鲁本说道。“不过写这些方法来获取和设置实例变量真是辛苦。有没有更快的方法呢?”
达戈龙点了点头。“其实是有的,”她说。“有三种内置的快捷方法来读取和写入实例变量:attr_reader、attr_writer和attr_accessor。它们是这样工作的。”她用爪子触碰了计算装置,出现了这些文字:
another_minstrel_3.rb
class Minstrel
attr_accessor :name
attr_reader :ballad
def initialize(name)
@name = name
@ballad = 'The Ballad of Chucky Jim'
end
end
“举个例子,如果你将符号:name传递给attr_reader,它会自动创建一个叫做name的方法,用来读取实例变量@name。attr_writer会自动创建一个叫做name=的方法,用来改变@name的值,而attr_accessor则会同时创建name和name=这两个方法。”达戈龙点击了她的爪子。“在这个例子中,我用:name调用了attr_accessor,用:ballad调用了attr_reader,这意味着我可以既获取又更改吟游诗人的名字,但只能读取他的ballad,而不能修改它。让我们创建一个新的吟游诗人来测试一下。”
>> **load 'another_minstrel_3.rb'**
=> true
>> **wherefore = Minstrel.new('Wherefore')**
=> #<Minstrel:0x0000010413c0e0 @name="Wherefore", @ballad="The
Ballad of Chucky Jim">
“太完美了,”达戈龙说道。“让我们看看attr_accessor能不能让我们像之前一样获取和更改那位吟游诗人的name。”
>> **wherefore.name**
=> "Wherefore"
>> **wherefore.name = 'Wherefive'**
=> "Wherefive"
>> **wherefore**
=> #<Minstrel:0x0000010413c0e0 @name="Wherefive", @ballad="The
Ballad of Chucky Jim">
“现在让我们看看是否能读取吟游诗人的ballad,但不改变它;这就是attr_reader应该做的事情,”达戈龙说道。她在计算装置上填入了更多代码:
>> **wherefore.ballad**
=> "The Ballad of Chucky Jim"
>> **wherefore.ballad = 'A Song of Mice and Friars'**
NoMethodError: undefined method `ballad=' for
#<Minstrel:0x0000010413c0e0>
Wherefore 震惊地摇了摇头。“太不可思议了!”他说。“有了这些 Ruby 工具,我马上就能写出歌曲来。”
“这是 Ruby 中最神奇的部分之一,”Off-White 骑士说道。“当我们围绕对象设计程序时,我们就在做一种叫做面向对象编程的事情,它让我们能够编写描述现实世界事物(如吟游诗人和歌曲)的程序。一切都变得轻松了千倍!”
“这太棒了,真是太棒了,”Wherefore 说道。“我真是不知道该怎么感谢你们。怎么才能报答你们呢?”
“嗯,”斯嘉丽说,“其实,我们是来找你问问,是否注意到王国里发生了什么异常的事情。王国各地的 Ruby 系统整整一天都在崩溃,我们开始觉得这些问题可能不是偶然的。”
“把鳞片给他看!”鲁本说。
“哦,太棒了!”斯嘉丽说着,从口袋里拿出了那片闪闪发光的绿色鳞片。“你见过这样的东西吗?我们一开始以为它可能属于达戈龙,但我们检查过了,她并没有少一片。”
“嗯,”Wherefore 说,“真是个难题。不,我想我从没见过有什么生物有像这样的鳞片,但我确实在一小时前看到过一些奇怪的东西,在松树林里。”
国王、鲁本和斯卡利特交换了吃惊的眼神。
“是什么?”斯卡利特问。
“嗯,”威尔福说,“我只听到了一小段对话,不过是几个人低声在那片灌木丛后说话。我去看看发生了什么,但当我靠近时,他们就跑了——三个人,可能四个,”他说道。

“他们是谁?”国王问。
“我没看清,”威尔福说,“但我听到的那部分确实相当卑鄙。他们说什么没造成足够的影响,打算去找王后谈谈他们正在做的事情。我敢打赌,当他们逃跑时,肯定是直奔城堡去了!”
“城堡!王后!”国王喊道。“天哪,天哪!如果这些就是我们的破坏者,王后可能处于极大的危险之中!”
“我们必须尽快回去!”斯卡利特说道。“白骑士,达格龙,你们能帮我们吗?”
骑士若有所思地皱起了眉头。“我有责任留在松林中,帮助任何迷路的人,”她说,“但我的职责同样属于国王和王后。我可以尽快传达消息,告诉大家麻烦已经出现,并派遣尽可能多的朋友去城堡!”
“请说!”斯卡利特说道,“那你呢,达格龙?”
达格龙摇了摇头。“魔法和智慧都有代价,”她说,“我不能离开红松林。但是,有一条通往城堡的捷径。”
“哪里?”鲁本问。
“地下通道!”威尔福说。“是的,我知道。跟我来,我带你们去!”
国王、斯卡利特和鲁本感谢了白骑士和达格龙,挥手告别后急忙赶上已经走到草地半程的威尔福。它们都飞快地沿着一条弯曲的小路冲过去,路旁是树根和交织的树干,几分钟气喘吁吁的奔跑后,他们来到了那棵巨大的红松树,比周围的任何一棵都大,远远望去一眼难见尽头。
“下去了!”威尔福喊道,并且在树干上敲了三下。随着一声愉快的叮,一扇门在树干侧面打开,露出了一个狭小的电梯厢。
“乘电梯下到次次地下室,”他说着,把他们三个塞了进去。“你会看到一条狭长的通道,通向西方。走到尽头后,找一个大黑管子。上面会写着——”
“—神秘的管道!”鲁本和斯卡利特异口同声地喊道。“我们今天早些时候看到哈尔多消失在城堡的下层,这条通道肯定是通向同一个地方!”
“那你就知道路了!”威尔福说。“再见,祝好运——同时,我会帮白骑士和达格龙尽快把援助送到你们那儿。”话音刚落,电梯门紧闭,国王、斯卡利特和鲁本开始向地下深处下降。
《拨打旋律》,或者说是“吟游诗人的快递服务”
现在我们已经向《四处流浪的歌手》讲解了 Ruby 中的对象和类,是时候帮助他创建他自己的Ballad(歌曲)了!否则,他就不算是一个真正的歌手了。不过别担心——既然你已经了解了类及其工作原理,创建一个简单的类来帮助《四处流浪的歌手》创作更快、更好的歌曲将不再是难题。
让我们从创建一个新的文件 ballad.rb 并输入以下代码开始。
ballad.rb
class Ballad
➊ attr_accessor :title
attr_accessor :lyrics
➋ @@number_of_ballads = 0
➌ def initialize(title, lyrics='Tralala!')
@title = title
@lyrics = lyrics
@@number_of_ballads += 1
end
➍ def self.number_of_ballads
@@number_of_ballads
end
end
➎ ballad = Ballad.new('The Ballad of Chucky Jim')
➏ puts "Number of ballads: #{Ballad.number_of_ballads}"
puts "Ballad object ID: #{ballad.object_id}"
puts "Ballad title: #{ballad.title}"
puts "Ballad object ID again!: #{ballad.object_id}"
puts "Ballad lyrics: #{ballad.lyrics}"
难以置信的是,现在你已经学会了这么多 Ruby,实际上这里没有什么新内容!你以前见过这些东西:创建类和类的实例,使用attr_accessor,使用类和实例变量,给类和实例添加方法,所有这些。让我们逐行查看并看看输出。
首先,我们在 ➊ 创建一个Ballad类,拥有一个title(标题)和lyrics(歌词),我们可以读取并修改它们(感谢attr_accessor)。
接下来,在 ➋,我们设置了一个类变量@@number_of_ballads,用来跟踪我们的类创建了多少首歌曲,而我们的initialize方法在 ➌ 同时设置歌曲的名称和歌词,并将@@number_of_ballads加 1。
对于类定义的最后部分,我们在 ➍ 为Ballad类本身添加了一个number_of_ballads方法,这将让我们稍后访问@@number_of_ballads。
最后,我们在 ➎ 使用Ballad.new创建一首新的歌曲,然后在 ➏ 打印出一些关于我们歌曲的有趣事实。
你可以通过使用终端进入保存ballad.rb文件的文件夹,然后在命令行输入ruby ballad.rb来运行文件中的代码。
你的对象 ID 会和我的稍微不同,但你应该能看到类似这样的内容:
Number of ballads: 1
Ballad object ID: #<Ballad:0x0000010413e0e0>
Ballad title: The Ballad of Chucky Jim
Ballad object ID again!: #<Ballad:0x0000010413e0e0>
Ballad lyrics: Tralala!
=> nil
我们刚刚证明了self.number_of_ballads方法有效,我们的对象 ID 在创建对象后不会改变,并且通过attr_accessor的魔力,我们可以访问我们在歌曲中存储的所有信息。
这些都没问题,但真正有趣的部分是如何进一步拓展它!例如,你可以从小处开始,编写代码来修改你创建的歌曲的标题,或者在创建后更新其歌词。(你觉得这会改变对象 ID 吗?)
你还可以添加更多的attr_reader、attr_writer或attr_accessor。你可以添加更多的方法(比如创建一个playing_time方法来返回歌曲的时长是多少分钟?)。你还可以添加类方法或创建额外的歌曲。
你甚至可以迎接最大的挑战:实际写出《查基·吉姆的歌》!世界是你的牡蛎。(如果你不喜欢牡蛎,那世界就是你的杯子蛋糕。)
你知道这些!
你在这一章学到了不少内容,但远远不及你在学习方法时那样充实!了解对象和类几乎就像度假一样轻松!即便如此,我们还是花点时间再回顾一遍,确保你已经完全掌握了。
对象和类
你已经知道几乎所有的东西在 Ruby 中都是对象,但在这一章中,你学习了更多关于对象的内容,并仔细查看了对象 ID。对象的 ID 号就像指纹:每个对象都有自己独一无二的 ID,两个对象不会有完全相同的 ID。一般来说,Ruby 创建的对象 ID 号比你创建的对象 ID 号要低:
>> **0.object_id**
=> 1
>> **:minstrel.object_id**
=> 465608
我们还看到,类是我们创建一堆具有相似特征的对象的方式。我们通过class关键字创建类,如下所示:
>> **class Monkey**
>> **# Class magicks go here!**
>> **end**
创建类本身很好,但类在我们实例化(创建)该类的对象之前,并不会为我们做太多事情。你可以把类想象成饼干模具,把它们创建的对象想象成饼干:饼干模具(类)做了一堆非常相似的东西,但我们最感兴趣的其实是饼干本身(对象)。
比如,我们可以用class关键字定义一个Monkey类,然后通过调用Monkey.new来实例化它——也就是从我们的Monkey类饼干模具中做出一个特定的猴子:
monkey_review.rb
class Monkey
@@bananas = 5
def initialize(name)
@name = name
end
def eat_banana
@@bananas -= 1
puts "Ate a banana! #{@@bananas} left."
end
end
很好!到目前为止,我们已经有了一个Monkey类,里面有两个方法和一个类变量。类变量@@bananas跟踪所有猴子实例的香蕉数量,initialize方法在调用Monkey.new时设置猴子的名字,eat_banana方法将@@bananas减少 1。
接下来,让我们创建几只猴子:
>> **load 'monkey_review.rb'**
=> true
>> **socks = Monkey.new('Socks')**
=> #<Monkey:0x000001052c77b0 @name="Socks">
>> **stevie = Monkey.new('Stevie')**
=> #<Monkey:0x00000104ca38e8 @name="Stevie">
现在我们可以让每只猴子吃个香蕉,看看会发生什么:
>> **socks.eat_banana**
Ate a banana! 4 left.
=> nil
>> **stevie.eat_banana**
Ate a banana! 3 left.
=> nil
你注意到每次任何猴子实例吃香蕉时,我们的Monkey类的@@bananas类变量都会减少吗?记住,这是因为类变量是该类所有实例共享的。
我们可以在类中结合使用局部变量、实例变量、类变量和全局变量,如下所示:
monkey_review_2.rb
class Monkey
$home = 'the jungle'
@@number_of_monkeys = 0
def initialize(type)
@type = type
@@number_of_monkeys += 1
puts "Made a new monkey! Now
end
end
在这里,我们已经把Monkey类修改为拥有一个全局的$home变量('the jungle'),一个@@number_of_monkeys类变量,用来跟踪Monkey类创建了多少个实例,还有一个@type实例变量,让每只猴子都有不同的类型。
>> **load 'monkey_review_2.rb'**
=> true
>> **blue = Monkey.new('blue monkey')**
Made a new monkey! Now there's 1.
=> #<Monkey:0x00000104aafb40 @type="blue monkey">
>> **silver = Monkey.new('silver monkey')**
Made a new monkey! Now there's 2.
=> #<Monkey:0x00000104ab3b28 @type="silver monkey">
>> **gold = Monkey.new('golden monkey')**
Made a new monkey! Now there's 3.
=> #<Monkey:0x00000104ab7c00 @type="golden monkey">
看看每个@type是如何对每只猴子独一无二的,但它们都在改变同一个@@number_of_monkeys变量吗?
最后,由于$home是全局变量,程序的每个部分也都可以访问它:
>> **puts "Our monkeys live in #{$home}."**
Our monkeys live in the jungle.
=> nil
变量和作用域
这一切可能有点难以理清楚,所以我创建了下面这张方便的表格来帮助你记住局部、全局、实例和类变量之间的区别。
| 变量类型 | 形式 | 能在哪里看到? |
|---|---|---|
| 局部 | odelay |
在定义它的顶级作用域、方法或类内部。 |
| 全局 | $odelay |
任何地方! |
| 实例 | @odelay |
在定义它的类内部或该类的任何实例中。每个实例都有自己的副本。 |
| 类别 | @@odelay |
在类内部或者类的任何实例中。每个实例与所有其他实例共享相同的类变量。 |
请记住,通常不建议使用全局变量,因为它们不仅在程序中的任何地方都是可见的,而且还可以在程序中的任何地方被修改。当变量可以在许多地方被修改时,若发生意外情况,可能很难弄清楚是程序中的哪个部分做出了修改。我展示给你全局变量,是为了让你了解它们是什么以及如何工作,但在几乎所有情况下,它们带来的麻烦远大于它们的价值。
正如你在上一个示例中看到的,我们可以从类定义外部访问$home变量,因为它被定义为全局变量(全局变量以$开头)。我们只有在变量处于正确的作用域中时,才能访问它。让我们回顾一下本章早些时候的一些示例:
>> **local_variable = 'Local here!'**
=> "Local here!"
我们的local_variable存在于这个外部作用域中,但它在类定义内部并不存在:
>> **class OutOfTowner**
>> **puts local_variable**
>> **end**
NameError: undefined local variable or method `local_variable' for
OutOfTowner:Class
local_variable在方法定义内部也不存在!
>> **def tourist**
>> **puts "Can you take our picture, #{local_variable}?"**
>> **end**
>> **tourist**
NameError: undefined local variable or method `local_variable' for
main:Object
我们的变量number存在于块内,但一旦块的代码执行完,它就消失了:
>> **3.times { |number| puts number }**
0
1
2
=> 3
>> **puts number**
NameError: undefined local variable or method `number' for
main:Object
我们发现 Ruby 有一个内建变量self,它指代方法将要调用的对象,我们可以使用self直接向类中添加方法(而不仅仅是向它们创建的对象添加方法),如下所示:
monkey_review_3.rb
class Monkey
@@number_of_monkeys = 0
def initialize
@@number_of_monkeys += 1
end
def self.number_of_monkeys
@@number_of_monkeys
end
end
你之前见过这个!这是我们的Monkey类,它有一个@@number_of_monkeys类变量,一个initialize方法,每次我们创建一个新的猴子时都会增加这个变量,还有一个self.number_of_monkeys方法,这意味着我们可以调用Monkey.number_of_monkeys来查看我们到目前为止创建了多少个猴子:
>> **load 'monkey_review_3.rb'**
=> true
>> **Monkey.number_of_monkeys**
=> 0
它现在是0,但是如果我们创建一个猴子,我们会看到这个数字上升!
>> **monkey = Monkey.new**
=> #<Monkey:0x0000010490af60>
>> **Monkey.number_of_monkeys**
=> 1
如果你不确定程序中特定部分的self值是什么,你可以随时使用puts self来查看它是什么。
我们还学到,如果一个对象有一个实例变量,我们想查看或修改它,我们必须编写方法来实现。我们可以像下面这样自己编写这些方法:
minstrel_review.rb
class Minstrel
def initialize(name)
@name = name
end
def name
@name
end
def name=(new_name)
@name = new_name
end
end
在这里,我们在initialize方法中设置了@name,这意味着每次我们调用Minstrel.new时,我们都会传入该游吟诗人的名字。name方法会获取这个@name变量,而name=方法允许我们为游吟诗人分配一个new_name . . .
. . . 但我们也可以使用快捷方式attr_reader(用于读取实例变量)、attr_writer(用于修改实例变量)和attr_accessor(同时做两者)。我们所做的就是将实例变量名作为符号传递,例如:
minstrel_review_2.rb
class Minstrel
attr_accessor :name
attr_reader :ballad
def initialize(name)
@name = name
@ballad = 'The Ballad of Chucky Jim'
end
end
在这里,我们使用了attr_accessor并传入:name符号,它会自动为我们创建name和name=方法;我们用attr_reader和:ballad调用,所以我们只会得到一个读取@ballad实例变量的ballad方法。看看如果我们尝试更改我们的歌谣会发生什么!
>> **load 'minstrel_review_2.rb'**
=> true
>> **wherefore = Minstrel.new('Wherefore')**
=> #<Minstrel:0x0000010413c0e0 @name="Wherefore", @ballad="The
Ballad of Chucky Jim">
>> **wherefore.ballad**
=> "The Ballad of Chucky Jim"
>> **wherefore.name**
=> "Wherefore"
>> **wherefore.name = 'Wherefive'**
=> "Wherefive"
>> **wherefore.ballad = 'A Song of Mice and Friars'**
NoMethodError: undefined method `ballad=' for
#<Minstrel:0x0000010413c0e0>
面向对象编程
最后,我们学到,编写围绕类和对象展开的程序叫做面向对象编程(OOP)。我们的minstrel是一个很好的对象示例:一段行为像现实世界中的某个东西的代码!它有属性(关于它自己的事实)以及行为,后者就是指对象知道如何使用的方法。我们可以用Minstrel类来定义任何吟游诗人的行为,如下所示。
minstrel_review_3.rb
class Minstrel
attr_reader :name
@@number_of_minstrels = 0
def initialize(name)
@name = name
@@number_of_minstrels += 1
end
def sing(song_name)
puts "Time to sing a song called: #{song_name}!"
puts 'Tralala!'
end
def self.number_of_minstrels
@@number_of_minstrels
end
end
我们的类有一个attr_reader,用于:name(这意味着我们可以读取名称,但不能修改它),还有一个@@number_of_minstrels类变量,用于跟踪我们创建了多少实例,以及一个initialize方法,给我们的吟游诗人起名字并增加@@number_of_minstrels。
它还有两个方法:一个是sing,是一个吟游诗人实例的方法,演唱一首小歌;另一个是self.number_of_minstrels,是Minstrel类的方法,告诉我们到目前为止我们创建了多少个吟游诗人。
让我们看看它们的实际应用吧!
>> **load 'minstrel_review_3.rb'**
=> true
>> **wherefore = Minstrel.new('Wherefore')**
=> #<Minstrel:0x000001031eac68 @name="Wherefore">
>> **Minstrel.number_of_minstrels**
=> 1
wherefore.sing('A Tail of Two Foxes')
Time to sing a song called: A Tail of Two Foxes!
Tralala!
=> nil
看!我们可以创建一个新的吟游诗人,调用Minstrel.number_of_minstrels查看我们创建了一个,然后调用我们的吟游诗人实例(wherefore)的sing方法,听他唱他的“狐狸的故事”。
事情开始变得有些悬疑了,所以我要去拿一包爆米花—马上回来。与此同时,去看看国王、Scarlet 和 Ruben 回到城堡后会发现什么,并准备好迎接更多面向对象的 Ruby 魔法!
第九章 继承红宝石的魔法
她陛下的珍奇动物园
国王、鲁本和斯嘉丽沿着地下通道向城堡的方向快速奔跑。
“还要多远?”鲁本气喘吁吁地问。
“我不确定,”国王说道,“但 Wherefore 告诉我们尽可能走远,然后我们就能到达神秘的管道。”他思索了一下。“不过应该不会太远,”他最终说道,“这是安布罗斯洞窟,虽然它们延伸至整个王国的地下,但我知道哈尔多可以在几分钟内穿越城堡和比松树林更远的地方。”
“没错!哈尔多熟悉这些隧道,”斯嘉丽说道。她沉默地跑了几分钟。“如果……会怎样……”她开口说道。
“什么如果?”鲁本问道。
“那么,如果引发这一切麻烦的人是哈尔多让进来的呢?或者如果哈尔多是他们中的一员呢?”
“闭嘴!”国王说道,“哈尔多从小时候就跟着我,他绝不会做任何伤害我们或这个王国的事!”
“我们应该考虑所有可能性,”斯嘉丽说道。
“即便如此,”国王说道,“所有嫌疑人都是无辜的,直到我们证明他们有罪。如果我们运气好,等我们到达城堡时,能当场抓到这些恶棍!”
“随时都会的,”鲁本说道。“看!”
前方,狭窄的隧道展开成一个宽阔的洞窟。国王、斯嘉丽和鲁本跑进开阔的空间,然后停下片刻,喘着气。
“就是这里,”国王说道,“城堡下方的地下室下面!现在我们只需要找到神秘的管道,然后爬回到我的皇家书房。”
“就是这里!”斯嘉丽说道。在黑暗中,他们勉强看见远角落里神秘管道的轮廓。
三人走到管道底部,管道正在轻轻地咕噜作响。
“现在怎么样?”鲁本问道。“神秘的管道里满是水!我们怎么爬上去?”
“我们之前打开了那个 Flowmatic 什么的,”斯嘉丽说道,“我们可以把它关掉!”她在管道底部摸索,直到找到那个熟悉的计算装置的方形形状,然后打开了它的盖子。IRB 提示的光芒照亮了他们的脸。
“对了!”国王说道,“我们之前改了什么变量?”
“flowmatic_on!”鲁本回答道。
斯嘉丽迅速在计算装置上输入:
>> **flowmatic_on = false**
=> false
随着一声缓慢的booooop和咕噜咕噜的声音,神秘的管道关闭并排空了。
“做得好,斯嘉丽!”国王说道,他走到管道的另一侧。他抓住一个突出的金属轮子,转动了几下。轮子转了好几圈,随着一声空洞的砰,它连接的门慢慢打开了。
“进入神秘的管道!”国王喊道,三人都爬了进去。
鲁本直视着管道顶部,眯着眼睛。“我连上面的光都看不见!”他说道,“这个管道太大了!哈喽!”他喊道,神秘管道回响着:哈喽! 喽! 哦!
“爬上去得费时费力,如果我们能做到的话!”斯嘉丽说道。她想了想。“我或许有个主意。”她转向鲁本和国王。“你们信任我吗?”她问道。
“凭我的生命!”国王说道。
“到终点!”鲁本说道。
“好吧,”斯嘉丽说道。“屏住呼吸!”她伸手到神秘管道旁边的计算机设备上,开始输入:
>> **flowmatic_on = true**
她猛地把金属门关上,瞬间,管道里充满了水。
几秒钟,三人漂浮在神秘管道底部,屏住呼吸。接着,整个管道微微颤动,随着一声深沉的嗖,水流的冲击力将国王、斯嘉丽和鲁本直接推了上去!

几秒钟后,他们三人开始放慢速度,发现自己漂浮在神秘管道狭窄顶部仅几英寸的地方。国王伸手按下管道门的门闩,随着一阵水流,三人跌跌撞撞地从门口掉进了国王的皇家书房。
“天才!绝对是天才!”国王急促地说道。
“谢谢你,”斯嘉丽说着,微微鞠了一躬。“但是我们得赶紧去找皇后!你知道她在哪儿吗?”
“她应该刚从皇家陛下的 Ruby 黑客大会回来,”国王说道,“所以我想她现在应该在她的皇家办公室里。走吧!”说完,他冲出了房间。
Ruby 和斯嘉丽紧随其后。“皇后参加了 Ruby 大会?”斯嘉丽一边跑上楼梯一边问。
“的确如此!”国王说道。“你可能会惊讶地发现,虽然我对这些 Ruby 的事情还很新,但我的妻子可是个不折不扣的黑客。”
“太棒了!”鲁本说道。“也许她能帮我们修好这些 Ruby 故障,抓住那些搞破坏的坏人。”
“我希望如此。啊!我们到了,”国王说道,急停在一扇带有金色把手的巨大木门前。
他同时拉下两个把手,猛地推开门,急匆匆地冲了进去。
皇后正坐在一把高背椅上,专心致志地敲打着计算机设备。
“他们试图闯入我的计算机设备!”皇后说道。“这简直不可理喻!”
“他们是谁?”斯嘉丽和鲁本齐声问道,跟着国王进入皇后的皇家办公室。
“我不知道!”皇后一边打字一边说道。“他们有四个人,我抓到他们在我的计算机设备前,试图破解我的密码。幸运的是,我对安全性要求非常严格。”
“她确实是,”国王说道,一边拧干他那蓬松的白胡子。“她甚至不让我在网上买橡皮糖!”
“有充分的理由,”皇后说道,停下了打字。“上次我让你做这事时,你竟然把一大笔钱给了一个自称是橡皮糖国王的人!”她停顿了一下,看了看斯嘉丽和鲁本。“我相信我们还没见过面,”她说道。“你们是?”她上下打量了他们三人。“你们为什么都这么湿?”
“我是鲁本,”鲁本说道,“这是斯卡雷特。我们在帮助国王找出是谁让 Ruby 出现故障的,我们从深红松树林一路跑来,游过神秘管道来帮助处理这件事!”
“好吧,你来对地方了!”女王说道。“如果这事我不做到底,那就没天理了。”她又开始打字。

“你有好好观察过他们吗?”斯卡雷特问道。“有没有什么线索?你看到或听到什么能帮助我们抓住他们的吗?”
“我没看到他们的脸,”女王说道,“但是我听到他们试图破解我的计算设备密码。他们有四个人——听起来是两个男孩和两个女孩。我从 Hacktastic Ruby 大会早早回来,想着试试我学到的一些 Ruby 技巧,当我走进办公室时,正好抓到他们作案!我大声喊叫他们投降,他们就跑了,我立刻派卫兵去追他们。与此同时,我一直在努力增加计算设备的安全性,确保它能完全防御攻击。”
“他们从你的计算设备上得到了什么吗?”斯卡雷特问道。
“谢天谢地,不是,”女王说道。“他们没拿到我的密码,但如果他们拿到,我们就麻烦大了。凭那个,他们可以不受任何限制地访问王国里的任何系统!”
“这些坏蛋越来越猖狂了!”国王在女王的办公室里来回踱步。“我们得尽快抓住他们,不能让他们再作恶。下次我们可能就不这么幸运了。”
女王点了点头。“我已经指示卫兵将任何他们抓到的嫌疑人直接带来我们这里审问,”她说道。“与此同时,我已经升级了我的计算设备的所有安全措施。剩下的就是更新我的一些 Ruby 程序,使它们也更安全!”
“Ruby!”鲁本说道。“那正好是我们的专长。我们能帮忙吗?”他问道。
女王微笑着说道。“那真是太好了,”她说道。“虽然我在不少编程语言上都有些造诣,但在 Ruby 上我还是个新手。”她在椅子上挪了挪,鲁本和斯卡雷特爬到她旁边坐下。
“首先,首先,”女王说道,“你们知道怎么创建 Ruby 类吗?”
复习一下类
“我想是的,”鲁本说道。“我可以创建任何我想要的类吗?”女王点了点头,鲁本在她的计算设备上输入了代码:
>> **class Animal**
>> **attr_accessor :name**
>>
>> **def initialize(name, legs=4)**
>> **@name = name**
>> **@legs = legs**
>> **end**
>> **end**
=> nil
“明白了!”女王说道。“你定义了一个 Animal 类。你使用了 attr_accessor 来自动生成一个方法,用来访问动物的 name,而 initialize 方法则在每次创建新动物时,设置动物的 @name 和 @leg 数量。”
“没错!”鲁本说道。“如果我们用 Animal.new 创建一个动物,但没有指定它的腿数,它默认会是 4 条腿。”
女王点了点头。“这对我来说很有道理。你们可以开始创建几个动物试试。”
鲁本继续打字:
➊ >> **monkey = Animal.new('George', 2)**
=> #<Animal:0x00000104953940 @name="George", @legs=2>
➋ >> **monkey.name = 'Kong'**
=> "Kong"
➌ >> **dog = Animal.new('Bigelow')**
=> #<Animal:0x00000104950178 @name="Bigelow", ➍@legs=4>
“太棒了!在 ➊,我们创建了 monkey,一个 Animal 类的实例,并将它的名字设为 'George',腿数为 2。接下来,在 ➋,我们将 monkey 的名字改为 'Kong',展示了我们的 attr_accessor 让我们不仅能读取还能更改名字。
“最后,在 ➌,我们创建了 Animal 类的第二个实例 dog,名字为 'Bigelow'。由于我们没有为 dog 指定腿数,它默认会有四条腿,就像在 ➍ 所显示的返回值那样。”
一些类
女王思考了一下。“是的,这样挺好。那么,”她接着说,“想象一下,如果我们不仅仅把 monkey 和 dog 当作 Animal 类的实例,而是想要将 Monkey 和 Dog 作为单独的类来处理。我们该怎么做呢?”
“嗯,我们可以这样做,”Ruben 说道,他开始输入:
>> **class Monkey**
>> **attr_accessor :name**
>>
>> **def initialize(name)**
>> **@name = name**
>> **@legs = 2**
>> **end**
>> **end**
=> nil
“正是如此,”女王说道。“这定义了一个 Monkey 类,猴子类创建的猴子会有 @name 和两个 @leg。attr_accessor 还会自动为每只猴子创建一个 name 方法,让我们可以获取它的名字。为了从类中创建一只新的猴子,我们使用 Monkey.new 并传入它的名字作为字符串值。像这样!”她在计算机装置上输入:
>> **monkey = Monkey.new('George')**
=> #<Monkey:0x00000104bdf3a8 @name="George", @legs=2>
>> **monkey.name**
=> "George"
“我们也可以对 Dog 类做同样的事情,”Ruben 继续说道。“我们知道几乎每只狗都有四条腿,所以它的结构会和 Monkey 类一样,只是类名不同,@leg 的数量会是 4。”他说着在计算机装置上输入:
>> **class Dog**
>> **attr_accessor :name**
>>
>> **def initialize(name)**
>> **@name = name**
>> **@legs = 4**
>> **end**
>> **end**
=> nil
“就像我们可以通过 Monkey.new 创建新的猴子,并传入一个字符串作为猴子的名字一样,我们也可以通过 Dog.new 创建新的狗狗,并传入一个字符串作为狗的名字!”Ruben 说道。
>> **dog = Dog.new('Bigelow')**
=> #<Dog:0x00000104be3d18 @name="Bigelow", @legs=4>
>> **dog.name**
=> "Bigelow"
“这肯定是创建一对类的方式之一,”女王说道,“但看起来你为 Monkey 和 Dog 类写了很多重复的代码。”
“没错,”Ruben 说道。“这样可以吗?”
继承与 DRY 代码
“嗯,”女王说道,“每当你发现自己写了重复的代码时,你应该问问自己是否非得这样做。好的代码——不像我丈夫这样,”她忍住笑意说道,国王正在从他华丽的王袍袖子里倒出水,“——应该是 DRY 的。”
“我知道那个!”国王说道,一边将水滴从他的弦上抖掉并放回口袋。“它代表 不要 重复 自己。”
“他知道这个,因为他总是重复自己,”女王低声对 Scarlet 和 Ruben 说道。“但是,是的,”她接着说,“如果你避免在代码中重复自己,就能节省大量时间!而且,如果你需要修改某个部分,你只需要修改 一个 地方,而不是多个地方。”
“我喜欢这个!”Ruben 说道,“但我们怎么才能让我们的类代码更 DRY 呢?”
“通过 继承,”女王说道。
“继承!”Scarlet 说道。“我好像以前听说过这个,但不太清楚它是什么意思。”
“我来给你们展示,”女王一边解释一边在计算装置上敲打着。“我们已经创建了一个名为Animal的类。假设我们可以使用这个类作为创建Monkey 和Dog类的方式呢?”
>> **class Dog < Animal**
>> **def bark**
>> **puts 'Arf!'**
>> **end**
>> **end**
=> nil
“class Dog < Animal 这一部分的意思是Dog类继承自Animal类。它对 Ruby 说:‘创建一个新的名为Dog的类,它知道如何做Animal类做的一切,’”女王说道。“然后我们只需要像平常一样添加一个方法。在这里,我为Dog类添加了一个bark方法,因为狗会叫。”她卷起了袖子。
“这里有一个惊人的地方:因为Dog继承自Animal,新的狗类可以做任何动物能做的事,并且还能够做狗能做的事。它们会有一个name方法和默认的四条腿,并且知道如何bark!”
>> **dog = Dog.new('Bigelow')**
=> #<Dog:0x00000104c89218 @name="Bigelow", @legs=4>
>> **dog.name**
=> "Bigelow"
>> **dog.bark**
Arf!
=> nil
“令人震惊!”国王说道。
“不是吗?”女王说道。“这也意味着,我们可以通过再次从Animal类继承,而不必重新输入所有关于Monkey类的定义。因为我们从Animal继承,我们得到了name方法和默认的@legs值为4,而且我们还会得到我专为猴子添加的这个新的make_sounds方法。”
>> **class Monkey < Animal**
>> **def make_sounds**
>> **puts 'Eeh ooh ooh!'**
>> **end**
>> **end**
=> nil
“现在我们可以创建一个有名字和两条腿的新猴子了。不仅可以通过我们从Animal继承来的name=方法改变它的名字,还可以make_sounds!”
“我们可以获取名字并更改它吗?”Ruben 问道。
女王点了点头。“记住,我们是从Animal继承的,而Animal有attr_accessor :name。这会自动创建一个name方法,用于获取名字,还有一个name=方法,用于设置名字。明白了吗?”
>> **monkey = Monkey.new('George', 2)**
=> #<Monkey:0x00000102deaed8 @name="George", @legs=2>
>> **monkey.name = 'Oxnard'**
=> "Oxnard"
>> **monkey.make_sounds**
Eeh ooh ooh!
=> nil
“哇!”Scarlet 说道,“这太神奇了——monkey和dog都有各自的方法,但它们也可以做任何Animal能做的事!”

“这就是继承如此美妙的原因!”女王说道。“考虑到我们之前的Animal类,它有attr_accessor用于:name,并且有一个initialize方法来设置@name和@legs实例变量,我们可以创建两个新的类,这些类继承了这些信息,并添加一些新内容——比如为Dog实例添加一个bark方法,为Monkey实例添加一个make_sounds方法。”
“Ruby 中的继承就像现实生活中的继承一样,”女王继续说道。“就像你可能继承了你父亲的眼睛颜色或你母亲的数学才能,Ruby 中的对象也可以继承其他对象的信息和方法。”
“哦!”Ruben 说道。“所以我们不仅可以使用类来创建许多相似的对象,避免编写额外的代码,我们还可以编写从其他类借用代码的类?”
“说得对,”女王说道。“如果两个类之间有我喜欢称之为‘is-a’关系,比如‘猴子是一种动物’或者‘狗是一种动物’,我们可能会在代码中使用继承。”
“但是Dog类永远不会继承自Monkey类,”Scarlet 说,“因为狗不是猴子的一种。”
“没错,”女王答道。
“你能再展示一下语法吗?”Scarlet 说。“这是个好技巧,我想记住它。”
子类和父类
“当然可以,”女王回答。“当你有一个类继承自另一个类时,你使用class关键字,就像往常一样。然后你写下要继承的类的名称,我们称之为子类或子类,然后是<。你可以把那个小小的<看作是箭头的尖端,意思是‘把右边类的所有能力和特性带入左边的类!’最后,你在<的右边写下你要继承的类的名称,这就是我们所说的父类或父类。最后,你可以像平常一样定义任何新的方法。它看起来像这样,”她说着并敲击键盘:
super_and_subclass.rb
➊ class MySuperclass
def say_hello
puts 'Hello!'
end
end
➋ class MySubclass < MySuperclass
def say_goodbye
puts 'Goodbye!'
end
end
“这里我们有两个类,MySuperclass和MySubclass,”女王解释道。“MySubclass在第➋行继承自MySuperclass,因此MySubclass的实例不仅拥有在MySubclass类中定义的say_goodbye方法,而且还可以使用它从MySuperclass继承的say_hello方法!让我们看看当我们创建一个新的MySubclass实例时会发生什么。”
>> **load 'super_and_subclass.rb'**
=> true
>> **subby = MySubclass.new**
=> #<MySubclass:0x00000104a4c478>
>> **subby.say_hello**
Hello!
=> nil
>> **subby.say_goodbye**
Goodbye!
=> nil
“我通过MySubclass.new创建了一个名为subby的MySubclass实例,”女王说道。“正如我承诺的那样,subby可以使用在MySubclass中定义的say_goodbye方法,也可以使用在MySuperclass中定义的say_hello方法,因为MySubclass继承自MySuperclass,因此它可以做任何MySuperclass能做的事。”
“谢谢!”Scarlet 说,“我现在明白了。”她看着屏幕几秒钟。“一个类可以继承多个其他类吗?”她问道。
“唉,不行,”女王说。“你只能在<的左边放一个类名,在右边放一个类名。然而!”她继续说道,“确实有一个 Ruby 技巧,可以让你将多个类的行为混合到一个类中,我们稍后会讲到。”
“好的,”Ruben 说,“但是如果你想让你的子类有一个与父类不同版本的方法呢?”
覆盖方法:海盗也是人
“现在这个,我们可以做到了,”女王说。“任何子类都可以在任何时候覆盖它从父类继承的方法。让我们来看看。我们会创建一个名为Person的父类和一个名为Pirate的子类,每个类都有一个speak方法。当然,海盗和普通人的说话方式可不太一样,对吧?”Scarlet 和 Ruben 点点头。“所以,”女王继续说道,“这两个speak方法会不同。”她在计算设备上敲入了代码:
pirates_and_people.rb
➊ class Person
attr_reader :name
def initialize(name)
@name = name
end
def speak
puts 'Hello!'
end
end
➋ class Pirate < Person
def speak
puts 'Arr!'
end
end
“从 ➊ 开始,我定义了Person类并添加了attr_reader :name,这样我们就能够获取和修改任何Person实例的名字,”女王说道。“initialize方法将name设置为我们在调用Person.new时传入的字符串,而speak方法只会打印'Hello!'。”
“到目前为止我跟上了!”国王说道。
“接下来,在 ➋ 处,我定义了Pirate类继承自Person类,因此Pirate实例将能够执行Person实例能够执行的任何操作,”女王说道。“但是!我为Pirate类定义了自己的speak方法,打印出'Arr!'。我们稍后会看看它是如何工作的。首先,让我们创建每个类的一个实例,确保我们能够顺利创建它并获取它的名字。”
>> **load 'pirates_and_people.rb'**
=> true
>> **esmeralda = Person.new('Esmeralda')**
=> #<Person:0x00000104bfaa90 @name="Esmeralda">
>> **rubybeard = Pirate.new('RubyBeard')**
=> #<Pirate:0x00000104bfedc0 @name="RubyBeard">
>> **esmeralda.name**
=> "Esmeralda"
>> **rubybeard.name**
=> "RubyBeard"
“现在,让我们测试一下我们的speak方法,”女王说道。“因为Pirate创建了自己的speak方法,所以Pirate的实例将使用那个方法,而不是继承自Person的那个方法,”她解释道。“但是由于我们没有修改Pirate通过attr_reader :name继承来的name和name=方法,我们仍然可以像处理普通人一样处理海盗的名字——获取和修改名字。”
>> **esmeralda.speak**
Hello!
=> nil
>> **rubybeard.speak**
Arr!
=> nil
“那真是太酷了,”鲁本说道,“但是我们什么时候决定重写一个方法呢?”
“每当一个类继承自另一个类,而且你想要大部分相同的行为,但不是完全一样的时候,”女王说道。“在这个例子中,我们希望Pirate是Person,并且像Person一样创建它,但我们希望确保我们的Pirate听起来像海盗。所以我们只需要重写我们希望在普通人和海盗之间有所不同的方法!”
“那说得通,”Scarlet 说道,“但是如果我们想要两者的结合呢?也就是说,如果我们想要修改一个继承来的方法,但又不完全替换它呢?”
使用 super
“我很高兴你问了这个问题,”女王说道。“这正是 Ruby 允许我们做的——我们只需要super关键字。使用我们之前创建的Animal类,我们将创建一个已经存在的方法的新版本,就像我们之前对speak方法所做的那样,并添加我们的新代码。然后,我们会使用super来告诉 Ruby:‘好吧,我已经完成了对这个方法的修改!现在让它执行父类版本方法中所有的操作。’它是这样工作的,”她说着,输入了:
super_dog.rb
class Dog < Animal
def initialize(name)
puts 'Just made a new dog!'
super
end
end
“现在我们可以像之前一样创建一个继承自Animal的Dog类,”女王说道。
>> **load 'super_dog.rb’**
=> true
>> **dog = Dog.new('Bigelow')**
Just made a new dog!
=> #<Dog:0x00000104c6f020 @name="Bigelow", @legs=4>
“不过在这里,我们为Dog类定义了自己的initialize方法,Dog的实例将使用这个方法,而不是继承自Animal的那个方法,”女王继续说道。
“就像海盗使用自己的speak方法,而不是Person类的那个方法一样,”鲁本说道。
“正是!”王后说。“我们在Dog initialize方法中添加了自己的puts语句来打印出一条信息,但之后我们使用了super来告诉 Ruby:‘好的!现在,使用Animal的initialize方法。’super所做的就是调用父类方法的版本!由于Animal的initialize方法为我们设置了@name和@legs实例变量,因此你会看到不仅有@name="Bigelow",还有@legs=4!”
“哎呀,太惊人了,”国王说,他终于把自己弄干了。“Ruby 到底有什么是做不到的?”
“这不算什么,”王后说。“真正有趣的部分来了。我们将通过继承、方法重写和super来创造一些可靠的伙伴,帮助我们防御入侵我们王国的敌人!”
用守卫狗和飞猴保护王国
“但在我们继续之前,”王后说,“我们还是回到我们的Dog和Monkey类吧。首先,我会重新定义一个Dog类,因为已经有一段时间没看它了。”她在她的计算机装置上打字:
guard_dog.rb
class Dog < Animal
attr_accessor :name
def initialize(name)
@name = name
end
def bark
puts 'Arf!'
end
end
“我们的Dog类继承自Animal,并将以name进行初始化,它还会有一个bark方法,随时可以让它叫个不停,”王后说。“接下来,我将创建一个全新的类,继承自Dog。让我们继续修改guard_dog.rb吧!”她在计算机装置上打字:
guard_dog.rb
class GuardDog < Dog
➊ attr_accessor :strength
➋ def initialize(name, strength)
@strength = strength
super(name)
end
➌ def bark
puts 'Stop, in the name of the law!'
end
➍ def attack
puts "Did #{rand(strength)} damage!"
end
end
“在这里,我创建了一个继承自Dog的GuardDog类。在 ➊,我为:strength添加了一个attr_accessor,这样我们就可以设置和获取新守卫狗的力量。接下来,在 ➋,我添加了一个initialize方法,它部分重写了Dog中的initialize方法:它设置了GuardDog的@strength,然后仅传递name调用super,以使用Dog的initialize方法,进而设置@name。在 ➌,我完全重写了Dog中的bark方法,并为GuardDog设置了一个独有的叫声。
最后在 ➍,我添加了一个全新的attack方法,用于打印出一条字符串,说明狗造成了多少伤害。该方法使用了 Ruby 内置的rand方法来选择一个介于零和GuardDog的力量之间的随机数。
“哇!”鲁本说。“太棒了!我没想到你还能用参数来调用super。”
“哦,是的,”王后说。“如果单独调用super,它会用子类initialize方法获得的所有参数来调用父类的initialize方法。GuardDog比Dog多接受一个参数——它还需要strength,而Dog仅用name来创建。如果我们尝试同时给Dog传递这两个参数,就会导致错误。所以我们只用name来调用super,确保Dog的initialize方法能接收到它期望的参数数量。”
每一只守卫狗都有它的日子
“那么,”王后继续说,“让我们创建一个新的GuardDog并进行测试吧!”
>> **load('guard_dog.rb')**
=> true
>> **rex = GuardDog.new('Rex', 7)**
=> #<GuardDog:0x0000010334e168 @strength=7, @name="Rex">
>> **rex.strength**
=> 7
>> **rex.bark**
Stop, in the name of the law!
=> nil
>> **rex.attack**
Did 1 damage!
=> nil
>> **rex.attack**
Did 4 damage!
=> nil
“现在我们有了一种特殊的狗——GuardDog——它有自己的一套方法!”皇后说。“我们部分重写了它的initialize方法,因为我们希望它有strength,但之后我们使用super像创建普通狗那样完成了它的创建。我们重写了bark方法,因为我们希望GuardDog有bark方法,然后通过添加一个完全新的attack方法结束,GuardDog有这个方法,而普通的Dog没有。”
“我现在开始明白了,”鲁本说。“我们通过继承来减少需要重新输入的代码量,当我们想做出例外并赋予子类特殊行为时,我们重写方法!”皇后点点头。
“别忘了super,”斯嘉丽说。“当我们想要部分修改子类中方法的行为,但不完全替代它时,我们会用这个。”
国王皱起了眉头。“这很有道理,但我们能再看一些吗?”他问。“一下子记住这些有点多。”
再来一次,带点情感!
鲁本点点头。“能再来一个例子,确保我们理解吗?”他问。
“当然,”皇后说。“这是另一个继承、方法重写和super的例子,这次我们使用我们可靠的Monkey类。让我们让Monkey看起来像这样,”她说着,打了几行代码:
flying_monkey.rb
class Monkey < Animal
attr_reader :name, :arms
def initialize(name, arms = 2)
@name = name
@arms = arms
end
def make_sounds
puts 'Eeh ooh ooh!'
end
end
“在这里,我们有一个Monkey类。通过使用attr_reader,我们可以获取(但不能修改)猴子的name和arms数量,默认为2。我们还有一个make_sounds方法,它会打印出一个字符串。”
“看起来很标准,”国王说。
“接下来,”皇后继续说,“我们将创建一个继承自Monkey的FlyingMonkey类。我们将继续在flying_monkey.rb中添加内容!”她在她的计算机装置中输入了代码:
flying_monkey.rb
➊ class FlyingMonkey < Monkey
➋ attr_reader :wings
➌ def initialize(name, wings, arms = 2)
@wings = wings
super(name, arms)
end
➍ def throw_coconuts
coconuts = rand(arms)
damage = coconuts * wings
puts "Threw #{coconuts} coconuts! It did #{damage} damage."
end
end
“对于我们的FlyingMonkey类,”皇后说,“首先我们继承自Monkey ➊。接下来,我们为:wings添加一个attr_reader,这样我们就知道我们的FlyingMonkey有多少翅膀 ➋。我们用一个特定数量的@wings来初始化飞行猴子,但接着调用super让Monkey类来设置@name和@arms的数量 ➌。然后我们定义一个全新的throw_coconuts方法 ➍,这个方法使用 Ruby 内置的rand方法来计算飞行猴子投掷椰子的伤害值。椰子的数量是一个从零到飞行猴子手臂数量之间的随机数,伤害值则是这个数字乘以猴子翅膀的数量,因为有更多翅膀的猴子可以飞得更高。”
“好的!”皇后说。“让我们创建一个飞行猴子并测试它的方法。”
>> **load 'flying_monkey.rb'**
=> true
>> **oswald = FlyingMonkey.new('Oswald', 6, 4)**
=> #<FlyingMonkey:0x000001013d1718 @wings=6, @name="Oswald", @arms=4>
>> **oswald.make_sounds**
Eeh ooh ooh!
=> nil
>> **oswald.throw_coconuts**
Threw 3 coconuts! It did 18 damage.
=> nil
“太棒了!”斯嘉丽说。“我们通过使用FlyingMonkey自己的initialize方法来添加翅膀,然后让Monkey类来完成设置名字和手臂数量的工作。而且因为FlyingMonkey继承了Monkey,所以飞行猴子不仅可以throw_coconuts,还可以使用Monkey的make_sounds方法!”
“好极了!”国王说。“我敢打赌那只猴子在扔椰子方面也非常出色。想来,飞行猴子自然是应该具备这种本事。”
“我敢打赌它能发出非常逼真的猴子叫声,”鲁本补充道。
“我想这有道理,”斯嘉丽说,“但有件事困扰着我:为什么我们的 GuardDog 会知道怎么说话?”
“他是一只非常聪明的狗,”皇后说。
“非常聪明,”国王说。
“说到这个,”皇后说,“我觉得该是时候让我们的守卫狗和飞行猴子开始工作了!”她按下椅子扶手上的一个按钮,她的计算设备开始嗡嗡作响。几秒钟内,办公室四周的门打开了,几十只守卫狗和飞行猴子涌了出来!

“塔可星期二!”国王说。“我原本以为所有这些小工具和黑客会议都是浪费时间和金钱。”
“恰恰相反,”皇后说,“我觉得它们可能会拯救这个王国!”
国王张开嘴准备说话,但就在此时,皇后桌上的一部鲜红的电话开始疯狂地响了起来。她接起电话。“喂?”她说。她等了一会儿,眼睛睁得大大的。“保持原地不动!我们马上就到!”她挂断电话,跳下椅子。“卫兵有消息!”她说。“他们在皇家马厩。快点,我们走!”

说完,他们四人迅速从皇后的办公室冲出去,朝着后面的马厩跑去,守卫狗们迅速前冲,飞行猴子紧随其后。
皇后的机器
这真是越来越激动人心了!在国王、皇后、鲁本和斯嘉丽去捉拿坏人时,让我们跳进去帮助皇后创建一个 Ruby 类,来帮助保持她所有的皇家事务机密;毕竟,GuardDog 和 FlyingMonkey 也只能做这么多!我想为她的计算设备创建一个更加安全的登录账户,可能正是我们所需要的;我们可不希望有人再次轻易闯入。所以,我们将设置一个 Account 类,里面有一个 password,让皇后用来登录她的计算机。
让我们从创建一个名为 secrecy.rb 的新文件开始,并键入以下代码。
secrecy.rb
➊ class Account
attr_accessor :username, :password
➋ def initialize(username, password)
@username = username
@password = password
end
end
➌ class SuperSecretAccount < Account
➍ def initialize(username, password)
@reset_attempts = 0
super(username, password)
end
➎ def password=(new_password)
while @reset_attempts < 3
print 'Current password?: '
current_password = gets.chomp
if @password == current_password
@password = new_password
puts "Password changed to: #{new_password}"
break
else
@reset_attempts += 1
puts "That's not the right password."
puts "Attempt #{@reset_attempts} of 3 used up!"
end
end
end
➏ def password
'The password is secret!'
end
end
➐ regular = Account.new('Your name', 'your password')
super_safe = SuperSecretAccount.new('Your name', 'your password')
➑ regular = Account.new('Your name', 'your password')
super_safe = SuperSecretAccount.new('Your name', 'your password')
puts "Your regular account password is: #{regular.password}"
regular.password = 'Something else!'
puts "Your regular account password is now: #{regular.password}"
puts "If we try to see the secret account password, we get: #{super_
safe.password}"
changed_password = 'Something else!'
puts "Trying to change your secret account password to: #{changed_
password}..."
super_safe.password = changed_password
这段代码比较长,我们一步一步地来看。
首先,我们在 ➊ 创建一个基本的 Account 类,设置一些实例变量(在 ➋ 的 initialize 方法中查看它们)。Account 类的实例可以通过 attr_accessor 让 Ruby 代码随意读取和修改 @username 和 @password 的值,提供了对这两个属性的访问权限。
我们已经有了一个相当不错的开始!这段代码让我们能够为某人创建账户,并允许该人设置密码,就像你为一个网站或电子邮件设置密码一样。然而,问题是,这段代码允许任何 Ruby 代码修改用户密码,这显然是我们不希望看到的。
为了解决这个问题,我们在 ➌ 创建了 SuperSecretAccount 类,它继承自 Account,接下来是事情变得有趣的地方。首先,SuperSecretAccount 的 initialize 方法同样接受一个用户名和密码,并将这些传递给 super,让 Account 负责设置这些实例变量 ➍。SuperSecretAccount 还创建了一个新的实例变量 @reset_attempts,用来跟踪用户尝试登录的次数。
接下来,SuperSecretAccount 类重写了 password= 方法 ➎(由 Account 的 attr_accessor :password 创建的两个方法之一),因此它要求用户输入旧密码才能更改密码。如果她输入了正确的密码,程序会更新密码并立即 break 跳出 while 循环;如果她三次尝试失败,程序将在不更改密码的情况下退出。
之后,SuperSecretAccount 类重写了 password 方法 ➏(由 Account 的 attr_accessor :password 创建的另一个方法),并让它打印出字符串 The password is secret!,而不是像通常那样直接显示密码。最后,我们创建了几个账户 ➐,并尝试获取和设置密码 ➑。
你可以通过在命令行中输入 ruby secrecy.rb 来运行文件中的代码。确保你在与 secrecy.rb 文件相同的文件夹中,然后输入:
$ **ruby secrecy.rb**
这是我得到的输出(你的可能会稍有不同,取决于你在运行脚本时输入的内容):
➊ Your regular account password is: your password
Your regular account password is now: Something else!
➋ If we try to see the secret account password, we get: The password
is secret!
➌ Trying to change your secret account password to Something else!...
Current password?: **lasers**
That's not the right password.
Attempt 1 of 3 used up!
Current password?: **ninjas**
That's not the right password.
Attempt 2 of 3 used up!
Current password?: **your password**
Password changed to: Something else!
首先,我们看到程序打印出我们常规账户的密码,然后是我们更改密码后的新密码 ➊。那太简单了!
接下来,在 ➋,我们看到我们的秘密账户正确地隐藏了密码,若我们尝试查看密码,它只会打印出 The password is secret!。
最后,我们尝试在 ➌ 更改我们的秘密账户密码。我们先输入两个错误的密码(lasers 和 ninja),然后输入正确的密码 your password,我们的 Ruby 程序打印出我们成功地将密码更新为 Something else!。
随意玩玩这段代码。当你尝试获取和设置 secrecy.rb 中 regular 账户的密码时会发生什么?当你尝试更改 super_safe 账户时又会怎么样?
如果我们尝试在 super_safe 账户上设置密码,并且传入正确的当前密码,会发生什么?如果是错误的呢?尝试多次输入错误密码会怎么样?
一旦你完成了对代码的探索,你可以尝试思考我们可以做些什么来让它变得更好。例如,我们可以为Account或SuperSecretAccount添加哪些方法,使它们更有用?(也许添加一个reset_password方法,以防你完全忘记了密码?)SuperSecretAccount可能会从Account重写哪些方法?有没有哪些方法可能使用Account的一部分功能,但不是全部?我们该如何做到这一点?(提示:super将会参与其中。)
最后,Ruby 确实有一些内置方法可以帮助你提高代码的安全性(或至少控制哪些方法可以被调用)。如果你愿意,可以在 Ruby 文档中详细阅读:ruby-doc.org/core-2.0.0/Module.html#method-i-private。
你已经知道这些了!
你在这一章学了一些复杂的内容,但我相信你已经掌握得很好了。为了确保这一点,我们再过一遍所有内容。
首先,我们回顾了如何使用class关键字创建 Ruby 类。
class Greeting
def say_hello
puts 'Hello!'
end
end
接下来,你发现 Ruby 类可以通过继承相互共享信息和方法:就像一个人可以继承父母的特征一样,一个 Ruby 类可以从另一个类继承信息和行为。进行继承的类称为子类,而被继承的类称为父类。
继承的语法如下:
class Dog < Animal
def bark
puts 'Arf!'
end
end
在这个例子中,由于Dog继承自Animal,所以Dog类的实例(通过Dog.new创建)可以使用Animal类中定义的任何方法。
我们还学习了方法重写和super关键字。方法重写就是在子类中编写一个与父类方法同名的方法;当我们创建子类的实例时,它会使用子类版本的方法,而不是父类的版本。每当你想要在子类中实现不同或更具体的行为时,你就需要重写父类的方法。例如,假设你正在编写一个游戏,其中巫师是英雄(class Wizard < Hero),巫师在他们的attack方法中使用魔法,而不是游戏默认的剑。
你可以像这样重写一个方法:
class Hero
def initialize(name)
@name = name
end
def attack
puts "Swung sword for #{rand(5)} damage!"
end
end
class Wizard < Hero
def attack
puts "Cast spell for #{rand(20)} damage!"
end
end
我们可以从以下例子中看到这一点:这个英雄很擅长剑术,但巫师知道如何施放魔法!
>> **aragorn = Hero.new('Aragorn')**
=> #<Hero:0x0000010334e398 @name="Aragorn">
>> **aragorn.attack**
Swung sword for 4 damage!
=> nil
>> **gandalf = Wizard.new('Gandalf')**
=> #<Wizard:0x000001033627f8 @name="Gandalf">
>> **gandalf.attack**
Cast spell for 17 damage!
=> nil
如果我们只想修改方法的部分内容,可以使用super;我们添加所需的额外功能,然后调用super来调用父类的版本,如下所示:
class Wizard < Hero
def attack
super # This calls Hero's attack method
puts 'But I also know magic! You shall not pass!'
end
end
attack方法做了两件事。首先,它通过super调用父类的attack版本(也就是Hero类中定义的attack方法,它只打印Swung sword的消息)。然后,它打印额外的信息(But I also know magic! You shall not pass!)。当你想修改父类方法的行为,但又不完全替换它时,就可以这么做。
>> **gandalf = Wizard.new('Gandalf')**
=> #<Wizard:0x000001032d4278 @name="Gandalf">
>> **gandalf.attack**
Swung sword for 2 damage!
But I also know magic! You shall not pass!
=> nil
最后但同样重要的是,你看到可以通过带有参数的super来将正确的参数传递给父类的方法:
class Dog
attr_accessor :name
def initialize(name)
@name = name
end
end
class GuardDog < Dog
attr_reader :strength
def initialize(name, strength)
@strength = strength
super(name)
end
end
现在,当我们创建一个GuardDog时,它会添加自己的@strength,并让Dog来负责添加@name:
>> **mook = GuardDog.new('Mook', 2)**
=> #<GuardDog:0x00000102fcfca8 @strength=2, @name="Mook">
>> **mook.name**
=> "Mook"
>> **mook.strength**
=> 2
好了!到此为止,你已经是一个类的高手了。嗯,差不多——正如你有方法更新和更改你的方法一样,也有方法更新和更改你的类;你甚至可以将来自不同类的行为混合到你创建的 Ruby 类中!Ruby 类的最后一块拼图就是模块,如果我们加快速度,就能准时赶到皇家马厩,了解它们的所有内容。
第十章:一种不同颜色的马
完全的熊猫乱象
国王、女王、鲁本和斯卡利特一层又一层地绕过楼梯,向皇家马厩走去。就在鲁本和斯卡利特以为这曲折的楼梯永远没有尽头时,女王来到了一扇沉重的橡木大门前,推开了它们。他们全都眯着眼睛跑进了宫殿后方田野的阳光里,离得不远处就是皇家马厩的入口。
“这边!”女王说道。“快点!”
他们跑到马厩的前门,那里有两名女王的卫兵在等候。每个卫兵都抓住了一位非常眼熟的皇家水管工高级学徒的胳膊。

“哈尔多!”鲁本倒吸一口气。
“好了好了,”国王说道,“我相信这一切一定有合理的解释。”尽管他说着这些话,国王看上去依然很担心。他转向哈尔多。“哈尔多,这一切的解释到底是什么,天晓得你到底做了什么?”
在哈尔多来得及回应之前,女王走近卫兵。“哈尔多不是我在我的皇家办公室里看到的那个人,”她说道。“那时有四个人,他们个头要矮得多。请放开他。”
卫兵们点点头,放开了哈尔多的胳膊。
“谢谢,殿下,”哈尔多说道,拍了拍自己身上的灰尘。
“你为什么在这里的马厩?”国王问道。
“这就是我们刚刚问他的,”那位鼻子弯曲的卫兵说道。
“我在试着解释,”哈尔多说道。“你看,经过在安布罗斯洞窟的搜寻却一无所获后,我回到了我的工作,作为皇家水管工的高级学徒。不过,我从斯卡利特和鲁本那里学到了很多 Ruby 的知识,因此我可以在几个小时内完成一天的工作。于是,我有些空闲时间,就兼任了皇家马夫的兼职学徒。”
“太棒了,”国王显然松了口气,看到哈尔多不是他们一直追的坏人。“真是一个全能的人!”
“我不确定我会走那么远,”哈尔多微微脸红地说道。
“陛下们,”那位没有弯曲鼻子的卫兵说道,“我们从女王的办公室追赶一群戴兜帽的人,但一旦他们到达这里的马厩,我们就失去了他们的踪迹。当我们看到发生的事情时,只有哈尔多一个人在附近。我们以为他可能卷入其中,于是我们召唤了女王。”他耸耸肩,“结果是哈尔多出来看看发生了什么事,试图帮忙。”
“等一下,”女王说道。“你在马厩看到什么事情了?”
卫兵们交换了一个不安的眼神。“你最好来看看,”那位鼻子弯曲的卫兵说道。
一行人匆忙走进马厩。两名卫兵指向第一个马厩,国王、女王、鲁本和斯卡利特探头望了进去。
“这是我见过的最奇怪的马,”国王说道。

“那……是一只熊猫,”鲁本说道。
“而且它是红色的!”女王呻吟道,“天啊,这里发生了什么事?”
“不应该是红色吗?”斯卡雷特问。
“一点也不!”女王说,“所有的皇家熊猫应该是紫色的!”她跑到下一个摊位,然后是下一个,再下一个。“这个是蓝色的!”她喊道,“这个是黄色的!一个熊猫都不是紫色的!”她摊开双手,“谁听说过紫色熊猫的狂欢游行,结果熊猫都是除了紫色的其他颜色?”
“等一下,熊猫以前是紫色的,但现在不是了吗?”鲁本说,“它们天生就是紫色的吗?”
“那么,紫色熊猫狂欢游行究竟是什么?”斯卡雷特问。
“一个一个来,”女王说。她转向鲁本,“不,熊猫不是天生紫色的。它们一出生是白色的,但我们给它们喂食特殊的营养丰富的食物,才让它们变成紫色的。至于游行,”她对斯卡雷特说,“我们每个月举行一次,庆祝王国的和平与繁荣。我们认为如果我们的生活中有一点疯狂,至少我们应该能掌控它。”她叹了口气,“当然,考虑到今天的混乱,我们是没法举行游行了。”
“别急,”斯卡雷特说,“我敢打赌我们能修好它!听起来像是有人动了熊猫食物的手脚。它在哪里?”
“在这边,”哈尔多说,“食物是由熊猫供应器 3000 号准备的。”
他们走过一排排摊位,走到马厩的另一边,那儿有一台巨大的圆形机器,上面布满了刻度盘和开关,正在嗡嗡作响。中间的一个熟悉的屏幕在发光。

“计算装置!”鲁本说,“熊猫供应器 3000 号是用 Ruby 编写的吗?”
“绝对是的,”哈尔多说,“自从你们帮我修好了神秘管道之后,我就尽可能多地学习 Ruby。我敢说我已经学得相当好了,”他说着,一边把大拇指挂在工装裤的带子上。“我甚至已经掌握了这个熊猫供应器。”
“你能告诉我们有人动过熊猫的食物吗?”鲁本问道。
“那你能修好吗?”女王焦急地问道。
创建模块
“我想是的,”哈尔多说,“我们看看。”他打开了一个名为colorize.rb的文件,这时大家看到的内容是:
module Colorize
def color
[:red, :blue, :green, :yellow].sample
end
end
“啊哈!”哈尔多说,“我看出了问题所在。有人把color方法改成了返回一个随机颜色的符号——红色、蓝色、绿色或黄色。这就是sample方法的作用,”他解释道,“它从一个数组中挑选一个随机项。”
“这就是为什么熊猫都是不同的颜色,除了紫色!”鲁本说,“不过等一下——这个文件里根本没有关于熊猫食物的内容。那第一行是什么意思?”
“那个?那意味着这段代码是一个模块,”哈尔多一边挠着浓密的黑胡子一边说道,“你可以把 Ruby 模块看作是一个装满了方便的信息和方法的桶,随时可以用到。”
“它看起来像是一个类,”斯卡雷特说。
“这就像一个类!” Haldo 说。“像类一样,模块有它们自己的方法。事实上,模块其实就是:一组方法的集合!”
“那类和模块之间的区别是什么?” Ruben 问。
“模块实际上和类完全相同,只是我们不能使用new方法来创建新的模块,” Haldo 解释道。“首先,我们快速回顾一下类。”他启动了 IRB 并输入了:
>> **class FancyClass; end**
=> nil
“这只是创建了一个名为FancyClass的新空类,” Haldo 解释道。
“那个分号是做什么用的?” Scarlet 问。
“这只是告诉 Ruby 一行代码结束了,” Haldo 说。“通常在 IRB 中,你通过按 RETURN 或 ENTER 键开始新的一行来实现,但由于我们的类和模块定义是空的,所以我们可以使用分号告诉 Ruby 一行代码已经结束,并且我们将开始新的一行。”他耸耸肩。“有些人不喜欢使用分号。每个人有自己的喜好!现在,让我们创建一个FancyClass的实例。”
>> **FancyClass.new**
=> #<FancyClass:0x000001044d80c8>
“你们以前创建过类的实例,对吧?” Haldo 问。Scarlet 和 Ruben 点了点头。“很好!”他说。“现在,让我们创建一个模块,尝试创建它的实例。”
>> **module ImportantThings; end**
=> nil
>> **ImportantThings.new**
NoMethodError: undefined method `new' for ImportantThings:Module
“尝试创建模块的实例会导致错误,因为模块没有像类那样的new方法,” Haldo 说。
“所以,如果你不能创建模块的实例,” Ruben 说,“那你能用它做什么?”
“我来给你们展示!” Haldo 说,“让我们创建一个自己的模块。”他输入了:
>> **module Bucket**
>> **MAX_BITS_AND_TRINKETS = 100**
>> **def announcing_bits_and_trinkets**
>> **puts 'Step right up! Bits and trinkets available now!'**
>> **end**
>> **end**
=> nil
“MAX_BITS_AND_TRINKETS是什么,” Scarlet 问,“为什么它是全大写的?”
常量
“那是一个常量,” Haldo 说。“常量像变量,但一旦你设定了它们的值,就不能再改变。它们以大写字母开头——例如,类名和模块名就是常量——虽然你技术上可以在 Ruby 程序中重新赋值给常量,但 Ruby 会警告你这么做。看?”他输入了:
>> **RUBY = 'Wonderful!'**
=> "Wonderful!"
>> **RUBY = 'Stupendous!'**
(irb):2: warning: already initialized constant RUBY =>
"Stupendous!"
“当你创建一个不是类或模块的常量时——也就是说,只是一个不会改变的值的名字——你通常会把它写成全大写,” Haldo 说。
“常量只能在模块里使用吗?” Ruben 问。
“不行!” Haldo 说,“你可以在 Ruby 程序中的任何地方使用它们。我现在之所以提到它们,是因为类名和模块名在技术上是常量,因为它们以大写字母开头。”
“这挺酷的,” Scarlet 说,“但是如果常量和方法都被放在模块里,如何访问它们的全大写形式呢?”
“我很高兴你问了这个问题,” Haldo 说,笑着。“让我们来看一下!”他又输入了一些代码:
>> **class Announcer**
>> **include Bucket**
>> **end**
=> Announcer
“在这里,我创建了一个Announcer类,它包含了Bucket模块。我们的Bucket模块包含一个常量MAX_BITS_AND_TRINKETS,它被设置为100,还有一个方法announcing_bits_and_trinkets,它会在屏幕上打印一些文字。
“当我们在类中include一个模块时,模块中的常量和方法可以被该类的任何实例使用。因为我们在Announcer中包含了Bucket,所以Announcer现在可以使用Bucket中定义的任何常量和方法!让我们创建一个Announcer实例,看看当我们使用Bucket中定义的方法时会发生什么。”
>> **loud_lucy = Announcer.new**
=> #<Announcer:0x00000103f0c5b8>
>> **loud_lucy.announcing_bits_and_trinkets**
Step right up! Bits and trinkets available now!
=> nil
“哇!”鲁本说。“loud_lucy知道如何使用announcing_bits_and_trinkets方法,尽管它在Bucket模块中定义!”
拓展你的知识
“完全正确!”哈尔多说。“但include并不是唯一一种将模块中定义的常量和方法引入其他类的方法。看看这个。”他又敲了一些代码:
>> **class Announcer**
>> **extend Bucket**
>> **end**
=> Announcer
>> **Announcer.announcing_bits_and_trinkets**
Step right up! Bits and trinkets available now!
=> nil
“如果我们将模块Bucket通过extend引入类,那么这些常量和方法就可以被类本身使用,”哈尔多解释道。“在这种情况下,类Announcer——而不是它的实例loud_lucy——可以使用该方法。通常情况下,你会希望实例拥有该方法,而不是类,所以根据我的经验,你通常会更常使用include而不是extend。”
“记得我说过有一个 Ruby 技巧,让你可以将多个类的行为混合到一个类中吗?”女王问。“这就是方法!”
Mixin 与继承
“等一下,”鲁本说。“所以你可以有一个类继承另一个类并且包括模块来添加额外的方法?”
“自己看!”哈尔多回答道,然后他敲了几行代码:
>> **module Enchanted**
>> **def speak**
>> **puts 'Hello there!'**
>> **end**
>> **end**
=> nil
“首先,我创建了一个包含单一speak方法的Enchanted模块。”
>> **class Animal**
>> **def initialize(name)**
>> **@name = name**
>> **end**
>> **end**
=> nil
“接下来,我创建了一个Animal类,用来设置我们创建的Animal实例的名字。”
>> **class Dog < Animal**
>> **include Enchanted**
>> **def bark**
>> **puts 'Arf!'**
>> **end**
>> **end**
=> nil
“在下一步中,我创建了一个继承自Animal并包含Enchanted模块的Dog类。如果我们做对了,我们的Dog实例应该能同时使用Dog bark方法和Enchanted speak方法。现在就试试看!”
>> **bigelow = Dog.new('Bigelow')**
=> #<Dog:0x000001049df148 @name="Bigelow">
>> **bigelow.bark**
Arf!
=> nil
>> **bigelow.speak**
Hello there!
=> nil
“当我们以这种方式使用模块时,我们称之为mixin,”哈尔多说,“因为你将新的常量和方法混合到一个现有的类中。基本上,Dog现在拥有了Animal和Enchanted的功能,尽管它仅直接继承自Animal。我们可以include任意多个类!假设我们在某个地方定义了所有这些模块,我们可以将它们一一使用:”
class Dog
include Enchanted
include Magical
include AnythingWeLike
# ...and so on and so forth
end
“所以如果你有一个Dog类和模块Enchanted、Magical以及AnythingWeLike,”国王说,“如果你用Dog类创建了一只狗,那只狗就可以使用Enchanted、Magical或AnythingWeLike中定义的任何方法。”
“完全正确,”哈尔多说。“我们也可以通过extend为我们的类引入任意多个模块。”他继续敲打着键盘:
class Dog
extend Enchanted
extend Magical
extend AnythingWeLike
# ...and so on and so forth
end
“太棒了!”斯卡利特说。
“等一下,”鲁本说。“这意味着在计算装置的某个地方,有一个包含Colorize模块的熊猫食物文件?”
引入另一个文件
“完全正确,”哈尔多说。“它的名字正是 panda_food.rb。看一下!”他打开了文件让大家看。“这就是控制熊猫食物的代码。”
注
接下来的几个例子只是让你跟着看一看而已——按原样运行这些代码会导致错误!我们会在本章稍后自己运行这个例子。
require './colorize'
class PandaFood < Food
include Colorize
attr_reader :calories
CALORIES_PER_SERVING = 1000
def initialize
@calories = CALORIES_PER_SERVING
end
end
“它是这么工作的,”哈尔多说。“我们选择一只熊猫——霍戈斯是我最喜欢的——看看我们能不能弄明白它的食物出了什么问题。”他说着,打开了 IRB 并输入:
>> **hogarths_food = PandaFood.new**
=> #<PandaFood:0x00000104480850 @calories=1000>
>> **hogarths_food.calories**
=> 1000
“attr_accessor 让我们访问 @calories 实例变量,它的值是 1000,”哈尔多解释道。“现在我们来看一下颜色!”
>> **hogarths_food.color**
=> :yellow
“嗯,”哈尔多说。“这能对吗?我们再试试。”
>> **hogarths_food.color**
=> :blue
“看到了吧!”哈尔多说。“你看到了吗?这就是我们的麻烦所在。其他在 Panda Provisionator 3000 上运行的 Ruby 程序在给机器指令制作食物时,会检查熊猫食物的颜色,它们得到的是黄色和蓝色,而不是紫色!”
“然后熊猫们吃了食物并变了颜色!”鲁本说。“哇,那应该是很快发生的。”
哈尔多点了点头。“熊猫们刚刚吃了食物。它们从白色变成其他颜色其实需要一些时间,但一旦它们变成了某种颜色,吃不同颜色的食物就会让它们的颜色立即变化。”
“所以把它们换回来应该是小菜一碟!”斯卡雷特说。“我们只需要把颜色改回紫色。”她研究了一会儿屏幕。“嘿,哈尔多,”她说,“这个 require 是干什么的?”
“我很高兴你注意到了这一点,”哈尔多说。“require 方法将 Ruby 代码从你当前工作之外的文件中引入!所以在 IRB 中你只是随便玩玩的时候不需要它,但如果你写了一个 Ruby 文件,你可以使用 require 来引入来自其他文件的代码。你甚至不需要输入 .rb 文件扩展名;只要输入 require,然后文件名作为字符串,就能立即使用那里的代码。”
他创建了一个名为 test_colors.rb 的文件,并开始输入:
➊ require './colorize'
➋ class TestColors
➌ include Colorize
end
test = TestColors.new
➍ puts test.color
哈尔多关闭了文件。当他运行 ruby test_colors.rb 时,他们看到的是:
$ **ruby test_color.rb**
blue
$ **ruby test_color.rb**
yellow
“看到了吗?”哈尔多说。“我们可以创建一个名为 test_colors.rb 的文件,然后在其中 require 我们之前看到的 colorize.rb 文件 ➊。一旦这么做了,我们可以创建自己的 TestColors 类 ➋,从 colorize.rb 文件中 include Colorize 模块 ➌,然后使用 color 方法 ➍!”
“不错!”鲁本说。“但为什么我们需要在 colorize 前加上 ./?”
“这有点复杂,”哈尔多说,“不过简短的回答是,当你想要require一个 Ruby 文件时,你需要告诉 Ruby 去哪儿找它。./表示,‘就在这个文件夹里找!’如果我们需要从我们所在的文件夹之外的文件夹中引用东西,我们就用两个点告诉 Ruby 上一级文件夹。这可能会让人困惑,”哈尔多接着说,“所以我画了几张图来帮助自己记住。我想我还留着它们!”他在口袋里翻找了一会儿,然后拿出一张纸,展开,展示给国王、女王、斯卡利特和鲁本看。

“我明白了!”斯卡利特说,“一个点加一个斜杠表示‘在当前文件夹里找’,两个点加一个斜杠表示‘上一级文件夹找’,而每当我们需要进入嵌套文件夹时,我们就用斜杠分隔的文件夹名称。”
“完全正确,”哈尔多说。
“但是有没有这种情况,你不需要使用点或斜杠?”她问。
“那也有点复杂,”哈尔多说,“不过简短的答案是,当然可以。我可以找时间给你演示一下,但有一种方法可以使用互联网下载其他人写的 Ruby 文件集合,这些文件叫做gems,你可以在自己的代码中使用!”
“听起来真棒!”斯卡利特说。
“确实是!”哈尔多说,“等我们搞清楚这个谜题后,我很乐意给你展示。”
“我想我已经弄明白这一切了,”国王插话道,“但是自从你提到常量,我一直在想,包含一个模块到类中是获取它常量的唯一方法吗?”
查找常量
“一点也不!”哈尔多说,“看看这个。”他迅速在计算装置上敲打了几下:
>> **module APocketFullofMethods**
>> **NUMBER_OF_METHODS = 42**
>> **end**
=> nil
>> **NUMBER_OF_METHODS**
NameError: uninitialized constant NUMBER_OF_METHODS
>> **APocketFullofMethods::NUMBER_OF_METHODS**
=> 42
注解
这些示例如果你试试就能正常工作,所以赶紧试试吧!
“这里,我定义了一个名为APocketFullofMethods的模块,”哈尔多说,“在其中,我放了一个常量,NUMBER_OF_METHODS,它的值是 42。你看,如果我从模块外部尝试访问NUMBER_OF_METHODS,我会得到一个NameError,但是如果我输入APocketFullofMethods::NUMBER_OF_METHODS,我就能得到42!”
“太棒了!”国王说。
“但是那两个连在一起的冒号是干什么的?”斯卡利特问。
“啊,我以前见过这个,”女王说,“那是作用域解析操作符,对吧,哈尔多?”
“哦,是的,”哈尔多说,“不过我觉得这个名字有点令人困惑。实际上,你可以把它当作一种查找方法:那四个点看起来像是一双小眼睛。它是我们指定查找哪个模块来找到我们创建的东西的方式。”
“真酷!”鲁本说。
“不是吗?”哈尔多说,“Ruby 模块主要有两个用途。第一个,正如我向你展示的,是将新行为混入到 Ruby 类中。第二个叫做 命名空间。你可以把它看作是为你命名的东西——主要是方法和常量——创建独立的空间。”他把太阳镜推到鼻梁上。“你看,如果你定义一个方法,并给它起个名字,然后再定义同名的方法,Ruby 会用新定义的方法替换旧方法。但如果你把一个同名的方法放进模块里,而模块外面也有一个同名的方法或常量,那你就可以同时使用它们!”
“模块必须创建一个新的作用域!”鲁本说。“所以有两个同名的方法,把一个放在模块里就像是有两瓶相同的汽水,一个在冰箱里,另一个不在。对于方法来说,一个在模块里(冰箱里),另一个不在,所以你可以根据它所在的位置来判断它们的区别。”
“没错,”哈尔多说。
“而且我们刚才说的关于方法的内容,对常量也适用吧?”斯卡雷特问道。
“它的确能!”哈尔多回答道。
“如果把一个模块放到另一个模块里面会发生什么?”鲁本问道。
“你只需要继续使用那些 :: 点,”哈尔多说。“例如,如果你在 Colorize 模块里面有一个 Pastel 模块,想要访问 Pastel 模块里面的 NUMBER_OF_PASTEL_COLORS 常量,你可以写成 Colorize::Pastel::NUMBER_OF_PASTEL_COLORS。”
“如果模块内部的内容都进行了命名空间处理,就像你说的那样,”斯卡雷特问道,“那是否意味着你可以有两个同名的东西,一个在模块里面,另一个在模块外面?”
“完全正确!”哈尔多说道。他打字:
>> **module Namespace**
>> **GREETING = 'Hello from INSIDE the module!'**
>> **end**
=> nil
>> **GREETING = 'Hello from OUTSIDE the module!'**
=> "Hello from OUTSIDE the module!"
“这里,我定义了两个同名的常量:GREETING。第一个在 Namespace 模块内部,另一个在主作用域中,外面没有任何模块。我们可以通过这种方式告诉 Ruby 要获取哪一个。”他又打了几行代码:
>> **GREETING**
=> "Hello from OUTSIDE the module!"
>> **Namespace::GREETING**
=> "Hello from INSIDE the module!"
“我明白了!”鲁本说。“那两个冒号告诉 Ruby 使用哪个作用域!”他停顿了一下。“我们能不能对类方法也做这些操作?我的意思是,如果一个模块能包含用 def 创建的方法,那它是不是也可以有方法通过 self.def 被添加到包含的类中?”
哈尔多点了点头。“你可以使用作用域解析运算符来获取类方法和常量,但在 Ruby 中,我们通常用点号获取类方法,用两个冒号获取常量。因为这个方法是类方法,”他继续说道,“它就像是在普通对象上调用一个方法一样。记住,类本身就是对象!这里有个例子——我们并没有定义这些方法,所以代码不会运行,但它看起来大概是这样的:
MyClass.fancy_class_method
MyClass::CLASS_CONSTANT
“呼!”国王说着,坐在一捆干草上。“我觉得我已经明白了——竟然如此简单。”斯卡雷特和鲁本相视而笑。
“我不明白的是,”国王继续说道,“这些家伙怎么这么快就破坏了 Colorize 模块。他们只在马厩待了几秒钟!他们打字的速度有多快?”
“我想我可能刚刚找到了答案,”女王说,她一直在检查熊猫配给机 3000。她伸手到机器的一侧,拉出一块有些磨损的金属片。
“那是什么?”哈尔多问。
“这个,”女王说,“是一个 Key-a-ma-Jigger。它是一个可以预先加载代码的小设备。我们的捣蛋鬼们一定知道 Provisionator 是如何工作的,并将一些代码预先加载到这个小机器上来破坏它。他们只需要插上去并运行!”
“甜玉米松饼!”国王说,“我们对上了专业人士。”
“我倒是说,”鲁本皱着眉头说,“我们怎么抓住他们?他们现在肯定已经跑了一英里远了。”
女王一直在研究 Key-a-ma-Jigger,她嘴角微微上扬。“我想我也知道这个,”她说,“看!Key-a-ma-Jigger 是五个一组出售的,而这个上面还挂着一圈,这意味着这很可能是它们的最后一个!”她把那台小机器紧紧握在手中。“我猜它们需要更多,而全王国只有一个地方生产 Key-a-ma-Jigger。”
“哪里?”斯卡雷特、鲁本和哈尔多同时问道。
“是的,亲爱的,哪里?”国王问。
“重构工厂!”女王回答道。
“重构工厂!”哈尔多说,“那个地方在王国的中心,环路可以在几分钟内带你到那里!”
“走吧!”国王说,“我们直接乘快车去王国中心。我们要抓住这些带毒的犯罪分子,现行抓捕!”
“去环路!”女王说。她转向哈尔多,“哈尔多,你介意留下来修好 Provisionator 吗?”
“一点也不,陛下,”他说,“这应该不需要太久。”
“谢谢你,”女王说。她转向其他人,说道,“快点,赶快!”说完,她们冲出了马厩,朝着宫殿旁的小山上的环路平台跑去。
一匹不同颜色的马
现在你已经了解了模块是如何工作的,你可以帮助哈尔多修复熊猫配给机,并让所有的熊猫恢复原来的颜色!如果运气好的话,你将能够及时修好它们,参加紫色熊猫狂潮游行。
让我们先创建一个新的文件,命名为 colorize.rb,并输入以下代码。实际上,这次我们会创建两个文件:一个是模块文件,另一个是包含该模块的类文件。
colorize.rb
module Colorize
def color
:purple
end
end
首先,我们设置了 Colorize 模块,并创建了一个非常简单的 color 方法,它只返回我们想要的颜色(:purple)。
在你电脑的同一个文件夹里,创建一个名为 panda_food.rb 的文件,并在其中输入以下代码。写两个文件而不是一个可能有点奇怪,但这里没有什么是你不知道怎么做的!
panda_food.rb
➊ require './colorize'
➋ class Food
def serve
puts 'Food is ready!'
end
end
➌ class PandaFood < Food
➍ include Colorize
attr_accessor :calories
➎ CALORIES_PER_SERVING = 1000
def initialize
@calories = CALORIES_PER_SERVING
end
def serve
puts 'One piping hot serving of panda food, coming up!'
end
➏ def analyze
puts "This food contains #{@calories} calories and is #{color}."
end
end
➐ hogarths_food = PandaFood.new
puts hogarths_food.analyze
首先,我们在 panda_food.rb 文件的 ➊ 处 require 了 colorize.rb。接着,我们定义了一个非常简单的 Food ➋ 类,PandaFood 类继承自它 ➌,并且我们在 PandaFood 类的 ➍ 处包含了 Colorize 模块。最后,我们加上了一个常量来告诉我们每份食物的卡路里数 ➎,并定义了一个 analyze 方法来告诉我们食物的卡路里含量和颜色 ➏。(在对待食物这件事上,你永远不能太小心!)最后,我们创建了 PandaFood 的一个实例并调用了它的 analyze 方法 ➐。
和往常一样,尝试通过命令行输入 ruby panda_food.rb 来运行你文件中的代码。确保你在与你的 panda_food.rb 文件相同的文件夹中,然后输入:
$ **ruby panda_food.rb**
你应该能看到这个:
This food contains 1000 calories and is purple.
紫色的熊猫食物!我们的熊猫得救了!
这应该能够很好地满足 Haldo 的需求,但你可以通过一点努力让这段代码更好(你可以直接从 Refactory 购买,价格低廉,仅为九十九九九九十九九十九五)。例如,我们的 Colorize 模块只有一个方法,它做的就是返回紫色。我们如何修改 color 方法,以便设置任何我们想要的颜色呢?我们还可能想在 Colorize 中添加哪些方法?
我们对 Food 类的使用也不多——PandaFood 覆盖了 Food 唯一的一个方法!我们还能在 Food 中添加什么内容来让它变得更好呢?(提示:可能性是无穷无尽的!)
最后,记住 Haldo 在 创建模块 中看到的被篡改过的代码吗?它是这样的:
module Colorize
def color
[:red, :blue, :green, :yellow].sample
end
end
如果你有冒险精神,试试把 colorize.rb 文件中的代码改成这个,然后重新运行 ruby panda_food.rb。看看每次颜色是如何变化的,就像我们的英雄们看到的那样?
你知道这个!
我能看出你对这个模块的操作非常熟练。(我是一个非常出色的性格判断者。)不过,为了确保我也能掌握这一点,我们再复习一遍。Haldo 解释了很多,而我没做什么解释,所以我想确保这一切都已经牢牢地记在我的脑袋里。
首先,我们学习了模块,了解它们基本上就像类一样,只是你不能使用 new 方法来创建它们的实例。我们看到,我们可以将模块作为 命名空间 使用,这只是一种高级说法,意思是它们让我们能够像这样优雅地组织代码:
module Bucket
MAX_BITS_AND_TRINKETS = 100
def announcing_bits_and_trinkets
puts 'Step right up! Bits and trinkets available now!'
end
end
我们还学习了 常量(如 MAX_BITS_AND_TRINKETS),它们与 Ruby 变量类似,只是它们的值应该是不变的。(你可以修改它们,但 Ruby 会发出严厉的警告。)常量总是用大写字母表示。
我们还看到了,可以通过使用 include 或 extend 来将模块作为 混入 使用。当我们使用 include 时,它会将模块中的所有方法添加到包括它的类的实例中;当我们使用 extend 时,这些模块方法会被添加到类本身:
module Greetings
def sailor
puts 'Ahoy there!'
end
def pirate
puts 'Avast, ye salty dog!'
end
def robot
puts 'BEEP BOOP WHAT IS UP'
end
end
在这里,我们刚刚创建了一个包含几个方法的Greetings模块。接下来,我们将创建一个Message类,并包含Greetings模块:
class Message
include Greetings
end
然后我们会看到,任何Message的实例都可以使用Greetings中定义的方法!
>> **message = Message.new**
=> #<Message:0x007fd6022c7948>
>> **message.pirate**
Avast, ye salty dog!
=> nil
如果我们改用extend Message与Greetings,那么Greetings的方法将可以被Message类本身使用:
>> **class Message**
>> **extend Greetings**
>> **end**
=> nil
>> **Message.robot**
BEEP BOOP WHAT IS UP
=> nil
记住,现在拥有robot方法的是Message类本身,而不是Message的实例!如果我们尝试创建Message的实例并调用它的robot方法,就会报错:
>> **my_message = Message.new**
=> #<Message:0x000001030cdf88>
>> **my_message.robot**
NoMethodError: undefined method `robot' for
#<Message:0x000001030cdf88>
但是如果一个类包含了Greetings模块,那么该类的实例就会拥有该方法:
>> **class Message**
>> **include Greetings**
>> **end**
>> **my_message = Message.new**
=> #<Message:0x00000103108d18>
>> **my_message.robot**
BEEP BOOP WHAT IS UP
=> nil
通过将模块包含进已经继承自其他类的类中,我们可以在保持只有一个父类的简洁性的同时,享受从多个类继承的所有好处:
module Enchanted
def speak
puts 'Hello there!'
end
end
在这里,我们再次创建了我们的Enchanted模块,并包含了它那经过验证的speak方法。
class Animal
def initialize(name)
@name = name
end
end
class Dog < Animal
include Enchanted
def bark
puts 'Arf!'
end
end
我们之前已经看到过这个例子:我们只需定义一个Animal类和一个继承自它的Dog类。Dog类有一个方法:bark。
>> **bigelow = Dog.new('Bigelow')**
=> #<Dog:0x000001049df148 @name="Bigelow">
>> **bigelow.bark**
Arf!
=> nil
>> **bigelow.speak**
Hello there!
=> nil
最后,我们看到Dog类的实例,例如bigelow,可以使用bark(它从Dog继承而来)和speak(它从Enchanted继承而来)!
当我们的模块和类都在同一个文件中时,这一切都很顺利,但如果它们不在同一个文件呢?没错:我们可以使用require!为了将我们写的一个文件导入到另一个文件中,我们只需使用require方法,并提供一个字符串,指明我们想要的文件名(不需要.rb扩展名)。记住,我们需要使用点和斜杠来告诉 Ruby 去哪里查找:./表示“在当前文件夹中查找”,而../表示“跳出当前文件夹并四处查找”。如果我们想向上跳两个文件夹,可以使用../../;如果我们想访问当前文件夹中但嵌套在* fancy_things和genius_ideas文件夹中的genius_idea_3.rb*文件,我们就写./fancy_things/genius_ideas/genius_idea_3。
所以举个例子,如果我们将colorize.rb与下面的 Ruby 脚本放在同一个文件夹中,我们会这样写:
require './colorize'
class Food < PandaFood
include Colorize
# ...and so on and so forth
end
最后,你看到我们可以使用作用域解析运算符来访问模块中的特定常量(甚至是深层嵌套的常量!),而且我们可以像往常一样使用点语法来获取类方法:
MyClass::AModuleInsideThat::YetAnotherModule::MY_CONSTANT
MyClass.some_method
有了这些,你现在已经正式掌握了关于 Ruby 类和模块的所有知识!(好吧,好吧,总是有更多可以学习的,但你已经知道了写日常 Ruby 程序所需的所有内容。)事实上,你已经学会了这么多 Ruby,以至于我们现在要从学习新东西中短暂休息一下,集中精力重写一些我们已经知道的代码。重写代码,目的是让它做相同的事情,但看起来更好或运行得更快,这叫做重构,而——幸运的是!——这正是 Refactory 的核心内容。
第十一章 第二次的机会
在重建厂进行重建
国王、女王、鲁本和斯嘉丽在环形平台停稳的瞬间跳了下来,门口嗖的一声打开。他们直奔重建厂那扇闪闪发光的红色金属大门,站台出口的方向就能看到它们。
“就在这里!”国王说道。“快点!”他们走近时,守卫大门的两名守卫匆忙地拉开大门,同时试图敬礼。
他们四人飞快地穿过大门,沿着一条长长的铺砌道路行驶。前方出现了重型的重建厂:一座巨大的红色金属建筑,顶部有十几个烟囱冒出看起来十分宜人的粉红色烟雾。

他们来到了一扇大大的闪亮双开门,门被撑开着。从里面透出温暖的红光。国王和女王毫不犹豫地走了进去,鲁本和斯嘉丽紧随其后。
“我的好朋友!”国王大声喊道,挥手示意一位拿着文件夹的戴安全帽的男子。“我们有紧急情况!我们需要立刻与工头谈话!”
那名男子抬起头,差点把文件夹掉下来。“陛下!”他说道。“当然,当然!马上!”他一边抓住安全帽,一边拿着文件夹飞奔进入了重建厂的深处。
国王、女王、鲁本和斯嘉丽站在入口处,喘着气。斯嘉丽四下张望。“我们在哪里?”她问。
“这是重建厂的主入口,”女王说道。她点了点头,指向建筑内部更远处的光亮。“那边是重建厂的生产区,所有的实际工作都在那儿进行。”
“他们在这里制造 Key-a-ma-Jiggers 吗?”鲁本问道。
女王点点头。“除了其他的事情,”她说。
鲁本张开嘴准备问重建厂还生产什么,但就在这时,戴着安全帽的年轻人回来,身后跟着一个年长的男子,眼睛闪闪发光,胡须浓密而蓬松。

“陛下!殿下!”那位年长的男子对国王和女王说,向每位王室成员鞠了一躬。“我能为您做什么?”
“封锁工厂!”女王说道。“我们有理由相信重建厂内有闯入者,必须将他们阻止!”
那位留着胡子的男子简短地点了点头,穿过重建厂狭窄的入口,走到一部明亮的红色电话旁。他拿起听筒,拨了一个数字。当他对着电话说话时,他的声音回荡在整个重建厂内:
-
封锁所有出口!这不是演习!
-
封锁所有出口!这不是演习!
老人把手放在电话的听筒上。“这些闯入者长什么样?”他问道。
“我们不确定,”女王说道。
“但他们有四个。”那位男子再次点了点头,然后继续拨打电话:
开始逐区搜索四个入侵者!拘留任何可疑人员并立即报告!
说完,他挂掉电话,微笑着走回到他们那边。
“这应该能解决问题,”他说。“如果 Refactory 里有任何入侵者,我的团队会立刻找到他们,并立即通知我们。”
“非常感谢!”Scarlet 说道。“但是,嗯,你到底是谁呢?”
“为什么,我是工头,Rusty Fourman!”那人说着,掀起了他的安全帽。“我是这里 Refactory 所有操作的负责人。”他指了指从他那儿叫来的年轻人。“这是我的得力助手 Marshall Fiveman。”
“很高兴见到你,”Marshall 说道。
“我也很高兴见到你!”Ruben 说道。
“Rusty 一直在管理 Refactory,我记得的所有时间里都是这样,”国王说道。
“那有多长?”Scarlet 问道。
“哦,我不知道,”国王说。“至少需要几天时间吧。”
“好多年了!”Rusty 笑着说道。他拉了拉自己的胡子,突然变得严肃起来。“我猜这些入侵者就是把你们带到我这的吧。我毫不怀疑我们很快就能抓到他们,但你知道他们可能在这里做什么吗?”
“是的!”Scarlet 一边从口袋里摸索,一边说道。“你们做这些的吗?”她问道,递出了那个 Key-a-ma-Jigger。
Rusty 盯着她手中的小块金属看了看。“嗯,是的,我们确实在这里制作 Key-a-ma-Jiggers,”他说。“还有一些其他的东西。大多数时候,我们的工作是重构 Ruby 代码。”

“重构?”Ruben 问道。“那是什么?”
“基本上就是在重写你的程序时,”Rusty 说。
“重写它们?!”Ruben 说。“可是我第一次写它们花了那么多时间!为什么还要再写一遍?”
“因为你可以让你的代码变得更快,更容易阅读,或者更容易更新,而且它仍然能完成相同的工作。”Rusty 说道。他想了想。“如果我给你示范一下可能更容易些。我们可以做几个更常见的 Ruby 重构,我觉得你很快就能明白了。”他看了看手表。“工厂封得严严实实,等我团队找到你的嫌疑人就只是时间问题了。与此同时,让我们做点 Ruby 重构吧!”
工头示意他们走近一些,并带他们深入 Refactory,朝着那个温暖的光芒走去,那个光芒将建筑内的一切都染成了深红色。他走到一根长长的拱形栏杆旁,俯瞰着一个看起来像是熔化的红色金属缓缓冒泡的池子,打开了一个看起来很熟悉的机器——计算装置!国王、皇后、Scarlet 和 Ruben 走到他旁边,他开始在键盘上打字。
“那么,” Rusty 一边用一只手挠鼻子,一边继续用另一只手打字,“在我所有在 Refactory 的岁月里,我见过很多 Ruby 代码。随着时间的推移,我发现了一些非常有效的代码模式,也有一些效果不太好的模式。你们想看看一些好的模式吗?”他问道。
“当然!”国王回答道。
变量赋值技巧
“比如说,”Rusty 说,“我经常看到这样的代码,代码作者希望把一个变量设置为某个特定的值,但仅仅是在该值尚未被设置时。所以我可能会写这样的代码,检查某个变量是否是nil,如果是,就把它设置为默认值。”他继续键入:
>> **rubens_number = nil**
=> nil
>> **if rubens_number.nil?**
>> **rubens_number = 42**
>> **end**
=> 42
“这对我来说完全没问题,”国王说。
“哦,这完全是正确的 Ruby 代码,”Rusty 说,“它将做我们预期的事情——因为rubens_number是nil,Ruby 会把它设置为42。但是,有一种更清晰的写法!”他继续键入:
>> **rubens_number ||= 42**
=> 42
>> **rubens_number**
=> 42
“你可以把||=看作是||(‘或’)和=(变量赋值)的结合体,”Rusty 说。“这个组合意味着:‘如果rubens_number没有值,就将它设置为42。’这和键入下面的代码是一样的!”他继续键入:
>> **rubens_number = nil**
=> nil
>> **rubens_number = rubens_number || 43**
=> 43
“如果变量已经有值怎么办?”Scarlet 问。
“让我们来看看!”Rusty 说。他继续键入:
>> **scarlets_number = 700**
=> 700
>> **scarlets_number ||= 42**
=> 700
>> **scarlets_number**
=> 700
“在这种情况下,”Rusty 说,“scarlets_number已经有了值700,所以||=什么也不做。正如我提到的,||表示‘或’,你可能已经见过=表示‘给变量赋值’。”Scarlet 和 Ruben 点了点头。
“所以,”Rusty 继续说,“当我们写||=时,我们告诉 Ruby:‘嘿!你应该条件性地赋值给这个变量。’这其实是个复杂的说法,意思是我们希望 Ruby 使用它已经知道的值或者使用新值,前提是该变量尚未被设置。对于rubens_number,因为没有值,所以赋值为42;对于scarlets_number,我们已经设置了700,所以||= 42什么也不做。”
“但我们能写这个吗?”Scarlet 问道,并键入:
>> **rubens_number = 42 if rubens_number.nil?**
=> 42
“哎,是的!”Rusty 说,他的大胡子随着笑容向上翘起。“我不一定会在这个例子中使用那个代码,因为我可以轻松地使用||=,但是在 Ruby 中,使用内联if和unless进行重构是一个非常常见的做法。”
“内联是什么意思?”Scarlet 问道。
“我来给你演示!”Foreman 说,他继续向计算装置输入更多代码:
>> **if !rubens_number.nil?**
>> **puts 'Not nil!'**
>> **end**
Not nil!
=> nil
“那也能完成任务,”Rusty 说,“但是为什么要使用if和!,如果我们可以直接使用unless呢?”
>> **unless rubens_number.nil?**
>> **puts 'Not nil!'**
>> **end**
Not nil!
=> nil
“现在,这稍微好一点,”Rusty 继续说,“但它仍然比我们需要的代码行数多。如果我们有if或unless,但是没有else,我们可以把整个条件写成一行,像这样。”
>> **puts 'Not nil!' unless rubens_number.nil?**
Not nil!
=> nil
“现在,这个是最棒的!”Rusty 说。“我们不仅可以把if !转换成unless,还可以把unless与我们正在测试的变量写成一行!”
“我们也能在if中这么做吗?”Scarlet 问。
“没错!”Rusty 说,他继续键入:
>> **puts '42! My favorite number!' if rubens_number == 42**
42! My favorite number!
=> nil
“现在,就像if一样,我们也可以在unless中使用else,”Rusty 说,“但是虽然if/else对我来说很容易理解,unless/else却让我感到困惑。”
Crystal-Clear 条件语句
“我同意,”国王说道,揉了揉头。“所以我们应该把if !转换成unless,并且如果没有else,我们可以将if或unless写成一行?”
“完全正确,”Rusty 说。“这个很容易让人困惑:”
>> **unless rubens_number.nil?**
>> **puts 'Not nil!'**
>> **else**
>> **puts 'Totally nil.'**
>> **end**
Not nil!
=> nil
“但是 这个 一目了然!”
>> **if rubens_number.nil?**
>> **puts 'Totally nil.'**
>> **else**
>> **puts 'Not nil!'**
>> **end**
Not nil!
=> nil
“事实上,”Rusty 继续说道,“我们可以把它们写成两行一行的语句——一个 if 和一个 unless。我觉得这不太容易理解,但我会给你们演示一下,以防你们有兴趣。”
>> **puts 'Not nil!' unless rubens_number.nil?**
Not nil!
=> nil
>> **puts 'Totally nil.' if rubens_number.nil?**
=> nil
“记住,”Rusty 说道,“puts 返回 nil,所以我们才会在 => 后面看到它。但因为 rubens_number 是 42 而 不是 nil,Ruby 就不会打印 'Totally nil.'。”
“我觉得 if/else 那个最容易理解,”Ruben 说道,“但它仍然多了很多行。如果有 else,有没有更简单的写法?”
“正好有,”Rusty 说道,“我们可以使用一个 三元运算符。它长这样!”
>> **puts 1 < 2 ? 'One is less than two!' : 'One is greater than two!'**
One is less than two!
=> nil
“失落的扭曲扎带的甜美幽冥!”国王大声喊道,“我们这片和平王国里,究竟是什么东西?”
“它并不像看起来那么可怕。我们只需在代码中使用一个问号,后面跟着冒号,”Rusty 说道,“在这种情况下,我们希望代码通过 puts 打印出某些内容。接下来,我们给 Ruby 一个 表达式:这个表达式的结果要么为真,要么为假。在这个例子中,表达式是 1 < 2。”Rusty 摸了摸胡子。“然后我们写一个问号,后面跟着如果表达式为真时 Ruby 应该做的事情。最后,写一个冒号,后面跟着如果表达式为假时 Ruby 应该做的事情。因为 1 确实 小于 2,所以 Ruby 会打印出 One is less than two!。”他沉思了一下,“实际上,你可以把它当作写一个 if/else,只是把它写成了一行。? 就像是简写版的 if,而 : 就像是简写版的 else。”
“真是太神奇了,”女王说道,“不过你不觉得它有点难读吗?”
“有时候,”Rusty 承认道,“所以我通常会坚持使用常规的 if/else。但是如果代码很简短,我有时会把 if/else 重构成 ? :。”
“如果你想检查的表达式是一个带问号的方法呢?”Ruben 问道,“三元运算符还能工作吗?”
“哦,是的,”Rusty 说道,他迅速打字:
>> **bill = nil**
=> nil
>> **puts bill.nil? ? "Bill's nil!" : "Bill's not nil at all."**
Bill's nil!
=> nil
“那第三行可能看起来有点棘手,两个问号这么近,”Rusty 说道,“所以你要小心使用它们。记住,nil? 是 Ruby 内置的方法,如果调用的对象是 nil,它返回 true,否则返回 false。”
“还记得,nil 被返回是因为 puts 没有返回值,而不是因为它返回了 bill!”女王说道。
“完全正确,完全正确,”Rusty 说道。
“这看起来不错,”Scarlet 眯着眼睛看着计算装置的屏幕,“但是我觉得一长串 ? : 符号——甚至是 if/else!——会变得难以阅读。有没有一种好的方法来写代码,当 Ruby 需要做很多不同的事情,而我们不必到处写 if 和 else?”
何时需要使用 case 语句
“你对重构有着敏锐的眼光,”Rusty 说。“的确,我们可以用某些东西来替代 Ruby 中的if和else。虽然我自己不常用它,”他继续说道,“但它确实比长链条的if、elsif和else要更具可读性。它被称为case语句。来看看吧!”他说着输入了代码:
>> **number = 1**
>> **case number**
>> **when 0**
>> **puts "Zero!"**
>> **when 1**
>> **puts "One is fun!"**
>> **when 2**
>> **puts "Two. It's true!"**
>> **when 3**
>> **puts "Three for me."**
>> **else**
>> **puts "#{number}? I don't know that one."**
>> **end**
One is fun!
=> nil
“我们使用case关键字告诉 Ruby 要关注哪个变量,”Rusty 解释道。“然后我们可以使用when来表示:当这个值符合条件时——也就是说,当这个值是我们正在查看的变量时——执行这个操作!”
“就像if和unless一样,我们用else来让 Ruby 在没有匹配时执行某些操作,”Ruben 说。
“完全正确,”Rusty 说。
“但这就是case语句能做的全部吗?”Marshall 插话道。“在我看来,仅仅检查一个变量是否为某个数值似乎没那么有趣。”
“哦,天哪,不,”Rusty 说。“它们可以变得非常复杂!”他说着输入了代码:
>> **number = 7**
>> **case number**
➊ >> **when 0**
>> **puts "That's definitely zero."**
➋ >> **when 1..10**
>> **puts "It's a number between 1 and 10, all right."**
➌ >> **when 42**
>> **puts "Ah yes, 42\. My favorite number!"**
➍ >> **when String**
>> **puts "What? That's a string!"**
>> **else**
>> **puts "A #{number}? What in the world is a #{number}?"**
>> **end**
It's a number between 1 and 10, all right.
=> nil
“我们可以检查一个数字是否为特定的值,比如0(➊)或42(➌),是否在一个范围内(➋),甚至是否是某个特定类的实例,比如String(➍),”Rusty 说。“case语句能快速完成很多if和else要花很长时间处理的工作。”
“那确实很高级,”国王说,“但如果我从 Ruby 学到了一件事,那就是最令人愉快的时刻就是我能在不写出每个细节的情况下完成任务。有没有这样的重构方法?”
简化方法
“我就知道你会问,”工头说道。“这是一个老方法,但仍然很棒。你知道方法和return吗?”
他们都点了点头。
“完美,”他说。“如你所知或所不知,Ruby 方法会自动返回它们评估的最后一段代码的结果。这意味着,如果你希望方法返回它评估的最后一个表达式,你完全可以省略return关键字。让我们定义一个简单的方法,检查它所接收的参数是否为true。”
>> **def true?(idea_we_have)**
>> **return idea_we_have == true**
>> **end**
=> nil
“现在,如果idea_we_have是true,这将return true,如果不是,则返回false,”Rusty 说,“但事实证明,Ruby 会自动返回它运行的最后一段代码的结果。我们根本不需要return!”
>> **def true?(idea_we_have)**
>> **idea_we_have == true**
>> **end**
=> nil
“啊,是的!”国王说道。“我想我们以前见过这种 Ruby 的魔法。”
“好吧,”Rusty 说,“但试试这个。若你有一个会返回布尔值的表达式——也就是说,它最终会是true或false——你不需要用==将它与true或false比较。那只是一个额外的步骤!你可以直接返回那个会是true或false的变量本身。”他说着在计算装置中输入了代码:
>> **def true?(idea_we_have)**
>> **idea_we_have**
>> **end**
=> nil
>> **most_true_variable = true**
=> true
>> **true?(most_true_variable)**
=> true
“most_true_variable是true,并且由于我们的方法会自动返回传入的任何参数,它就返回了true,”工头解释道。
“太棒了!”皇后说。“我喜欢这个方法是如此简单。不过,这只适用于true或false的变量吗?”
Rusty 点了点头,“虽然还有另一种很好的重构方式,能让我们判断 Ruby 值是否是 真值。”

“真值?”Ruben 和 Scarlet 一起问。
“真值!”Rusty 说,“当我说 Ruby 值是 真值 时,我的意思是:这个值不是 false 或 nil。记得这两个值在 if 和 unless 中是怎么工作的吧?”他问道,然后输入了:
>> **my_variable = true**
=> true
>> **puts 'Truthy!' if my_variable**
Truthy!
=> nil
“因为 my_variable 是 true,而 true 是一个真值,所以 if 语句的代码执行了,Ruby 打印出了 'Truthy!',”Rusty 说,“现在让我们看看,如果我们用 false 做同样的事情会发生什么。”
>> **my_variable = false**
=> false
>> **puts 'Truthy!' if my_variable**
=> nil
“什么都没打印!”国王说。
“没错,”工头说,“my_variable 是 false,所以 'Truthy!' 不会在屏幕上打印出来。同样的事情也发生在 nil 上。”
>> **my_variable = nil**
=> nil
>> **puts 'Truthy!' if my_variable**
=> nil
“false 或 nil 没有打印任何东西,因为它们是 假值;Ruby 中的其他所有值都是一个真值,”Rusty 解释道,“看看吧!”他又打了一些代码:
>> **my_variable = 99**
=> 99
>> **puts 'Truthy!' if my_variable**
Truthy!
=> nil
“不过,你会看到,nil 和 false 并不 完全 相同,99 和 true 也并不 完全 相同。”他再次输入:
>> **nil == false**
=> false
>> **99 == true**
=> false
“但是!”他大声说道,举起一个手指,“我们可以通过一个简单的 !! 把一个 真值 转换成 true,把一个 假值 转换成 false。你看,第一个 ! 让 Ruby 返回一个布尔值,但由于 ! 的意思是‘非’,它与我们想要的相反。第二个 ! 通过撤销第一个 ! 的效果,修正了这一点!”国王、Scarlet、Ruben 甚至女王都显得困惑。“这里,我来给你们演示,”工头提议,然后他在计算机装置上输入:
>> **truthy_value = 'A fancy string'**
=> "A fancy string"
>> **falsey_value = nil**
=> nil
>> **truthy_value**
=> "A fancy string"
>> **!truthy_value**
=> false
>> **!!truthy_value**
=> true
“所以 truthy_value 是一个字符串,”Scarlet 说,“由于它不是 false 或 nil,如果你把它放进 if 语句中,代码会运行。”
“没错,”Rusty 说。
“所以,”Scarlet 说,“!truthy_value 是 false,而 不是 !truthy_value——也就是说,!!truthy_value——是 true!”
“你明白了!”Rusty 说,“现在,看看对于假值是怎么工作的。”
>> **falsey_value**
=> nil
>> **!falsey_value**
=> true
>> **!!falsey_value**
=> false
“这完全相反!”Ruben 说,“nil 是假值,所以 !nil 是 true,而 !!nil 是 false。”
“完全正确,”工头说,“我们甚至可以写一个方法来检查某个东西是否为真值,像这样。”
>> **def truthy?(thing)**
>> **!!thing**
>> **end**
=> nil
>> **truthy?('A fancy string')**
=> true
>> **truthy?(nil)**
=> false
“在这种情况下,我们定义了一个 truthy? 方法,它接受一个参数 thing,”Rusty 说,“然后我们调用 !!thing:第一个 ! 如果 thing 是真值会返回 false,如果 thing 是假值会返回 true。由于这与我们想要的相反,我们使用 !! 来让我们的函数在 thing 是真值时返回 true,在 thing 是假值时返回 false。”
“太棒了!”Scarlet 说。
“不是吗?”Rusty 说,“一旦我们定义了 truthy?,我们就可以对 'A fancy string' 调用它,看到它是一个真值,然后对 nil 调用它,看到 nil 是假值。”
“我们还能做什么来让我们的 Ruby 程序更简洁、更清晰?”Ruben 问道。
“嗯,这个可能看起来很明显,”Rusty 说,“但实际上它是编程中最难的部分之一——给变量、方法和常量起个好名字!”
“什么意思?”Marshall 说道,他正在愤怒地在写字板上乱写。
“好吧,让我们以truthy?方法为例,”Rusty 说道,“看看如果我们选了一个更笨拙的名字会发生什么。”他迅速敲入:
>> **def is_this_a_truthy_thing_or_not?(thing)**
>> **return !!thing**
>> **end**
=> nil
“看起来太糟糕了,”国王说道。
“是的,确实可以,”Rusty 说道,“不仅如此,它还包含了一个额外的return,而我们并不需要这个。简单的方法要好得多。”
>> **def truthy?(thing)**
>> **!!thing**
>> **end**
=> nil
“啊!我明白了,”国王说道,“我们希望给我们创建的 Ruby 对象取一些简单、容易记住的名字,这样我们就可以少写代码,并且在以后引用代码时减少错误。”
“太好了!”Rusty 说道,“想象一下如果每次想检查一个值是否为真值,我们都得输入is_this_a_truthy_thing_or_not?,那简直是疯了!”
“我们还能怎样减少重复写代码的情况?”Marshall 问道。
去重代码
“好吧,有一种很好的方法就是在我们能做到的时候移除重复代码!”Rusty 说道,“在我们的程序中复制粘贴代码太容易了,这样一来如果变量名或值发生变化,修改程序就变得非常困难。看看这个,”他说着,敲入:
>> **def king?(dude)**
>> **if dude == 'The King'**
>> **puts 'Royal!'**
>> **else**
>> **puts 'Not royal.'**
>> **end**
>> **end**
=> nil
>> **def queen?(lady)**
>> **if lady == 'The Queen'**
>> **puts 'Royal!'**
>> **else**
>> **puts 'Not royal.'**
>> **end**
>> **end**
=> nil
“我在这里定义了两个方法,”Rusty 说道,“第一个,king?,检查传入的参数是否是'The King';如果是,它puts 'Royal!',否则输出'Not royal.'。我还定义了第二个方法,queen?,它检查传入的参数是否是'The Queen'。你看,这部分代码重复了多少?”Rusty 继续说道,“打这些代码非常无聊,而且更糟的是,如果我们想要改变任何打印出来的消息,我们得在两个地方做修改!我宁愿这么写,”他说着,然后他确实这么做了:
>> **royal?(person)**
>> **if person == 'The King' || person == 'The Queen'**
>> **puts 'Royal!'**
>> **else**
>> **puts 'Not royal.'**
>> **end**
>> **end**
=> nil
“现在我们有了一个方法,它做了两个方法的工作,”Rusty 说道。
>> **royal?('The King')**
Royal!
=> nil
>> **royal?('The Queen')**
Royal!
=> nil
>> **royal?('The jester')**
Not royal.
=> nil
“我喜欢这个方法多了,”Ruben 说道,“如果我们想的话,我们也可以使用三元运算符来写对吧?”
“当然!”Rusty 说道,“如果你愿意的话,我们过一会儿可以谈这个问题。”
“在我们继续之前,”国王打断道,“我担心如果我们走得太远,将方法结合得太多,可能会导致方法做太多工作,而且很难理解。”
“这种情况经常发生!”工头说道,“虽然通常你会希望写尽可能少的代码,但有时你最终写出的是非常庞大、难以思考的方法,这些方法其实应该被拆分成更小的部分。看看前几天通过重构器来的这个方法,”他说着,敲入了计算设备:
>> **list_of_numbers = [1, 2, 3, 4, 5]**
=> [1, 2, 3, 4, 5]
>> **def tally_odds_and_evens(numbers)**
>> **evens = []**
>> **odds = []**
>> **numbers.each do |number|**
>> **if number.even?**
>> **puts 'Even!'**
>> **evens.push(number)**
>> **else**
>> **puts 'Odd!'**
>> **odds.push(number)**
>> **end**
>> **end**
>> **puts "#{evens}"**
>> **puts "#{odds}"**
>> **end**
=> nil
“首先,它设置了一些变量,”Rusty 说道,“evens数组存储偶数,odds数组存储奇数,list_of_numbers存储要检查是否为偶数或奇数的数字。”
“接下来,tally_odds_and_evens方法遍历一个数字列表,并通过 Ruby 内建的even?和odd?方法检查每个数字是偶数还是奇数。对于每个数字,tally_odds_and_evens打印出它是偶数还是奇数,然后将它添加到相应的数组中。”
>> **tally_odds_and_evens(list_of_numbers)**
Odd!
Even!
Odd!
Even!
Odd!
[2, 4]
[1, 3, 5]
=> nil
“正如你所见,”Rusty 说,“它其实挺复杂的。”
“我倒是能理解!”国王说。“我几乎一句话都听不懂。”
“如果我们把这个大方法 tally_odds_and_evens拆成几个小的、命名清晰的方法,可能会更容易理解,”Rusty 说道,他接着打字:
>> **list_of_numbers = [1, 2, 3, 4, 5]**
=> [1, 2, 3, 4, 5]
>> **def tally_odds_and_evens(numbers)**
>> **evens = []**
>> **odds = []**
>> **numbers.each do |number|**
>> **alert_odd_or_even(number)**
>> **update_tally(number, evens, oddsna)**
>> **end**
>> **puts "#{evens}"**
>> **puts "#{odds}"**
>> **end**
=> nil
“首先,我们将重写 tally_odds_and_evens方法。我们会把打印Odd!或Even!的代码移到一个单独的方法alert_odd_or_even中,而更新计数的代码则移到另一个方法update_tally。我们将在一分钟内写完每个方法,”Rusty 说。
“有道理,”国王说。
“接下来,我们将去掉写Odd!或Even!到屏幕上的部分,把它封装成一个名为alert_odd_or_even的方法。事实上,我们可以使用我们学到的三元运算符把它做成一行代码!”
>> **def alert_odd_or_even(number)**
>> **puts number.even? ? 'Even!' : 'Odd!'**
>> **end**
=> nil
“然后,”Rusty 继续说道,“我们会把更新evens和odds数组的代码提取到一个单独的方法update_tally中。”
>> **def update_tally(number, evens, odd)**
>> **if number.even?**
>> **evens.push(number)**
>> **else**
>> **odds.push(number)**
>> **end**
>> **end**
=> nil
“这就是我们之前的代码,只是被封装成了一个独立的方法。虽然它的整体结构更清晰了,但它依然和之前一样有效,”Rusty 解释道。
>> **tally_odds_and_evens(list_of_numbers)**
Odd!
Even!
Odd!
Even!
Odd!
[2, 4]
[1, 3, 5]
=> nil
“总体来说,代码稍微多了一些,”Rusty 承认,“但现在它更清晰了,我们可以独立更改打印内容或更新偶数和奇数列表的方法。”
“太棒了!”国王笑着说。“我喜欢我的 Ruby 方法就像我一样:简短且简单!”女王、Ruben 和 Scarlet 忍住了笑。
Rusty 把安全帽推到头上。“这是我能想到的所有重构,”他说。他再次看了看手表。“奇怪,我们怎么还没收到搜索团队的回音呢。你之前告诉我这些坏蛋是在找什么来着?”他想了想,突然弹了下手指。“啊,没错!是你们的 Key-a-ma-Jigger。你们就是为了这个才来的吗?”
“是的!”Scarlet 说。“我们在皇家马厩的 Panda Provisionator 3000 上发现了这个,我们猜测这是神秘坏人最后一个拥有的,所以他们可能会回到这里找更多的。”她再次把那块小金属片递给了 Foreman。
Rusty 点了点头。“是的,那是我们的一件,”他说。“如果你们的麻烦制造者正在寻找更多,他们很可能会试图进入 Tricky Things and Trinkets 的保险库!”
“天啊!”女王说道。“那是什么?”
“这是我们放置大量物品的地方,”Rusty 说,“比如我们发现特别难重构的 Ruby 代码,还有一些东西和小玩意儿。这里也是我们存放大量库存的地方,包括我们的 Key-a-ma-Jiggers。”

国王用拳头击打掌心。“如果 Key-a-ma-Jiggers 就在那个地方,我敢肯定就是那里我们能找到罪魁祸首!”他说。“你能打个电话让你们的团队立刻前往那里吗?”
就在国王问完问题的瞬间,工头的红色电话开始不断响起。
Rusty 冲向电话并拿起它。“你好?”他说。他专心地听了一会儿,然后倒抽一口气。他用手捂住话筒。“我的一个小组在金库附近抓到了四个入侵者!”他说。他再次将电话放到耳边,然后深深叹了口气。“好吧,”他说,“派出所有可用的工人,快点!”他说完后挂断了电话。
“是什么情况?”女王问。“你们的小组抓到他们了吗?”
“没有,”工头呻吟道,“他们逃掉了!”女王的脸色变了,国王用手捂住了脸,Scarlet 和 Ruben 互相看了一眼,张大了嘴巴。
“但是!”Rusty 说,一只手举起一个手指,“我的每一位工人都在紧追不舍。我们的四个坏蛋被看到正朝着炼油厂的装货码头走去,那是一条单行道!我们会比你改名一个 Ruby 方法还要快就把他们包围起来。”
“那我们还在等什么?”女王说。“让我们去看看这段时间我们一直在追的到底是谁!”说完,她带着其他四人一起冲向了炼油厂深处的装货码头。
重新重构
练习使完美!现在你已经学会了很多方法,可以让你的 Ruby 代码更短,更易读,是时候把它们应用到几个特别棘手的方法上了。别担心,尽管你可能会有些小困难,但如果你以前的重构没有问题,接下来的这些也会轻松应对!(即使你在过程中遇到一点小挫折,到你完成这些示例时,你会成为一个重构大师。)
让我们开始吧,首先创建一个名为 first_try.rb 的新文件,并输入以下代码。这次我们将实际创建两个文件:一个是初始代码,另一个是我们要做的重构。first_try.rb定义了一个方法all_about_my_number,如果没有传入数字,它会将数字设置为42。然后,它会打印出一些关于这个数字的信息,包括数字是多少,它是正数、负数还是零。
first_try.rb
def all_about_my_number(number)
if number.nil?
number = 42
end
puts "My number is: #{number}"
if number > 0 == true
return 'Positive'
elsif number < 0 == true
return 'Negative'
else
return 'Zero'
end
end
如果你觉得这段代码不够好,别担心!我们马上就要重构它了。在你电脑的同一文件夹中,创建另一个名为 refactored.rb 的文件,并在其中输入以下代码。这段代码的功能与first_try.rb中的代码完全相同,但它看起来会更简洁。
refactored.rb
def describe_number(number)
number ||= 42
puts "My number is: #{number}"
sign(number)
end
def sign(number)
case
when number > 0
'Positive'
when number < 0
'Negative'
else
'Zero'
end
end
和往常一样,你可以通过在命令行输入ruby first_try.rb 和 ruby refactored.rb 来运行文件中的代码。由于我们在上一章中创建了两个文件,并且这里没有新代码,所以应该不会有太多意外!(虽然可能会有一些小的惊喜。)
你可能首先注意到的区别是在case语句中;之前我们做了类似这样的事情:
case number
when 0
puts 'Zero!'
# ... and so on
现在我们要做的是:
case
when number > 0
# ... and so on
这两种都是 100%正确的 Ruby 代码。如果你有一个变量,想要检查它是否等于某个值、是否是某个类的实例,或者是否在某个范围内,你就可以使用第一种语法;如果你想对一个值进行特定检查(比如number > 0),则可以使用第二种语法。
你可能还注意到我们跳过了一些重构。例如,我们从像if number > 0 == true这样的行中移除了==检查。有时候,你会开始用一种方式进行重构,然后意识到有一种更好的方法!其他时候,可能有很多种同样好的方式来重构你的代码,而你恰好选择了其中一种。
最后,我们成功地去除了大量重复代码(包括一些return语句,我们可以让 Ruby 隐式处理!),并将检查数字符号(正、负或零)的代码提取到一个独立的方法中。
我们如何让这个重构更加酷炫呢?可能有无数种方法,但这里有一些例子让你开始思考。例如,我们将nil?检查重构为||=。这样做还可以,但我们还能做些什么吗?(提示:我们在第七章中学到了设置默认参数。)另外,我们有一个if/else语句,我们将它转换成了case语句,但如果我们在某个地方使用三元操作符会更合适吗?为什么或者为什么不?请用 6000 个字以上解释你的答案。(提示:不要这么做——那会超级无聊。)
再举一个例子来挑战你的思维:我们并没有进行任何检查来确保传递给我们方法的参数确实是一个数字。如果我们传入一个布尔值呢?一个字符串呢?我们该如何重构方法,使它能够处理非数字输入呢?
你知道这个!
好了!一开始你可能觉得这一章内容不多——毕竟我们只是重新编写了我们一直在编写的代码——但事实证明,重写我们的 Ruby 代码甚至比第一次编写更具挑战性。为了确保你能应对这些挑战(提示:你绝对能),让我们再回顾一下我们所做的重构。
首先,你看到我们可以用||=有条件地设置一个值。换句话说,我们可以告诉 Ruby,如果变量没有已有值,就为它设置一个值;如果有值,就使用现有值:
>> **my_variable ||= 'pink smoke'**
=> "pink smoke"
>> **my_variable**
=> "pink smoke"
在这里,my_variable还没有被设置,因此||=将它设置为'pink smoke'。不过,如果变量已经有值,||=不会改变它。看看吧!
>> your_variable = 'blue smoke'
=> "blue smoke"
>> your_variable ||= 'pink smoke'
=> "blue smoke"
>> your_variable
=> "blue smoke"
你还看到我们可以用unless替换if !:
>> if !my_variable.nil?
>> puts 'Not nil!'
>> end
Not nil!
=> nil
>> unless my_variable.nil?
>> puts 'Not nil!'
>> end
Not nil!
=> nil
你还看到我们甚至可以将if或unless内联使用,如果不需要else的话:
>> puts 'Not nil!' unless my_variable.nil?
Not nil!
=> nil
如果涉及到else,通常最好坚持使用常规的if/else语句。
>> if true
>> puts 'True!'
>> else
>> puts 'False!'
>> end
True!
=> nil
然而,对于非常简短的if/else语句,有时候使用三元操作符会更合适,像这样:
>> puts true ? 'True!' : 'False!'
True!
=> nil
你学到了我们甚至可以在带有问号的方法上使用三元运算符!只需要确保使用两个问号:一个是方法名的一部分,另一个是三元运算符的一部分:
>> jill = nil
>> puts jill.nil? ? "Jill's nil!" : "Jill's not nil at all."
>> Jill's nil!
=> nil
我们还讨论了用case语句替换长链的if/elsif/else语句。case语句接收一个变量,根据其值执行不同的操作:
>> random_trinket = 'plastic cup'
=> "Plastic cup"
>> case random_trinket
>> when 'plastic cup'
>> puts "Plastic cup's on the up and up!"
>> when 'pet ham'
>> puts "A pet ham! What are you, an elf?"
>> when 'star monkey'
>> puts "I've always wanted one of those!"
>> else
>> puts "A #{random_trinket}, huh? Never heard of it!"
>> end
Plastic cup's on the up and up!
=> nil
接下来,我们回忆了 Ruby 的隐式返回。Ruby 方法会自动返回它们执行的最后一段代码的结果,所以这两个方法的效果完全相同:
>> def number_42?(number)
>> return number == 42
>> end
=> nil
>> number_42?(42)
=> true
>> number_42?(43)
=> false
>> def number_42?(number)
>> number == 42
>> end
=> nil
>> number_42?(42)
=> true
>> number_42?(43)
=> false
接下来,你发现当我们使用布尔变量(true或false)时,我们可以直接返回这些变量,而不是用==与true或false进行比较。这样做是有效的:
>> def thing_true?(thing)
>> thing == true
>> end
=> nil
但这样做完全相同,而且代码量稍微少一些:
>> def thing_true?(thing)
>> thing
>> end
=> nil
>> the_truest_thing_ever = true
=> true
>> thing_true?(the_truest_thing_ever)
=> true
实际上,我们可以通过在对象前面使用两个“非”符号(!)来获取任何 Ruby 值的真实性。一个真值的 Ruby 值在if语句中表现得像true,而假值则表现得像false。除了false和nil外,所有 Ruby 值都是“真值”。“真值”会执行if语句中的代码:
>> if true
>> puts 'Woohoo!'
>> end
Woohoo!
=> nil
而假值则不会:
>> if false
>> puts 'A waltz.'
>> end
=> nil
什么都没有发生!nil也没有发生任何事情:
>> if nil
>> puts 'A dill (pickle).'
>> end
=> nil
由于除了false和nil外,所有值都是“真值”,所以在if语句中,常规字符串会是“真值”:
>> if 'fancy string'
>> puts 'For a fancy king!'
>> end
For a fancy king!
=> nil
你可以通过!!在 Ruby 中检查一个值的真实性:
>> !!nil
=> false
>> !!'fancy string'
=> true
我也提到了一些你可能已经在心底知道的事情:为我们的常量、变量和方法起个好名字非常重要!看看第二个方法名比第一个方法名要好多少?
>> def is_this_value_truthy?(value)
>> !!value
>> end
=> nil
>> def truthy?(value)
>> !!value
>> end
=> nil
最后但同样重要的是,你看到删除重复代码并将程序拆分成执行非常具体任务的小方法,可以让我们的 Ruby 代码更容易编写、理解和修改。你读的代码越多,就会越发现这是真的,所以不要犹豫,去请教你身边的成年人帮你在网上找一些 Ruby 代码片段来阅读!
说到代码,我们即将看到 Ruby 语法的新篇章,当我们跟随国王、王后、斯卡利特、鲁本和拉斯蒂一起走到货运码头时。那里的不断拾取和放下将是一个完美的机会,来探索 Ruby 的输入和输出——也就是I/O——而我们很可能会第一次看到那些正在颠覆这个和平王国的恶棍们。
第十二章:阅读、写作与 Ruby 魔法
文件输入与输出
Ruben 环顾四周,叹了口气。“如果货运电梯这么慢,我们为什么要跑到这里来?”他问道。
“你知道,”国王一边揉着胡须下巴,一边说道,“我真的不知道。但我想它随时都会到达!”
就在国王刚开口时,货运电梯便带着一声巨大的铿锵声到达了。门滑开,露出一个巨大的金属电梯车厢。
“都上车!”Rusty 说道,他们纷纷爬了上去。Rusty 按下一个标有“装货码头”的圆形红色按钮,随着另一声铿锵,电梯车开始缓慢下降,驶向 Refactory 的心脏。
“我们很快就到,”Rusty 说道。
“一个慢的瞬间,”Scarlet 说道。Ruben 憋住了笑。

“别担心,”Rusty 说道。“Refactory 里面的每个工人都在那儿,所以那些坏蛋根本不可能逃脱!”
国王在电梯车里走来走去。“我等不及要审问那些无赖了,”他说道。“他们造成了这么多麻烦!我真想知道是什么让他们这么做的。”
“我敢打赌他们是邪恶的忍者巫师!”Ruben 说道。
“更像是邪恶的机器人海盗,”Scarlet 说道。
“无论他们是谁,他们都得为此负责,”女王说道。“不过我们很快就会知道。我们很接近——我能感觉到!”
“我们确实快到了,”Rusty 说道。“下一站:装货码头!”
一会儿后,货运电梯的门发出呻吟声打开,国王、女王、Scarlet、Ruben 和 Rusty 走上了 Refactory 装货码头那片巨大的繁忙的地面。
“Foreman 在这儿!”Rusty 对着一群戴着安全帽的男女喊道,同时带领大家走上金属走道,走到那间巨大的房间中心一个大平台上。“我们得到了什么?”
“先生!”Marshall 一边爬上走道,一边说道,“我赶在你们前面下来,试图评估情况。看起来我们有四个闯入者藏在其中一个装货码头里。”
“哪一个?”Rusty 问道。
Marshall 摇了摇头。“我们不知道!他们在我们看到他们去哪之前就藏了起来。我们只知道,当他们消失时,我们已经包围了码头,所以他们一定还在这里某个地方。”
Rusty 点了点头,轻轻摸了摸胡须。 “嗯,”他终于说道,“最好还是去找他们。” 他走到平台边缘,用靴子踩上一个大圆形凹陷处。随着一阵蒸汽的喷出,一根柱子从平台上升起。面向 Foreman 的一侧闪烁着一个计算装置屏幕特有的光辉。
“每个码头都由一个 Ruby 程序控制,”Rusty 说道,国王、女王、Ruben 和 Scarlet 围在他身边。“Ruby 把每个码头都当作一个文件来处理。如果我们能打开每个文件,就能找到我们失踪的罪犯!”
“一个文件?你是说像普通的计算机文件?”Scarlet 问道。
“正是!”Rusty 说道。“Ruby 几乎可以打开你能想到的任何文件:Ruby 程序、文本文件、图片,统统能打开!”
女王笑了。“我对文件了如指掌!”她说道。“我很乐意帮忙打开这些档口,找到我们的罪魁祸首。”她甩了甩手指。“有多少个文件?”她问道。
Rusty 指着远处的墙,那面墙上挂满了数百个沉重的金属门。

“哦我的天,”女王说道。“那么!我们最好开始了。”她转向 Scarlet 和 Ruben。“为了做这个,我们需要使用 Ruby 的文件输入/输出方法,”她解释道。“I/O 部分代表‘输入/输出’。输入是你放入文件中的内容,输出是文件中出来的内容。”
“就像你写文本文件或保存图片一样?”Scarlet 问道。
“非常像那个,”女王说道。“Ruby 可以将输入写入文件,就像用键盘输入然后点击保存一样。它也可以从文件中读取输出,就像双击文件并打开它一样!”
女王转向 Rusty。“我可以使用一个测试文件来展示它是如何工作的么?”她问道。
Rusty 点点头。“试试lunch.txt,”他说。“我想它里面只是有‘ONE KAT-MAN-BLEU BURGER, PLEASE’这段文字。”
“什么是 Kat-Man-Bleu 汉堡?”鲁本问道。
“这是 Refactory 自助餐厅的星期三午餐特餐!”Rusty 说道。“这里的食物没有 Hashery 的食物好,但也还行。那个文件里只是包含了最新的午餐订单。”
用 Ruby 打开文件
“非常好!”女王说道。“现在,如果你有一个名为lunch.txt的文件,里面只包含‘ONE KAT-MAN-BLEU BURGER, PLEASE’这段文字,你可以这样访问它!”她开始输入:
>> file = File.open('lunch.txt', 'r')
=> #<File:lunch.txt>
>> file.read
=> "ONE KAT-MAN-BLEU BURGER, PLEASE\n"
“这完全就像你双击lunch.txt文件一样,只不过我们可以直接在 Ruby 里读取文件的内容!PLEASE后面的\n是 Ruby 表示‘换行’的方式。如果你打开文件,它只会是‘ONE KAT-MAN-BLEU BURGER, PLEASE’这段文字,下面会有一行空白。”
女王想了想。“让我再多解释一点。File.open告诉 Ruby 根据一个名为lunch.txt的文件创建一个文件对象。”
“'r'是什么?”鲁本问道。
“那叫做模式,”女王说道,“它告诉 Ruby 以什么模式打开文件。'r'表示我们现在只是读取文件,而不是修改它。”
“好的,”Scarlet 说道,“那么,我们有一个存储在file中的文件对象。调用read方法会做什么?”
“完全是你想的那样!”女王说道。“它读取文件的内容并展示给我们看。”她停顿了一下。
“虽然通常我们是用一个块来打开文件,就像这样。”她继续输入:
>> File.open('lunch.txt', 'r') { |file| file.read }
=> "ONE KAT-MAN-BLEU BURGER, PLEASE"
“再次,我们用File.open,然后传入我们要打开的文件名作为字符串,后面跟着第二个字符串,告诉我们以什么模式打开文件。在这个例子中,我们使用了'r'表示‘读取’。”
“到目前为止明白了,”国王说道。
“我们不再像之前那样将文件对象保存到file变量中,然后调用read,”皇后继续说道,“而是将一个代码块传递给File.open。我们把file传递给代码块,然后在代码块中调用file.read!”
“用代码块打开文件和不使用代码块打开文件有什么区别吗?”斯卡雷特问。
“这是一个非常重要的区别!”皇后说,“当你用代码块打开文件时,文件会在代码块执行完毕后立即关闭。但是如果你不使用代码块打开文件,它不会自动关闭。明白了吗?”她输入了:
>> file = File.open('lunch.txt', 'r')
=> #<File:lunch.txt>
>> file.closed?
=> false
“如果你没有用代码块打开文件,怎么关闭它呢?”鲁本问。
“通过使用close方法,就像这样!”皇后说,边输入:
>> file = File.open('lunch.txt', 'r')
=> #<File:lunch.txt>
>> file.read
=> "ONE KAT-MAN-BLEU BURGER, PLEASE"
>> file.close
=> nil
“这看起来很简单,”国王说,“但是我们为什么一开始就需要关闭文件呢?”
“Ruby 会追踪我们打开的所有文件,而我们运行 Ruby 的计算机只允许我们同时打开有限数量的文件,”皇后解释道,“如果我们试图打开太多文件而不关闭它们,可能会导致计算机崩溃!”
“甜蜜的放风筝的豪猪!”国王说,“我们当然不想要那样的事情。”
“另外,如果你不关闭文件,”皇后继续说道,“Ruby 就不知道你已经完成了操作,如果你在没有正确关闭文件的情况下再次使用它,可能会发生一些意外情况。你甚至可能会不小心删除文件中的所有内容!”
“好的,我们会确保关闭我们打开的所有文件,”鲁本说,“听起来,用代码块打开文件是最简单的方式。”
“除了'r',我们还能传递什么给open方法呢?”国王边挠着他那小小的皇冠边问。“我们除了读取文件,还能做些什么呢?”
写入和向文件添加内容
“当然可以,亲爱的。”皇后说,“你看,Ruby 会按照你告诉它的方式做事,这意味着你必须非常精确地告诉它你要它做什么。当你open一个文件时,你传给open方法的第一个参数是文件名,第二个参数告诉 Ruby 你希望它对文件做什么。你可以用open做很多事——比如,open 'r'告诉 Ruby 打开一个文件,但仅仅是为了读取文件,从文件的开始位置读取。”
“还有哪些其他模式呢?”斯卡雷特问。
“嗯,你可以使用open 'w'来写入文件,”皇后说,“使用'w'模式会告诉 Ruby 创建一个你指定名称的新文件,或者完全覆盖任何已存在的同名文件。”
“覆盖!”斯卡雷特说。“你是说它会用你给定的文本替换掉已有文件中的所有内容?”
“没错,”皇后说。
“如果你想添加到现有的文件中呢?”鲁本问。
“为此,你可以使用'a'模式,”皇后说,“这种模式仍然会告诉 Ruby 创建一个你指定名称的新文件(如果文件尚不存在),但如果该文件已存在,Ruby 会从文件末尾开始写入,这样就不会丢失文件中已有的内容。”
“读取、写入和添加,”Scarlet 说。“我想这就是我们想做的所有操作。但如果你使用了一种模式告诉 Ruby 你要做一件事,但又尝试做另一件事,会发生什么呢?”她问。
“我来给你演示!”女王说。她在计算装置上打字:
>> file = File.open('lunch.txt', 'w')
=> #<File:lunch.txt>
>> file.read
IOError: not opened for reading
“一个错误!”Ruben 说。“那我们在打开文件时必须小心使用正确的模式了。”
“正是如此,”女王说。“记住:Ruby 会精确地执行你告诉它的操作。如果你使用'w'模式告诉 Ruby 你只想写入文件,然后试图从文件中读取,Ruby 就会迷惑并产生错误。”
“如果你既想读文件又想写文件呢?”国王问,他正在忙着检查粘在胡子上的一团粉红色的绒毛。
“那么我们需要传递一个稍微不同的模式给File.open,”女王说道。她转向 Rusty,“今天食堂有什么特色菜?”她问。
“烤奶酪三明治!”Rusty 说。女王点点头,在计算装置上打字:
>> file = File.open('lunch.txt', 'w+')
=> #<File:lunch.txt>
>> file.puts('THE MELTIEST OF GRILLED CHEESES')
=> nil
“哇,那是什么?”Ruben 说。“我不知道你可以用puts来写入文件!”
“是的,你可以这么做,”女王说。“puts和write的唯一区别是,puts会在你输入的文本后加上一行空白行,Ruby 通过\n表示这一空白行(记住,这代表‘换行’)。如果你打开文件,它就会是‘THE MELTIEST OF GRILLED CHEESES’这段文字,下面会有一行空白行!”
“现在,我们试着把午餐文本读回来,”女王说,“但看看第一次我们尝试时发生了什么!”
>> file.read
=> ""
>> file.rewind
=> 0
>> file.read
=> "THE MELTIEST OF GRILLED CHEESES\n"
“哇!”Scarlet 说。“第一次调用file.read时,我们什么也没得到,只有一个空字符串,但在你调用了file.rewind之后,我们就能读取到* lunch.txt *中的内容。rewind是做什么的?”
“就像你按下遥控器上的 REWIND 按钮将电影送回开头一样,Ruby 使用rewind方法将你送回文件的开头。如果你不rewind,然后在写入文件后试图直接读取,你只会得到一个空字符串!”女王回答。
“就像试图在电影已经放完时按下播放按钮!”Ruben 说。
“正是如此,”女王说。
“这都说得通,”Scarlet 说,“但我们用了'w+'模式,这意味着我们覆盖了原来的* lunch.txt *文件!”
“我们做到了,”女王说。“让我们把它放回去!我在操作的时候会教你几个新技巧。”她开始打字:
>> file = File.open('lunch.txt', 'a+')
=> #<File:lunch.txt>
>> file.write('ONE KAT-MAN-BLEU BURGER, PLEASE')
=> 31
>> file.rewind
=> 0
>> file.readlines
=> ["THE MELTIEST OF GRILLED CHEESES\n", "ONE KAT-MAN-BLEU BURGER,
PLEASE"]

“首先,我们使用File.open重新打开* lunch.txt 文件进行写入,使用'a+'模式,”女王解释道。“这告诉 Ruby 我们想把新文本添加到文件的末尾,而不是替换文件中已有的文本。接下来,我们调用file.write并传入我们想要添加到 lunch.txt *末尾的新文本。”
“为什么我们调用file.write时 Ruby 会返回31?”Ruben 问。
“一个很好的问题!”女王说道。“Ruby 正在告诉我们,它成功地将 31 个字符添加到了 lunch.txt 文件的末尾。”
“我明白了,”Ruben 说道。“所以 'a+' 模式一定意味着我们向文件中添加内容——这样我们就不会删除已经存在的内容——而 + 部分意味着我们既可以添加内容 也 可以读取文件!”
“正确!”女王说道。“你还会看到,由于添加文本让我们的位置一直到了文件的末尾,所以我们调用 file.rewind 将位置‘倒带’回文件的开始。这就是为什么 file.rewind 返回 0:我们已经回到了文件的最开始!”
“但那个 readlines 方法是做什么的?”Ruben 问道。“它只是给我们返回一个包含文件中所有行的数组吗?”
“说得对,”女王说道。“因为我使用 puts 添加了第一行,所以 ONE KAT-MAN-BLEU BURGER, PLEASE 被单独添加在了一行上。readlines 方法会读取文件,创建一个数组,每个数组元素就是文件中的一行文本。所以我们这里有一个包含两项的数组。”
“惊人!”国王说道,透过妻子的肩膀往下看。
“不是吗?”她问道。“还有一个 readline 方法,它一次只返回一行。看?”她继续输入:
>> file.rewind
=> 0
>> file.readline
=> "THE MELTIEST OF GRILLED CHEESES\n"
>> file.readline
=> "ONE KAT-MAN-BLEU BURGER, PLEASE"
“我们甚至可以用 readlines 和 each 一次性打印出所有行!”女王说道,打字速度更快了:
>> file.rewind
=> 0
>> file.readlines.each { |line| puts line }
THE MELTIEST OF GRILLED CHEESES
ONE KAT-MAN-BLEU BURGER, PLEASE
=> ["THE MELTIEST OF GRILLED CHEESES\n", "ONE KAT-MAN-BLEU BURGER,
PLEASE"]
“太厉害了!”Ruben 说道。
在处理文件时避免错误
“我想我现在开始理解文件输入输出了。但是,如果我尝试使用一个不存在的文件,会发生什么?”Ruben 问道,他伸手到计算机装置的键盘上,输入了:
>> File.open('imaginary.txt', 'r')
Errno::ENOENT: No such file or directory - imaginary.txt
“出错了!”Scarlet 说道。“这有道理。有没有办法在我们尝试使用文件之前先检查它是否存在 before?”
“好问题!”女王说道。“如果我们不确定一个文件是否存在,可以使用 Ruby 内建的 File.exist? 方法来检查。”她输入了:
>> File.exist? 'lunch.txt'
=> true
>> File.exist? 'imaginary.txt'
=> false
“太棒了,太棒了!”国王拍了拍手说。“有了这些精彩的 Ruby 工具,我毫不怀疑我们能很快抓住这些坏蛋。”
“没错!”女王说道。她转向 Rusty。“Ruby 程序里有代表所有装卸港口的东西吗?”她问道。
Rusty 点了点头。“有一个数组,loading_docks,它是一个文件数组。每个文件代表一个装卸港口门,所以如果你打开并读取所有文件,所有门就应该打开!”
女王思考了一会儿,手指悬停在键盘上方。然后她在计算机装置上输入:
loading_docks.each do |dock|
current_dock = File.open(dock, 'r')
puts current_dock.read
current_dock.close
end
一个接一个的,装卸港口的门缓缓打开,稍微停留了一会儿,然后滑动关上。每个港口的内容描述开始填满计算机装置的屏幕。
“Ruby 代码... Ruby 代码... Key-a-ma-Jiggers 装运... 在那儿!”Rusty 一边喊,一边指向远墙中央的一扇门。
四个身影从靠近墙壁左下角的装卸港口跃出,就在门开始再次滑动关闭时。

“停下!”国王大喊道。“我们包围了你们!”
那四个人的动作出奇地迅速,几乎把几位 Refactory 的工人撞倒,他们正试图找到最近的出口。
“阻止他们!” Rusty 大声喊道,他们五个人正沿着金属走道跑向装卸码头的地面。
几个 Refactory 工人同入侵者进行了搏斗,但他们太快,太灵活了。不到几秒钟,他们就一路跑到了出口!
“让开,让开!”王后喊道,他们五个人刚好在那群身影逃出门的同时,赶到了 Refactory 的出口。国王、王后、Ruby、Scarlet 和 Rusty 没有减速,一头冲过门口,进入了通向他们原路的小走廊。
“他们朝货梯走去了吗?”Ruben 一边跑一边喘气。
“更糟糕!”Rusty 说道。“他们正朝 WEBrick 路奔去!”
国王和王后一起倒吸了一口气。“WEBrick 路!”王后说道。“那条路直通出王国!如果他们从王国大门逃出去,我们就永远抓不住他们了!”
“那我们就得确保这种情况不会发生,”Rusty 说道。他转身并大声喊道:“大家,跟上他们!”说完,Refactory 的每个人都冲向小小的亮绿色出口标志,国王、王后、Scarlet、Ruben 和 Rusty 领头。
所有装卸码头,集合!
我们差点就抓到这些罪犯了!天啊,这种悬念快把我逼疯了。他们到底是谁?国王、王后、Ruben、Scarlet 和 Rusty 能及时抓住他们吗?明天 Refactory 食堂的午餐菜单是什么?这些问题肯定值得永远思考——至少,直到本章结束。与此同时,我们再多练习一点从文件中读取和写入数据吧。
我们从创建一个新的文件 loading_docks.rb 开始,输入以下代码。这是一个简单的小程序,它会为每个装卸码头创建一个文本文件,写入一些文本内容,然后再读取出来。
loading_docks.rb
def create_loading_docks(➌docks=3)
➊ loading_docks = []
➋ (1..docks).each do |number|
➍ file_name = "dock_#{number}.txt"
loading_docks << file_name
➎ file = File.open(file_name, 'w+')
file.write("Loading dock no. #{number}, reporting for duty!")
file.close
end
loading_docks
end
➏ def open_loading_docks(docks)
➐ docks.each do |dock|
file = File.open(dock, 'r')
puts file.read
file.close
end
end
➑ all_docks = create_loading_docks(5)
➒ open_loading_docks(all_docks)
虽然有一些来自前面章节的代码出现,但这里没有什么新鲜的内容需要担心。我们来逐行分析一下代码吧。
首先,我们创建一个名为 loading_docks 的空数组 ➊,用来存储我们将要创建的所有装卸码头文件的名字(这样我们可以稍后读取它们)。接下来,我们使用 (1..docks) 范围来创建和 create_loading_docks 方法要求的装卸码头数量一样多的文件 ➋(如果没有传递数字,默认是 3 ➌)。
对于范围中的每个数字,我们调用一个块,这个块会创建一个包含该数字的文件(比如 dock_1.txt),并将该文件名添加到 loading_docks 数组 ➍ 中。接着,我们打开文件,写入一串文本,再关闭文件 ➎。
最后,在 open_loading_docks 方法 ➏ 中,我们简单地获取包含码头名称的数组(它看起来像 ["dock_1.txt", "dock_2.txt"...],以此类推),对于每个文件名,我们打开文件进行读取,读取其内容,然后关闭它 ➐。所以当我们运行这个脚本时,使用 all_docks = create_loading_docks(5) ➑ 和 open_loading_docks(all_docks) ➒ 在最后,我们最终会创建 dock_1.txt 到 dock_5.txt,每个文件都包含其独立的编号和 "reporting for duty!" 字符串。
相当不错吧?
一如既往,你可以通过在命令行中输入 ruby loading_docks.rb 来运行完成的脚本。运行时,你会看到以下内容:
Loading dock no. 1, reporting for duty!
Loading dock no. 2, reporting for duty!
Loading dock no. 3, reporting for duty!
Loading dock no. 4, reporting for duty!
Loading dock no. 5, reporting for duty!
如果你查看运行了loading_docks.rb的目录,你还会看到每个码头的 .txt 文件,里面包含了我们的脚本输出的文本!
但我相信你现在已经开始思考如何改进这个简单的小脚本了。例如,我们可以将创建文件的数量从 5 个改成 1 个、3 个、10 个,或者任何我们选择的数字!不过要小心——创建太多文件不仅会填满你的文件夹,还可能会导致计算机崩溃。(这就是为什么我们默认创建 3 个文件,并且在示例中只创建了 5 个的原因。)
你可能已经注意到,我们是使用 'w+' 模式来写入文件的,这意味着如果我们再次运行脚本,它将用新内容覆盖文件。那么,如果我们想在文件中添加内容呢?(提示:'a+' 模式可能会涉及到。)
那么,如果我们想写入比普通文本文件更复杂的内容呢?如果我们想写入一个 另一个 Ruby 文件 呢?这不仅是可能的,而且是专业程序员每天都会做的事情。试着写入一个包含少量 Ruby 代码的文件——像 puts 'Written by Ruby!' 这样简单的内容。(确保你将文件以 .rb 结尾,而不是 .txt,这样 Ruby 才能运行它。)
最后,你打算如何使用我们看到的文件方法,比如 exist?、rewind 或 puts 呢?Ruby 文档中的 ruby-doc.org/core-1.9.3/File.html 中是否还有其他可能很酷的文件方法可以使用?记得在上网前向你的本地成年人请教!
你知道这个!
你可以读取!你可以写入!好吧,事实上你已经知道如何做这些事情了,但现在你知道了如何 使用 Ruby 来做这些事。我不怀疑你现在已经是一个完整的 Ruby 大师了,但为了确保你对我们刚才讲解的 Ruby 魔法没有任何疑问,让我们花点时间回顾一下。
你已经看到,Ruby 可以创建、读取、写入并理解 文件,这些文件就像你已经熟悉的计算机文件:文本文件、图片、Ruby 脚本等等。Ruby 可以使用 open 方法打开已经存在的文件:
>> file = File.open('alien_greeting.txt', 'r')
=> #<File:alien_greeting.txt>
它可以使用 read 方法来读取文件:
>> file.read
=> "GREETINGS HUMAN!"
当我们使用完文件时,应该使用 close 方法将其关闭:
>> file.close
=> nil
结果我们发现,如果一次性打开太多文件,我们可能会不小心让电脑崩溃,所以打开的文件最好及时关闭。幸运的是,如果我们使用块打开文件,Ruby 会自动为我们关闭文件:
>> File.open('alien_greeting.txt', 'r') { |file| file.read }
=> "GREETINGS HUMAN!"
Ruby 对于被告知该做什么很挑剔,所以我们必须使用不同的模式来告诉 Ruby 应该使用哪种输入和输出模式。当我们使用'r'时,我们告诉 Ruby 我们只希望它读取文件,当我们使用'w'时,我们告诉它我们只希望它写入文件。若我们希望 Ruby 同时读取和写入文件,可以使用'w+'模式:
>> new_file = File.new('brand_new.txt', 'w+')
=> #<File:brand_new.txt>
>> new_file.write("I'm a brand-new file!")
=> 21
>> new_file.close
=> nil
>> File.open('brand_new.txt', 'r') { |file| file.read }
=> "I'm a brand-new file!"
你发现'w+'会覆盖一个文件——也就是说,它会将现有文件中的所有内容替换成我们告诉 Ruby 写入的字符串。如果我们只想添加内容到文件,而不是完全替换它,我们可以使用'a'模式(如果我们还想从文件中读取,可以使用'a+'模式):
>> file = File.open('breakfast.txt', 'a+')
=> #<File:breakfast.txt>
>> file.write('Chunky ')
=> 7
>> file.write('bacon!')
=> 6
>> file.rewind
=> 0
>> file.read
=> "Chunky bacon!"
说到我们的朋友rewind,你看到我们可以用它将文件指针倒回到文件开头,从而读取整个文件:
>> file = File.open('dinner.txt', 'a+')
=> #<File:dinner.txt>
>> file.write('A festive ham!')
=> 14
>> file.read
=> ""
>> file.rewind
=> 0
>> file.read
=> "A festive ham!"
在第一次调用file.read时,字符串为空,因为我们已经到达文件的末尾。不过,当我们调用rewind时,我们回到了文件的开始位置,再次调用file.read时,文本就会显示出来。
你发现如果我们想在一行文本后添加一个空行,我们可以使用文件的puts方法而不是write。当我们重新读取文件时,Ruby 会把空行显示为一个反斜杠和字母n(\n):
>> file.puts('A sprig of fresh parsley!')
=> nil
>> file.rewind
=> 0
>> file.read
=> "A festive ham!A sprig of fresh parsley!\n"
事实上,你看到我们可以使用readline和readlines方法逐行读取文件。readline一次读取文件的一行,反复调用它就能一行一行地读取:
>> file = File.new('dessert.txt', 'a+')
=> #<File:dessert.txt>
>> file.puts('A gooseberry pie')
=> nil
>> file.puts('A small sack of muffins')
=> nil
>> file.rewind
=> 0
>> file.readline
=> "A gooseberry pie\n"
>> file.readline
=> "A small sack of muffins\n"
如果我们想一次性读取文件中的所有行,可以使用file.readlines结合each方法和一个代码块:
>> file.rewind
=> 0
>> file.readlines.each { |line| puts line }
A gooseberry pie
A small sack of muffins
=> ["A gooseberry pie\n", "A small sack of muffins\n"]
最后,你看到我们可以使用exist?方法来检查一个文件是否存在:
>> File.exist? 'breakfast.txt'
=> true
>> File.exist? 'fancy_snack.txt'
=> false
文件和文件的输入/输出现在对你来说可能没什么大不了的(尤其是因为你已经了解了它们的工作原理),但它们是计算机完成工作的一个重要部分。不要犹豫,去尝试在你的电脑上创建和修改文件,另外——如果得到许可——在互联网上搜索更多关于文件的资料,了解它们如何工作,以及你可以运行的任何有趣的 Ruby 代码来加深理解。但我不再啰嗦了:我们的英雄们正在紧追那些整天在王国里捣乱的骗子们,我们就快要揭开他们的面目,看看他们想要什么,以及国王、王后、鲁本、斯卡利特和重构号的船员们是否能一劳永逸地阻止他们!
第十三章 跟随 WEBrick 路
Ruby 和互联网
国王和王后从炼造厂的出口冲出来,走进明亮的傍晚阳光中,鲁本和斯卡雷特紧随其后。在他们前方,WEBrick 路延展得很远,深红色的砖块在阳光下微微发光。在远处,他们可以看见四个神秘的坏蛋形影不离地撤退,再远一点,是标志着王国边界的高墙。
“他们太快了!”鲁本喘着气,手扶在膝盖上。“我们永远也追不上他们!”
“永远不要说不可能,”拉斯提边说边跑到他们身后。“我们一定可以做点什么。”
王后转向他。“有没有办法关闭 WEBrick 路?”她问。

拉斯提想了想。“我不太确定,”他说,“但我有个主意。”他翻开了工作簿的所有页面,拿出一块带有熟悉屏幕的薄金属片。“这是我的便携计算装置,”他说。“我和我的工人们会继续前进,尽力抓住这些坏蛋。与此同时,如果你们能做点什么来关闭这条路,防止他们逃跑的话,可以用这个小计算机。”
“对!”斯卡雷特说道,接过工头的手持计算机。拉斯提向她眨了眨眼,然后示意炼造厂的男女工人迅速从出口涌出。“这边,大家!我们要尽力阻止这些坏蛋!快,快,快!”
当炼造厂的工人们跑下深红色的道路时,国王、王后、斯卡雷特和鲁本围在工头的便携计算装置旁。
“好,首先要做的是什么?”斯卡雷特说道。“我们怎么才能关闭这条路?”
国王拉了拉他蓬松的白胡子。“在我看来,”他说,“我们应该先检查一下这条路是否正常工作!如果它因为某些原因已经关闭,我们可以和拉斯提的队伍会合,去抓住这些坏蛋。”
斯卡雷特和鲁本互相看了一眼。“那是……其实是个好主意,”鲁本说道。“我们怎么测试这条路是否通畅?”
“嗯,”国王说道,“WEBrick 路就像王国中的一切一样,依赖 Ruby 运行,它是王国与外界之间的主要连接。如果我们能用 Ruby 检查是否能够从王国外部获取信息,那我们就能知道这条路是否正常工作。”
“就是这个!”鲁本说道。“如果我们能用 Ruby 连接互联网,那我们就知道这条路是通畅的!”
“但我们怎么做呢?”斯卡雷特说道。“我觉得我现在对 Ruby 已经挺熟悉了,可是我根本不知道从哪里开始连接互联网。”
“我想我知道怎么做了,”王后说道。“你看,连接到互联网就像是连接到一个文件——你只需要告诉 Ruby 该怎么做就行!可以吗?”她问。斯卡雷特点点头,将便携式计算装置递给了王后。王后开始打字。
“记得我们如何将代码写在不同的 Ruby 文件中,然后用require将一个脚本的代码引入到另一个文件吗?”女王问。Scarlet 和 Ruben 点了点头。“嗯,”女王继续说,“其实 Ruby 自带了一些小文件包,我们也可以用require来加载!”
“真的有?”国王问,难以置信。“当然!”女王说。“你可以把这些文件包看作是我们可以在自己项目中使用的代码小库,它们被称为gems。”
“没错!我好像听说过 Ruby gems,”Ruben 说。“有没有用于连接互联网的 gem?”
使用 open-uri Ruby Gem
女王点点头。“open-uri gem,”她说,“它让你的 Ruby 代码能够像打开文件一样打开互联网网站!一旦我们在 IRB 会话中require了它,我们就能做很多很棒的事情。”她在计算装置上输入:
>> require 'open-uri'
=> true
她把小机器递给了 Scarlet。“已经加载好,准备就绪!”她说。
Scarlet 看着屏幕。“难道我不需要'./open-uri',而不是仅仅'open-uri'?”她问。
“Gem 不需要!”女王说。“这是你自己创建的文件才需要,但如果你require一个 gem,你只需要像字符串一样输入它的名称。Ruby 为你做的一件事就是追踪 gem 在你电脑上的安装位置。既然它已经知道去哪里找 gem,你就不需要用./去查找当前目录了。只需要输入 gem 的名字,Ruby 就会做剩下的事。”
“完美!”Scarlet 说。“现在我们只需要一个网站来测试 WEBrick 通道是否正常工作。”
“啊哈!”国王说,举起了一根手指。“我们可以用我最喜欢的网站来测试它!”
“那是什么?”Scarlet 问,准备在键盘上敲字。
“Example.com!”国王说,满脸笑容。女王翻了个白眼。
“这也不赖,”Scarlet 承认,并开始在那个小小的计算装置上输入。
注意
为了让这段代码正常工作,你需要连接到互联网!如果需要帮助连接互联网,可以找个成年人帮忙。
>> site = open('http://www.example.com')
=> #<StringIO:0x000001032de2f0>
“首先,”Scarlet 说,“我们用open方法告诉 Ruby 根据我们输入的 URL 网站来创建一个对象。然后,我们就可以像操作普通文本文件那样使用.read方法,这样就能获得* www.example.com *网站的内容!”
>> site.read
=> "<!doctype html>...
屏幕很快显示出了来自example.com网站的代码。
“它能用了!”Scarlet 说。“WEBrick 通道必须打开。现在我们只需要找到方法来关闭它!”
Ruben 想了想。“嘿,等一下,”他说,“王国是通过 WEBrick 通道来请求并发送信息的吗?”
“确实能行,”国王说。
“那是不是意味着有某种Web 服务器在运行?”Ruben 问。
女王打了个响指。“鲁本,你真是个天才!”她说道。鲁本微微脸红。女王转向斯嘉丽。“不仅有一个网页服务器在运行,而且它是一个WEBrick 网页服务器。”她开始快速讲解并兴奋地做着手势。“WEBrick 服务器是 Ruby 的一段特殊代码,用来将信息从王国发送出去。你看,当你访问一个网站时,你实际上是在向一个网页服务器——也就是某个在互联网上的计算机——请求信息。好吧,当互联网上的人们想要了解我们的王国时,我们自己的 WEBrick 网页服务器就会发送出去!”
注释
WEBrick 服务器不仅仅存在于王国里——这就是你在自己计算机上使用的 Ruby 网页服务器的真正名称!
“如果那个网页服务器被关闭……”鲁本开始说道。“……那么王国里的任何东西都无法外出!”斯嘉丽补充道。
“现在我们只需要搞清楚如何找到服务器来关闭它,”鲁本说道。
“嗯,”国王踢了踢一块灰尘,“其实我可能能帮上忙。你看,我丢东西太频繁了,已经非常熟悉计算机装置的搜索功能了。”
“太完美了!”斯嘉丽说道。“我们认为那个文件应该叫什么?”
调查王国的网页服务器
“嗯,你可能想搜索一下WEBrick或者server,”国王说道。“无论如何,我会这么做。”
斯嘉丽点点头,按了几个键,开始搜索文件。她眯起眼睛,摇了摇头,打了几个字,叹了口气,思考了下,又继续打字。
“我想我明白了!”她终于说道。“我找到一个叫server.rb的文件。”
“打开它,打开它!”鲁本站在脚尖上,努力更清楚地看到屏幕。
斯嘉丽打开了server.rb文件,这就是他们看到的内容:
require 'webrick'
include WEBrick
server = HTTPServer.new(
:Port => 3000,
:DocumentRoot => Dir.pwd
)
trap('INT') { server.shutdown }
server.start
“让我们看看,”斯嘉丽说道。“看起来前两行require了webrick gem,并引入了WEBrick模块。”
“这看起来对我来说没问题,”鲁本说道。“那么接下来的几行代码似乎创建了一个新的 WEBrick 服务器。我不确定port是做什么的,但它被设置为数字3000。Dir.pwd那部分是什么意思?”
“我想我明白了,”女王说道。“就像 Ruby 使用File类来处理文件的方法一样,它使用Dir类来处理目录的方法,目录其实就是文件夹的意思。”女王指着屏幕说道。“pwd方法返回当前工作目录,所以Dir.pwd其实就是 Ruby 表示‘就在这个文件夹’的方式。网页服务器正在运行,并且正在从这个文件夹中发送信息!”
“好的!”斯嘉丽说道。“我们只需要关掉它,我觉得我找到了线索。那行trap('INT') { server.shutdown }——那是做什么的?”
女王研究了屏幕一会儿。“如果我没记错的话,那是 Ruby 的方式来表示当它收到一个中断信号时,它将关闭服务器!”
“中断信号?”鲁本问道。“那是不是意味着我们必须告诉服务器停止运行?”
“没错,”女王说道。“但是,怎么做呢?”
“同时按住 CTRL 键和 C 键!”国王突然说道。所有人都转过头看向他。“就这么做!”他催促道,挥舞着双手。“我们不能浪费一秒钟!”斯嘉丽点点头,立即按下了键盘,接着她看到的情景是:
INFO going to shutdown ...
INFO WEBrick::HTTPServer#start done.
“我们成功了!我们成功了!”斯嘉丽和鲁本跳了起来,互相拥抱。
王后转向国王,微笑着问:“你怎么知道怎么做的?”她问道。
国王尴尬地笑了笑。“嗯,我经常弄坏我的计算机设备,所以我学会了使用 CTRL-C 来停止程序!”大家都笑了起来。
“现在服务器已经关闭,”斯嘉丽说,“没有任何人和物能够离开这个王国!快,我们得赶紧跑到 WEBrick 通道去看看,看看我们是不是及时阻止了那些坏人!”
他们四个人匆匆沿着鲜红的道路跑去,心里希望自己及时关闭了 WEBrick 通道。从远处看,他们看到一大群 Refactory 工人在四处走动。随着他们靠近,他们开始辨认出个别工人,再到人脸。不久,他们已经接近到足以看清楚,看到 Rusty 正疯狂地挥手示意他们过来,而就在他旁边,四个戴着兜帽的恶棍正站在人群中央!
“太棒了!美味的早餐肉汁!辉煌的玉米松饼!我们抓住他们了!”国王几乎激动得泪流满面。
当他们跑到人群前面时,工头转向他们,满脸笑容。“陛下!殿下!”他依次向国王和王后致意。“我不知道你们是怎么做到的,但你们关停了 WEBrick 通道,给我们足够的时间抓住了这些恶棍!他们当时正试图拉扯王国墙上的大门,想要逃走,结果我们把他们困在了这里。”他交叉双臂,笑了笑,浓密的胡须在脸上微微晃动。“我很高兴正式把他们交给你们。”
“谢谢你,Rusty,”王后说道。国王和王后走到人群中央,两个 Refactory 工人各自抓住四个神秘人物中的一个。鲁本和斯嘉丽紧随其后。
“你们这些混蛋在我们的王国里制造了彻底的混乱!”国王说道。
“而且它们剥夺了王国所有市民的紫色熊猫游行,”王后皱着眉头说道。
“是的,正是如此,”国王说道。“而且我们受够了!现在是你们揭开你们真实身份的时候了。”国王向 Refactory 工人们点了点头。每个工人都把手放在每个恶作剧者的兜帽两侧。
“按照我的数点。一...二...二又半...三!”国王喊道,工人们一把拉开了恶棍的兜帽。人群惊呼一声。站在他们面前,吐着舌头,发出嘶嘶声的,是四条巨大的蛇!

“真是字面意义上的蛇!”Rusty 说道。“怎么样?”
“实际上是 Pythonssss,”那条离 Rusty 最近的蛇嘶嘶地说道。
“甜美的嗨比尔迪-吉比尔迪!一条会说话的蛇!”国王说,躲在一名特别强壮的 Refactory 工人后面。
“‘名字是 Terry,’”那条蛇说道。“‘实际上是 Terry One。’”她用头指了指旁边的蛇。“‘那是 John,’”她说道。“‘然后是 Terry Two,再是 Graham。’”
“‘很高兴见到你,’”约翰说道。
“‘嗯,我们当然不高兴见到你们,’”国王恢复了勇气说道。“‘你们到底在做什么,制造这么大的麻烦?’”他开始用手指算。“‘偷走我的字符串!堵塞神秘管道!搞乱循环!摧毁哈希计算装置!黑入女王的机器!剥夺熊猫的紫色!这份清单一长串!’”
Graham 不安地环顾四周。“‘你的字符串?我们没偷你的字符串!’”他说道。
国王举起了双手。“好吧,算了,也许那是我做的,”他说,“但剩下的都是你们干的。我要求一个解释!”
Terry Two 低下了头;Ruben 和 Scarlet 分不清她是生气还是难过。“因为大家都只顾着 Ruby,Ruby,Ruby!”她说道。“没人愿意再使用 Python 了。”
“‘Python?’Ruben 问道。”
“‘你看到了吗?’”Terry Two 说道,朝 Ruben 点了点头。“这孩子连 Python 都没听说过!”
“‘它是什么?’Ruben 问道,向前探了探身子。”
Terry Two 叹了口气。“一种编程语言,非常像 Ruby,”她说道。
“‘但更好,’约翰插嘴说道。”
“‘哦,完全更好,’Terry One 插话道。”
“‘但王国里没人用它,’”Terry Two 继续说道。
“‘我们认为如果能让人觉得 Ruby 有什么问题,他们可能会转换过来,’”他说。
Scarlet 愤怒地走近了这些蛇。“你们应该展示 Python 有多好,而不是让人觉得 Ruby 有什么问题!”她斥责道。
约翰悲伤地摇了摇头。“没有人愿意听,”他说道,“所以我们认为最好的机会就是吸引注意,即使它必须是负面的。”没有人能确定,但看起来那条巨大的蛇眼里似乎含着泪水。
国王的表情稍微缓和了一些。“你们都知道 Python 吗?”他问道。
蛇们互相看了看,显然很困惑。它们缓慢地点了点头。
“‘告诉我一下,’”国王说道。
“‘它是一个非常棒的语言,’Graham 说道,‘它有字符串、数字和布尔值。’”
“‘数组,也有!’”Terry One 补充道。
“‘对象、方法和类,’约翰说道,‘你可以编写任何你喜欢的程序。’”
国王点了点头,绕着小圈走。“看起来,Ruby 和 Python 其实没有那么大区别,’”他说道。
蛇们沉默了一会儿。“或许……不是,”约翰终于说道,“但如果是那样,为什么不使用 Python 而不是 Ruby 呢?”
“‘我想我真正的问题是,’”国王说道,“‘为什么非得选择其中一个呢?为什么不写你更喜欢的那个呢?’”Terry One 张开嘴准备发言,但国王继续说道。“‘其实,如果我告诉你,可以写出变成 Python 的 Ruby,你们会怎么想?””
一时间寂静无声。Ruben、Scarlet 和女王互相交换了一些惊讶的目光。
“你看,”国王说道,“我一直不是个很好的程序员。编程对我来说从来都不容易。所以我花了很多时间练习,阅读文章和示例代码,试图变得更好。”他说着,从长袍里拿出了一卷小卷轴。“在我的研究过程中,我发现了一段非常神奇的代码,它能将 Ruby 代码转化为 Python 可以理解的指令。你可以写 Ruby,Python 程序就会出来!这不是很棒吗?”他激动地说道。“你可以用任何语言,任何方式,去编写代码,并且你仍然可以对计算机讲故事,让它做你想做的一切。这就是编程的魅力!”
蟒蛇们沉默了一会儿。最终,泰瑞·一开口了。“我……以前从没这么想过,”她说。
“我想我想说的是,”国王说道,“在这个王国里,所有的好奇心,所有的诚挚学习欲望,所有的分享和教学,永远是受欢迎的。是的,我们确实用 Ruby 来处理我们的日常事务。是的,这是我们许多公民所知道、喜欢,甚至思考的语言。但这并不意味着它是唯一的方式,也绝不意味着我们认为从 Python 中学不到任何东西!”
拉斯蒂把手捧在嘴边。“好极了!”他说,全场爆发出掌声。
“你们怎么说?”国王温和地问,走向四条蟒蛇。“你们愿意帮我们了解一些 Python 吗?我们可以教你们一些 Ruby。”
蟒蛇们互相交换了眼神,然后开始点头。
“我们真的非常抱歉,”格雷厄姆说。“我们只是感到伤心和沮丧,不知道该如何表达我们的感受。”
“我们希望没弄坏什么东西,”泰瑞·二说道。
“我们会帮忙修复任何我们弄坏的东西,”泰瑞·一补充道。
“我尝过汉克的哈希,”约翰说。“那是我吃过的最棒的食物。如果我能再去那里吃一顿,我愿意修复一百个 Ruby 程序!”
“那就这么决定了!”国王说道。他看向王后,王后微笑着点了点头。他又把目光转向四条蟒蛇。“凭借我作为这个王国众多公民赋予我的权力,现正式赦免你们所有的过错!”他稍微向前倾了倾身。“如果你们想回宫殿来点蛋糕和茶水,那也可以。”
蟒蛇们兴奋地点点头,欣喜若狂。“那太棒了,”泰瑞·一说道。“非常感谢!”
国王举起双臂。“大家回到皇家宫殿!”他大喊。“大家都有蛋糕和茶!”
人群中爆发出一阵巨大的欢呼声。那些一直控制着蟒蛇的重生厂工人将它们放开,大家将国王、王后、斯嘉丽和鲁本抬到肩膀上。
“你真是救了大忙,陛下!”斯嘉丽对国王说道。他挥了挥手,示意不必在意这份夸奖。
“你们这些孩子救了大忙!”他说。“要不是你们,我们根本无法解决这个谜题,恢复王国的和平与繁荣。”
“假设我们都分了一块蛋糕,”女王说。“说到这个!我们来给这个蛋糕和茶的聚会加点儿派。”
“好啊!”国王说,他在过去的几秒钟里一直在拍打他长袍的口袋。“哦,萝卜,”他说。“我把我的线放哪儿了?”

超越王国的边界
我的天啊!蛇!我根本没料到会看到这个,距离还差 42 英里。我还有很多问题!它们是怎么潜入王国的?它们知道哪些酷炫的 Python 技巧,像我这样的 Ruby 程序员能学到哪些?它们是怎么不借助手就能操作 Key-a-ma-Jigger 的?
在我思考这些和其他伟大的谜题时,您可以继续练习使用 WEBrick 网络服务器。您可以创建一个名为 web_server.rb 的新文件,并输入以下代码。
web_server.rb
require 'webrick'
include WEBrick
server = HTTPServer.new(Port: 3000)
server.mount_proc '/' do |request, response|
response.body = 'Your Ruby adventure is just beginning!'
end
trap('INT') { server.shutdown }
server.start
这段代码与您之前看到的版本略有不同,但这里没有任何您没见过的内容!唯一需要注意的部分是内置于 WEBrick 中的mount_proc方法。这个方法告诉服务器如何响应某些请求;在本例中,如果您访问电脑上的/ URL,您应该会看到分配给response.body的消息。您电脑的内建网站是http://localhost/。
和往常一样,您可以通过在终端中输入ruby web_server.rb来运行完成的脚本。启动脚本后,您应该会看到一些数字和文本,如下所示:
INFO WEBrick 1.3.1
INFO WEBrick::HTTPServer#start: pid=78115 port=3000
(您的数字可能会略有不同,但文字应该差不多。)当您看到文本出现时,您的网络服务器已经启动并运行!打开您最喜欢的网页浏览器(例如 Chrome、Firefox、Internet Explorer 或 Safari),然后访问http://localhost:3000/。如果一切正常,您应该会在浏览器窗口中看到Your Ruby adventure is just beginning!。疯狂吧?您的第一个网站刚刚诞生!(我打算给它起名叫 Marigold。)当您用完服务器后,在运行 WEBrick 的终端中按住 CTRL-C 来关闭它。
这个网络服务器非常简单,如果我了解你们,你们肯定已经在考虑如何改进它了。那么,别客气!尽管去修改web_server.rb文件中的代码。(每次修改后,您需要使用 CTRL-C 来关闭服务器并重启,以便查看修改效果。)例如,您可以从仅仅修改response.body字符串开始,然后尝试调整端口号或者添加更多的mount_proc。
这里有个提示:如果您将以下代码添加到您的web_server.rb文件中,然后访问http://localhost:3000/favorite_vegetable,会发生什么呢?
server.mount_proc '/favorite_vegetable' do |request, response|
response.body = 'Certainly not yams!'
end
如果你想查看世界各地的人们所创建的宝贵资源,可以访问 RubyGems 网站,网址是 rubygems.org/。几乎任何任务,都可能有人为此创建了 gem,所以你应该时常光顾 RubyGems。花点时间浏览网站上的信息,你很快就能下载并使用别人创建的 gem!
说到分享代码,那个能够将 Ruby 转换成 Python 的神奇小程序真的是存在的!它是由一位名叫 why the lucky stiff 的程序员编写的,可以在 GitHub 网站找到,那里人们与全世界的人分享他们写的代码。你可以在 github.com/ 找到 GitHub 网站,在 github.com/whymirror/unholy/ 找到 Ruby 转 Python 的项目——叫做 unholy。 (这段代码非常先进,但如果你坚持下去,我相信你会逐渐理解的。我还没遇到过比你更聪明的人!)
你已经知道这些啦!
本章我们把你已经知道的部分代码和概念带到了一个新的层次,所以让我们稍微停一下,确保你能完全理解。
你学到我们可以通过使用 open-uri gem 来获取互联网上网站的信息:
>> require 'open-uri'
=> true
>> site = open('http://www.example.com')
=> #<StringIO:0x000001032de2f0>
>> site.read
=> "<!doctype html>...
一个 gem 只是别人创建的一组文件,目的是让你编写 Ruby 程序变得更轻松。我们可以像 require 自己写的文件一样,在程序中 require gem,只是我们需要在文件名前加上 ./,而对于 gem,我们只需要在 require 方法调用后以字符串的形式写出 gem 的名字。
随时可以尝试这段代码与其他网站进行交互!只要确保事先获得许可,并小心——有些网站返回的代码可能非常多,可能会填满你的终端窗口。
你还看到我们不仅仅是 请求 信息;我们可以通过像 WEBrick 这样的 web 服务器,将信息 提供 给访问者:
require 'webrick'
include WEBrick
server = HTTPServer.new(:Port => 3000)
server.mount_proc '/' do |request, response|
response.body = 'WEBrick is online and running fine!'
end
trap('INT') { server.shutdown }
server.start
你学到我们可以修改 web 服务器代码来写简单的消息,看到代码中的变化,通过停止并重新启动服务器,且使用 CTRL-C 可以停止服务器(当我们把它运行起来后),只需输入 ruby server_file_name.rb 或在 IRB 中调用 load 'server_file_name.rb'。
最后,我们稍微谈了一下如何通过访问 RubyGems 网站 (rubygems.org/),使用其他人编写的 gem,以及如何通过 GitHub 网站 (github.com/),与世界各地的人们分享和阅读代码。如果你想在这些网站上创建账户,可以找个成年人大人帮忙哦!
我们的故事也许结束了,亲爱的读者,但这并不意味着我已经完全停下了喋喋不休的脚步。在过去的几百页中,我们涵盖了大量的 Ruby 魔法,如果我只是说一句“好了,再见!”然后就草草了事,那我就会是个糟糕的教师、作家和程序员了。深呼吸一下,翻开下一页,让我们再花几句话回顾一下我们学到的所有疯狂、令人惊叹、精彩的内容。
第十四章。接下来该做什么
全局视角:你知道的
哇,这真是个故事。多么扣人心弦!多么悬疑!真是太令人震撼了,我在快结束时几乎都没怎么说话。你大概注意到了吧。我经常进进出出。
看起来我们好像没有走得太远,但如果你回想一下当初刚开始读这本书的时候,你对 Ruby 完全一无所知。你可能从未听说过“皇家管道工的高级学徒”、“哈舍里”或“达格龙”,更不用说字符串、对象 ID 或方法了。现在,你已经了解了这些内容,甚至更多!
我们已经学了这么多,我觉得有必要做一个最后的快速回顾。为了我自己。只是为了整理一下我这个凌乱的大脑。别担心—这会很快;如果你需要更深入的复习,可以翻回前面的章节,重新阅读你知道这些!部分(因为你完全知道,尽管你不总是记得每一个细节)。

我们从学习如何安装和设置 Ruby 开始。可不是一件小事!我们成功地将 Ruby 安装并运行在我们的电脑上,学会了如何通过 IRB 运行 Ruby 代码片段,并且发现了如何编写文件,叫做脚本,让我们能够将一堆代码行收集在一起,一次性运行它们。
一旦我们弄清楚了如何运行代码,接下来的步骤就是编写一些有趣的东西来运行。还记得当你只会打印字符串和加数字的时候吗?那时候编程的乐趣就是在puts中输出短语,或者把东西相乘?那时候的日子真好!但一旦你开始学会编写可以在现实中发生的故事——也就是程序——你就会迫不及待地想写出更大更好的程序。所以我们继续学习了控制流程(使用if、elsif、else和unless)和布尔值(true和false),没多久,我们就能控制信息在程序中的流动,修复神秘的管道,并引导哈尔多穿过地下迷宫。
然后我们真的是进入正题了!我们开始讨论 Ruby 循环和迭代器,使用 each 等方法打印数组中的所有值(数组就像一个物品列表:[1, 2, 3]),更新哈希(哈希就像一个包含项和值的简短字典:{ name: Lou, fancy: true }),并帮助汉克和吱吱吉姆修复哈舍里计算机。我们学习了范围,它只是一系列数字或字母(例如 (0..5) 或 ('a'..'z')),以及符号,它们是我们在 Ruby 中使用的名字或标签。例如,我们看到它们作为哈希的键被使用,像 { hamburgers: 'delicious' }。
就在这时,事情开始变得有趣了!Scarlet 在 Hashery 楼层发现了一个 Python 的音阶,带领我们前往 Carmine Pines。在那里,我们遇见了 Off-White Knight,他向我们展示了如何使用def关键字创建我们自己的方法。我们还看到了如何进行各种高级方法操作,比如设置默认参数、使用 splat 参数,并通过yield关键字编写可以接收代码块的方法。
我们继续去见了 Dagron,他解释了 Ruby 中的对象和类(类实际上就是创建其他对象的对象)。我们学习了程序中变量的作用域,包括全局变量、类变量、实例变量和局部变量。我们甚至了解了一个特殊的 Ruby 值——self,它指代当前对象!当我们深入学习 Ruby 类的语法时,我们了解了attr_reader、attr_writer和attr_accessor,这些是我们可以使用的快捷方式,让我们无需每次都编写方法来获取和设置实例变量,便可以从类外部更新它们。
我们还发现(在女王的帮助下)一些类可以继承其他类——也就是说,它们可以获得其他类的一些属性和能力——并且类允许我们通过继承来重用代码。我们看到,从其他类继承的类可以重写父类的方法——例如,一个继承自Dog类的GuardDog可以有自己的bark方法版本,执行与Dog的bark方法不同的操作——而且我们始终可以“向上”调用父类的方法,使用super关键字。
女王还教会了我们关于模块的知识,模块就像 Ruby 类一样,只不过我们不会创建它们的实例!它们用于通过include或extend来混入行为,这让我们能够从多个来源复用代码,同时仍然只使用一个父类。模块还用于命名空间,或者组织我们的代码,使得我们不必将所有代码都塞进一个文件里。
通过使用模块,我们可以轻松地控制在 Ruby 程序中所有变量、常量和方法的可用范围,并且可以创建具有一个父类的类,继承来自多个来源的行为。
然后我们来到了 Refactory,学习了如何重写代码,使其更加清晰,而不改变其行为。我们看到了一些提升 Ruby 代码的技巧,包括将大方法拆分成小方法,并去除重复代码,使每个方法只做一件事且做得很好。我们还在 Refactory 学习了文件的输入/输出,包括如何用 Ruby 打开、读取和写入文件。
最后,我们讨论了 Ruby 和互联网,学习了 Ruby gems、像 WEBrick 这样的 web 服务器,以及像 RubyGems 网站和 GitHub 这样的有用网站。我们看到了国王、王后、Scarlet 和 Ruben 如何运用他们的 Ruby 知识抓住 Python,并最终向它们展示 Ruby 和 Python 都是优秀的编程语言,而你通过 Ruby 知识走到了这里,结束了本书,充满了智慧。我为此感到无比自豪!这就是诚实的真相。
额外资源与进一步阅读
虽然这些页面中包含了大量 Ruby 魔法,但我们并没有涵盖 Ruby 所有的知识。有大量惊人的书籍和网站可以帮助你进一步学习 Ruby 和编程,我在这里列出了一些!不要觉得必须阅读所有(或任何)这些书籍或网站——它们只是你在完成本书后,继续学习 Ruby 的新途径。
初学者书籍
这些书非常适合初学者。
《Ruby 入门(第二版)》 作者:Peter Cooper(Apress,2009 年)。这本书听起来就是这个意思!一本出色的 Ruby 入门书籍。
《Ruby 编程(第四版)》 作者:Dave Thomas、Andy Hunt 和 Chad Fowler(The Pragmatic Bookshelf,2013 年)。这本书有时被称为“鹤嘴锄书”,因为封面上有一张巨大的鹤嘴锄图像。许多 Ruby 程序员说这是他们的 Ruby 必备书籍,我可以保证,在这里你不会有任何一个 Ruby 问题没有答案。
《Ruby 编程语言》 作者:David Flanagan 和 Yukihiro Matsumoto(O’Reilly Media,2008 年)。Yukihiro “Matz” Matsumoto 是 Ruby 语言的创始人,所以他对 Ruby 能做什么以及它的伟大之处有着许多宝贵的见解。这是另一本非常棒的 Ruby 入门书籍。
Why’s (Poignant) Guide to Ruby 作者:why the lucky stiff(en.wikipedia.org/wiki/Why’s_(poignant)_Guide_to_Ruby)。这是我第一次学习 Ruby 时阅读的书,如果你仔细阅读,你会发现我偷偷把 why 的一些技巧和笑话融入到了这本书中。why 的指南是一本充满魔力、疯狂、图文并茂的 Ruby 入门书。当你读完这本书后,拿起那本!我保证你会很高兴你这么做了。
中级书籍
这些书籍有些内容更为深入。
《Ruby 设计模式》 作者:Russ Olsen(Addison-Wesley Professional,2007 年)。本书讲解了编写 Ruby 代码的优秀设计模式。你将从专业人士那里学到技巧!
《优雅的 Ruby》 作者:Russ Olsen(Addison-Wesley Professional,2011 年)。想学习像本地 Ruby 程序员那样编写代码吗?阅读这本书!
《扎实的 Ruby 程序员》 作者:David Black(Manning Publications,2009 年)。想象一本像这样书的书,少一些魔法生物,多一些关于高级 Ruby 话题的内容,比如线程和错误处理。想象好了吗?恭喜!你已经想象到了 《扎实的 Ruby 程序员》。(想象一下吧!)
酷炫的 Ruby 脚本 由 Steve Pugh 编写(No Starch Press,2008 年)。如果你在寻找一本有趣的食谱书,一本包含从游戏到 Web 服务器的各种 Ruby 脚本的真正大餐,那就把这本书从最近的书店架子上拿走(或者请你的父母从网上订购)。
高级书籍
这些书籍是所有书中最为复杂的!
元编程 Ruby 2 由 Paolo Perrotta 编写(The Pragmatic Bookshelf,2014 年)。这本书探索了 Ruby 如何读取并在运行时改变其自身代码的深奥秘密!不适合心脏脆弱的人。
实用的面向对象设计在 Ruby 中 由 Sandi Metz 编写(Addison-Wesley Professional,2012 年)。如果你想像专家一样编写 Ruby 代码,这本书无疑是最佳选择。完成后,你的对象将闪闪发光,类将熠熠生辉!
Ruby 在显微镜下 由 Pat Shaughnessy 编写(No Starch Press,2013 年)。这本书直击 Ruby 的核心,探讨了所有那些零与一、比特与字节是如何转化为 Ruby 代码的。如果你读完这本书并完全理解其中的每个细节,请打电话给我并向我解释一下。
在线与多媒体
Railscasts
(railscasts.com/)
Ruby on Rails 是一个流行的框架,或者说是 Ruby 程序员用来创建 Web 应用程序的一套工具。Ryan Bates 的这些录屏演示展示了 Ruby 代码如何驱动 Rails 应用程序,而 Ryan 的讲解使得你在家也能轻松跟随学习。与 Ruby Tapas 类似,并非所有这些录屏都免费,所以如果你想观看全部内容,你可能需要得到身边成年人的帮助来注册。
Ruby5 播客
(ruby5.envylabs.com/)
这是更多关于 Ruby 的新闻,但以播客形式呈现!如果你更喜欢听而不是读,这个适合你。
Ruby Rogues
(rubyrogues.com/)
这是另一个播客,尽管我认为它更像是在听博客文章,而不像听新闻。如果你有兴趣了解更多关于 Ruby 的方方面面、其宝石和工具,听一听 Ruby Rogues 吧。
Ruby Tapas
(www.rubytapas.com/)
对于那些喜欢看视频而非阅读或听的朋友,Avdi Grimm 的这些录屏短片为你介绍了 Ruby 的不同部分,这些部分你可能还不了解。不过,只有少部分录屏是免费的,因此如果你想看完整的内容,你需要得到身边成年人的帮助。
Ruby Weekly
(rubyweekly.com/)
由同样编写了 Ruby 入门 的 Peter Cooper 精心策划的每周一次电子邮件,里面充满了有用的 Ruby 文章、教程和视频。这些内容相当进阶,但在你开始写 Ruby 一段时间后,这将是你寻找 Ruby 新闻的第一个地方。
互动资源
Codecademy
(www.codecademy.com/tracks/ruby/)
如果你想在自己舒适的网页浏览器中练习运行 Ruby,可以去 Codecademy 完成 Ruby 课程。有个有趣的事实:这些课程都是我写的,所以应该会感觉很熟悉!唯一的缺点是,它们涵盖了你在这里已经学过的很多内容,因此可能会显得有点过于熟悉。不过,如果你想练习编写代码并复习你学过的内容,可以去那里开始敲代码——而且它是免费的!(你必须年满 13 岁才能创建账户。)
Code School
(www.codeschool.com/paths/ruby/)
Code School 是另一个学习 Ruby 的好网站。像 Codecademy 一样,它要求你年满 13 岁才能创建账户,不同的是,它并不是免费的。不过它有很多优秀的视频,如果你通过观看别人操作来学习最有效,那这个网站可能很适合你。
Ruby Koans
(rubykoans.com/)
想象一下,如果你将本书中的所有知识分成一亿个小而智慧的格言,这些格言的形式是 Ruby 程序。那些就是 Ruby koans!如果你需要更多的练习,并且喜欢通过实践来学习,赶紧下载这些 koans 开始编程吧。它们还非常擅长教你如何进行测试,因为每个 koan 都像是一个失败的测试,你需要修复它才能通过;你修复的代码行越多,你的 Ruby 启蒙就越深。
Ruby Monk
(rubymonk.com/)
这有点像 Codecademy 的课程和 Ruby Koans 的结合。你完成的练习越多,掌握 Ruby 的进度就越远!
额外主题
我们讨论了大量的 Ruby 知识,但也有一些语言特性我们没来得及涉及——主要是因为它们不像我们讨论的内容那样有趣,而且这些话题相对有点复杂。不过,如果你感兴趣并想了解更多,我在这里列出了一个简短的清单。
Enumerable 模块
你可能在我们的学习过程中好奇,为什么数组和哈希都知道如何使用 each 方法。这是因为它们都包含了 Enumerable 模块,你也可以在自己的类中直接使用这个模块来简化代码!它包含了很多实用的方法,比如 all?、any?、include? 和 find。你可以在官方文档中阅读所有相关内容:ruby-doc.org/core-2.0.0/Enumerable.html。
正则表达式
正则表达式就像 Ruby 内部的一个迷你语言,它让你能够匹配单词或短语中的模式。例如,你可以用它们查找只包含大写字母的字符串,或者检查一个字符串是否是有效的电子邮件地址。正则表达式在许多编程语言中都有,但你可以在 www.regular-expressions.info/ruby.html 找到一些 Ruby 特定的信息。你还可以使用一个名为 Rubular 的免费工具,访问 rubular.com/。Rubular 让你可以实时测试你的正则表达式,这样你可以看到你的模式匹配了什么,没匹配到什么。
Procs 和 Lambdas
这些并不完全是块,也不完全是方法——它们介于两者之间!你可以将它们看作是没有名称的方法,或者看作是可以反复执行的“保存”块。你可以在 Ruby Monk 练习中了解更多关于它们的信息(请参见 交互式资源)。
信息隐藏
你可以通过使用 private 和 protected 方法,使你的 Ruby 类中的信息更安全。这在你作为团队的一部分编写 Ruby 代码时非常有用,尽管它们不能完全阻止其他程序员使用不应该使用的方法,但它们可以帮助你的队友理解哪些方法是可以依赖的,哪些方法仍在“开发中”。你可以在 Ruby 文档中阅读更多关于 public 和 protected 方法的内容: www.ruby-doc.org/core-2.0.0/Module.html#method-i-private。
处理异常
每当我们看到 Ruby 代码抛出错误时,通常我会解释为什么会发生错误,然后我们继续前进,但如果仔细想想,这并不总是最好的处理方式。有时候,当发生错误(也叫做异常)时,我们希望做些什么来处理它,比如设置一个默认值或打印一条消息到屏幕上。Ruby 中的异常处理称为——你猜对了——异常处理,如果你想了解更多关于它的信息,可以阅读 Avdi Grimm 的《Exceptional Ruby》 (exceptionalruby.com/).
反射/元编程
记得我们之前讨论文件输入输出时,我们看到可以编写 Ruby 代码来写 Ruby 代码吗?这意味着 Ruby 有能力查看并修改自己的代码!这种内省的部分称为反射,而 Ruby 改变自己编程能力的过程称为元编程。这是一种非常难写的 Ruby 代码,但如果你有兴趣,你可以通过 Paolo Perrotta 的《Metaprogramming Ruby》一书深入了解它。
调试
我们谈了一点修复代码中的错误,但我们没有谈论如何为其编写测试,或者调试(即修复)它的系统化方法。编写测试以证明你的代码是正确的,并且成为一个擅长调试的程序员,这些都是任何程序员必备的重要技能。如果你有兴趣了解更多,你可以阅读 Ruby 文档中关于内置 Ruby 测试库 MiniTest 的内容,网址是 ruby-doc.org/stdlib-1.9.3/libdoc/minitest/spec/rdoc/MiniTest/Spec.html。如果你感到特别冒险,你还可以阅读我最喜欢的测试库 RSpec,网址是 rspec.info/。
线程与进程
在我们所有的 Ruby 程序中,我们实际上只做了一件事:我们设置一个变量然后使用它,或者我们可能会遍历一个数组并将每个项打印到屏幕上。我们从来没有真正做到过完全同时做两件事。使用 Ruby 线程和进程时,实际上可以同时做两件事!正如你可能想象的那样,同时处理多个任务比每次只处理一个进程要困难得多,因此学习如何使用 Ruby 线程和进程需要一些实践。如果你想了解更多,你可以阅读 Jesse Storimer 的《与 Ruby 线程一起工作》(* www.jstorimer.com/products/working-with-ruby-threads/ *)。小心——这本书真的很高级!
创建网站
最后,虽然我们讨论过 Ruby 的 Web 服务器,如 WEBrick,但我们并没有深入讨论如何用 Ruby 创建整个网站。你可能听说过 Ruby on Rails(我在描述 Railscasts 时提到过它,在 在线与多媒体 一章),它是一个由许多 gem 组成的大型代码库,帮助简化用 Ruby 编写网站的过程。它是构建网站的一种好方式,且非常流行,但有时较新的 Ruby 程序员会难以理解它做了哪些事情以及它的设计决策。如果你想用 Ruby 来创建网站,你可能想从一个较小、较简单的程序开始(而且是我最喜欢的之一),它叫做Sinatra。你可以在网上找到它,网址是 www.sinatrarb.com/。
我承认:我一直在拖延。我不想让这本书结束!但可惜的是,我已经把所有 Ruby 的智慧都传授给你了。现在你知道我所知道的一切,而且你还拥有国王、女王、Ruben、Scarlet 和他们所有朋友们的聪明才智与经验。我知道你能做到!从一开始我就相信你。所以即使书必须结束,至少它以我正确的结尾!
当你合上这本书时,我希望你做一件事:启动你自己的个人计算机设备,编写一个 Ruby 程序。它可以做任何你想做的事情,无论是大是小,无论是愚蠢还是严肃。别担心它会坏掉!我们学习的唯一方法就是编写程序、破坏它们、修复它们并使它们更好,所以如果你的程序坏掉或最初没有按预期工作,也完全没问题。你是在为机器编写故事和诗歌,冒险的最大部分并不是拥有一个完成并完美运行的程序,而是沿途发生的所有疯狂事物。
所以,去吧!去写你能写出的最棒程序,享受乐趣吧。我会再见的。

附录 A. 在 Mac 和 Linux 上安装 Ruby
在 Mac 上安装
新款 Mac 自带 Ruby 2.0,因此如果你在这里,可能是使用了一个较旧的 Mac,它装的是 Ruby 1.8.7。别担心!我们会很快帮你升级。
打开你的终端并输入以下代码。($ 只是显示你开始输入的位置——不要输入$!)这将安装一个名为 RVM(Ruby 版本管理器)的工具以及 Ruby 2.0。
$ **\curl -L https://get.rvm.io | bash -s stable --ruby=2.0.0**
**--auto-dotfiles**
一旦完成,你会看到一大堆文本弹出,告诉你你的计算机正在下载 Ruby。下载完成后,关闭终端,重新打开终端,然后输入ruby -v。你应该看到计算机输出带有ruby 2.0.0的响应!
如果你的 Ruby 版本仍然不是 Ruby 2.0,你可以尝试使用 Homebrew 包管理器来安装它。首先,安装 Homebrew:
$ **ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/**
**install)"**
一旦命令成功执行完成,你可以直接输入以下内容:
$ **brew install ruby**
在撰写本文时,Homebrew 会自动安装 Ruby 2.1.3。这个版本比 Ruby 2.0 新一点,它将能够与本书中的代码示例兼容。
在 Linux 上安装
打开你的终端并输入以下代码。($ 只是显示你开始输入的位置——不要输入$!)这将安装一个名为 RVM(Ruby 版本管理器)的工具以及 Ruby 2.0。
$ **\curl -L https://get.rvm.io | bash -s stable --ruby=2.0.0 --auto-**
**dotfiles**
一旦完成,你会看到一大堆文本弹出,告诉你你的计算机正在下载 Ruby。下载完成后,关闭终端,重新打开终端,然后输入ruby -v。你应该看到计算机输出带有ruby 2.0.0的响应!
如果你遇到错误或计算机告诉你 Ruby 没有安装,找一个可靠的成年人并查看 Ruby 安装页面 www.ruby-lang.org/en/installation/。可能有一个专为你版本的 Linux 设计的最新安装包,使用该安装包安装 Ruby 可能会更加方便,而不是使用 RVM。你也可以请你的成年人去 IRC 上询问 #ruby 频道的朋友们帮助。
附录 B. 故障排除
在运行 Ruby 脚本或使用 IRB 时,你可能会遇到一些常见的错误。我在这里列出了一些错误以及修复它们的小贴士!
运行 Ruby 脚本时出错
从命令行运行 Ruby 脚本时,你可能会遇到两种常见的错误: 命令未找到 和 “没有此文件或目录”。以下是解决这些问题的一些建议。
找不到命令
如果你正在运行一个 Ruby 脚本,并且得到如下输出:
$: command not found
这很可能意味着你不小心在 ruby 命令前输入了一个 $。我使用 $ 符号来表示你正在从命令行运行 Ruby 脚本,并带有文件名(例如 ruby my_fancy_script.rb);你不应该输入 $ 符号本身!
没有此文件或目录
如果你遇到类似下面的错误:
No such file or directory -- some_filename.rb (LoadError)
这意味着你尝试运行ruby some_filename.rb,但是该文件在你当前所在的文件夹中并不存在。
要解决这个问题,首先确保你在保存 Ruby 脚本的文件夹中。你可以使用 cd 命令(“change directory”)从一个文件夹切换到另一个文件夹。查看 创建你的第一个脚本 获取有关如何使用 cd 命令的帮助。
如果你在正确的文件夹中,且命令仍然给出错误,检查你的文件名拼写!(我经常会打错 Ruby 文件名。)
使用 IRB 时的错误
使用 IRB 时,你可能会遇到一些常见错误。下面是如何解决它们的办法,以及修正拼写错误和其他错误的一些实用技巧。
未定义的本地变量或方法
如果你在 IRB 中尝试调用一个方法,并且得到类似的错误:
NameError: undefined local variable or method `some_method_name' for
main:Object
这意味着你尝试使用一个 Ruby 不认识的方法。当你退出并重新启动 IRB 时,Ruby 会忘记你之前的所有操作——所以如果你定义了一个方法,退出 IRB 后重新启动,你需要重新定义该方法才能继续使用它。(如果你需要复习如何定义方法,请查看 定义你自己的方法。)如果你的方法来自某个文件,请确保使用命令 load 'your_file.rb' 加载该文件,如果一切都失败了,再检查一下方法名称的拼写是否正确。
语法错误
如果你遇到类似下面的错误:
SyntaxError: (irb):1: syntax error, unexpected 'something_here'
这意味着你写的 Ruby 代码有些地方不太对,IRB 不知道该怎么处理它。检查代码中是否有小错误,例如拼写错误、数组元素之间缺少逗号,或是哈希中的 => 或冒号丢失。
无法将 nil 转换为字符串
如果你遇到类似下面的错误:
TypeError: can't convert nil into String
这意味着你试图用一种 Ruby 类型(如字符串、整数或nil)做某件事,但 Ruby 期待的是另一种类型。这通常发生在某个值是nil,但你并不知道;如果你看到这个错误,试着用puts输出所有变量的值,确保每个变量的类型(字符串、整数、数组等)符合你的预期!(有关puts命令的帮助,请参见了解 IRB,有关变量类型的复习,请参见更多关于变量。)
你刚才说的是什么 . . . ?
有时,你可能会看到 Ruby 打印出类似下面的内容:
...?
这意味着 Ruby 期待你“完成你的想法”。通常情况下,它意味着你按了 ENTER 却没有闭合一个字符串,或者你输入的最后一个符号是 + 或 -。你只需要完成那个想法——完成你开始输入的表达式,闭合你打开的字符串或数组,或者 Ruby 等待的其他内容——这样就可以了。例如:
>> 1 +
...? 2
=> 3
如果你不知道 Ruby 正在等待什么,或者你只是输入错了想重新开始,你可以按 CTRL-C 告诉 IRB 不要再等待你。你会返回到常规的 IRB 提示符,并可以从那里继续。(有关 CTRL-C 的更多信息,请参见调查王国的 Web 服务器.)
清除屏幕
有时你会在 IRB 中输入很多内容,想要清除屏幕。根据你使用的操作系统,有几种方法可以做到这一点。在 Mac 上,你可以按 ⌘-K 或 CTRL-L,或者在 IRB 中输入system 'clear'然后按 ENTER。如果你使用的是 Linux,按 CTRL-L 或输入system 'clear'应该有效。如果你使用的是 Windows,按 CTRL-L 或输入system 'cls'(不是'clear'!)应该可以清除屏幕。
返回到上一个命令
如果你在任何时候想返回到你在 IRB 中输入的上一个命令,只需按键盘上的上箭头!这非常方便,尤其是在你刚清除屏幕,然后意识到需要重新输入某个命令,或者你输入错了命令,想要在不重新输入所有内容的情况下重试。
查找答案!
最后,如果你遇到一个不知道如何处理的错误,去互联网上搜索一下(记得先征得你本地成年人的许可!)。每个人都会遇到错误,所以很可能已经有人找到了解决你可能遇到的错误的方法。即使是最优秀的程序员,也每天都需要查找自己不知道的东西。你越是习惯在遇到困惑时寻找答案,你在编写 Ruby 代码时就会越高效、越快乐。
更新
访问 nostarch.com/rubywizardry/ 获取更新、勘误和其他信息。
更多有趣的书籍,专为好奇的孩子们!

儿童 JavaScript 编程
编程的趣味入门
由 NICK MORGAN 编写
2014 年 12 月,336 页,$34.95
ISBN 978-1-59327-408-5
全彩

洛伦·伊普苏姆
关于计算机科学及其他不太可能的事物的故事
由 CARLOS BUENO 编写
2014 年 12 月,192 页,$16.95
ISBN 978-1-59327-574-7
全彩

儿童 Python 编程
编程的趣味入门
由 JASON BRIGGS 编写
2012 年 12 月,344 页,$34.95
ISBN 978-1-59327-407-8
全彩

学习 Scratch 编程
通过游戏、艺术、科学和数学的视觉编程入门
由 MAJED MARJI 编写
2014 年 2 月,288 页,$34.95
ISBN 978-1-59327-543-3
全彩

Rails 快速入门
Rails 开发的实用指南
由 ANTHONY LEWIS 编写
2014 年 10 月,296 页,$34.95
ISBN 978-1-59327-572-3

漫画指南™:数据库
由 MANA TAKAHASHI、SHOKO AZUMA 和 TREND-PRO CO., LTD. 编写
2009 年 1 月,224 页,$19.95
ISBN 978-1-59327-190-9
800.420.7240 或 415.863.9900 | sales@nostarch.com | www.nostarch.com


浙公网安备 33010602011771号