Ruby-之书-全-

Ruby 之书(全)

原文:The Book of Ruby

译者:飞龙

协议:CC BY-NC-SA 4.0

引言

无标题图片

由于你现在正在阅读一本关于 Ruby 的书,我认为可以假设你不需要我向你证明 Ruby 语言的优点。相反,我将采取一种多少有些非传统的步骤,从警告开始:许多人被 Ruby 的简单语法和易用性所吸引。他们错了。Ruby 的语法乍一看可能 看起来 很简单,但随着你对语言的了解越来越深,你将越来越意识到它实际上是非常复杂的。简单的事实是,Ruby 有许多陷阱等待着粗心大意的程序员跌入。

在本书中,我的目标是引导你安全地越过陷阱,带领你穿越 Ruby 语法和类库的波涛汹涌的水域。在这个过程中,我将探索 Ruby 的平坦、铺好的高速公路和崎岖不平的小路。到旅程结束时,你应该能够安全有效地使用 Ruby,而不会在途中遇到意外的危险。

《Ruby 编程宝典》 描述了 Ruby 语言的 1.8.x 和 1.9.x 版本。在大多数方面,Ruby 1.8 和 1.9 非常相似,为其中一个版本编写的程序在另一个版本中无需修改即可运行。然而,这个规则有一些重要的例外,这些例外在文本中都有说明。Ruby 1.9 可以看作是通往 Ruby 2.0 的一个过渡。在撰写本文时,Ruby 2.0 的发布日期尚未公布。即便如此,基于目前可用的信息,我预计本书中关于 Ruby 1.9 的大部分(或全部)信息也应适用于 Ruby 2.0。

什么是 Ruby?

Ruby 是一种跨平台的解释型语言,它具有与其他“脚本”语言(如 Perl 和 Python)许多共同特征。它的语法易于阅读,乍一看有点像 Pascal 语言。它是彻底面向对象的,并且与“纯”面向对象语言的鼻祖 Smalltalk 有很多共同之处。据说,对 Ruby 的发展影响最大的语言是 Perl、Smalltalk、Eiffel、Ada 和 Lisp。Ruby 语言是由 Yukihiro Matsumoto(通常被称为 Matz)创建的,并于 1995 年首次发布。

什么是 Rails?

在过去几年中,围绕 Ruby 的许多兴奋之处可以归因于一个名为 Rails 的 Web 开发框架——通常被称为 Ruby on Rails。Rails 是一个令人印象深刻的框架,但它并不是 Ruby 的全部。实际上,如果你在没有首先掌握 Ruby 的情况下直接跳入 Rails 开发,你可能会发现你最终创建的应用程序连你自己都不理解。(这在 Ruby on Rails 新手中非常普遍。)理解 Ruby 是理解 Rails 的必要前提。你将在第十九章(ch19.html "第十九章。Ruby on Rails")中了解 Rails。

Ruby 风格问题

一些 Ruby 程序员对“Ruby 风格”编程的构成有着非常固定——甚至可以说是强迫性的——看法。例如,有些人热情地坚持认为method_names_use_underscoresvariableNamesDoNot。将单词分开表示的命名风格被称为驼峰式——而且这是这本书中最后一次提到这一点。

我从未理解为什么人们对命名约定如此激动。你喜欢下划线,我无法忍受;你说 po_ta_toes,我说 poTaToes。在我看来,你选择在 Ruby 中编写标识符名称的方式对任何人都没有兴趣,除了你或你的编程同事。

这并不是说我对编程风格没有看法。相反,我有着非常强烈的看法。在我看来,良好的编程风格与命名约定无关,而与良好的代码结构和清晰度有关。例如,括号这样的语言元素很重要。括号可以澄清代码,避免在像 Ruby 这样的高度动态语言中可能意味着程序按预期工作或充满惊喜(也称为错误)的歧义。关于这一点,请参阅“歧义”和“括号”的索引条目。

在超过二十年的编程生涯中,我通过痛苦的教训学到的一点是,编写良好代码最重要的特性是清晰性和缺乏歧义。易于理解和调试的代码也更有可能易于维护。如果你通过采用某些命名约定来实现这一目标,那很好。如果你做不到,那也很好。“Ruby 圣经”并不宣扬风格问题。

如何阅读这本书

本书分为小块。每一章介绍一个主题,该主题被细分为子主题。每个编程主题都伴随着一个或多个小型、自包含、可立即运行的 Ruby 程序。

如果你想要遵循一个结构良好的“课程”,请按顺序阅读每一章。如果你更喜欢更实用的方法,可以先运行程序,并在需要解释时参考文本。如果你已经有一些 Ruby 经验,你可以自由地按任何你认为有用的顺序挑选主题。这本书中没有庞大的应用程序,所以你不必担心如果你不按顺序阅读章节可能会迷失方向!

深入挖掘

除了第一章之外,本书每一章都包含一个名为深入挖掘的部分。这是您将更深入地探索 Ruby 特定方面(包括我刚才提到的一些复杂路径)的地方。在许多情况下,您可以跳过“深入挖掘”部分,仍然可以学习到您需要的所有 Ruby 知识。另一方面,您通常在这些部分中会接近 Ruby 的内部工作原理,所以如果您跳过它们,您可能会错过一些相当有趣的内容。

理解文本

在《Ruby 编程秘籍》中,Ruby 源代码的编写方式如下:

def saysomething
    puts( "Hello" )
end

通常代码会带有注释。Ruby 注释是单行中跟随井号(#)的任何文本。注释会被 Ruby 解释器忽略。当我想要引起对 Ruby 显示的某些输出或代码返回的某个值(即使该值没有显示)的注意时,我会用这种类型的注释来表示:# =>。偶尔,当我想引起对用户应输入的某些输入的注意时,我会使用这种类型的注释:# <=。以下是一个示例,以说明这些注释约定:

puts("Enter a calculation:" ) # Prompt user        this is a simple comment
exp = gets().chomp()          # <= Enter 2*4    comment shows data to enter
puts( eval( exp ))            # => 8     comment shows result of evaluation

当一段代码返回或显示的数据太多,无法在单行注释中显示时,输出可能如下所示:

This is the data returned from method #1
This is the data returned from method #2
This is the data returned from method #3

helloname.rb

当一个示例程序伴随代码时,程序名称在此处以边注形式显示。

提供提示或额外信息的说明性注释如下所示:

注意

这是一个说明性注释。

对文本中提到的点的更深入解释可能如下所示:

进一步解释

这是一些额外的信息。如果您愿意,可以跳过它——但是如果您这样做,可能会错过一些有趣的内容!

下载 Ruby

您可以在www.ruby-lang.org/en/downloads/下载最新的 Ruby 版本。请确保下载二进制文件(而不仅仅是源代码)。Windows 用户可以选择使用 Ruby Installer 安装 Ruby,该安装器可在www.rubyinstaller.org/找到。Ruby 还有几种替代实现,其中最著名的是 JRuby。您可以在附录 D 中找到有关下载这些实现的信息。

获取示例程序的源代码

本书每一章的所有程序都可以作为.zip存档在www.nostarch.com/boruby.htm下载。

当您解压程序时,您会发现它们被分组到一个目录集中——每个章节一个目录。

运行 Ruby 程序

在包含你的 Ruby 程序文件的源目录中保持一个命令窗口通常是很有用的。假设 Ruby 解释器在你的系统上正确配置了路径,那么你将能够通过输入ruby 程序名来运行程序。例如,运行helloworld.rb程序的命令如下:

ruby helloworld.rb

如果你使用 Ruby IDE,你可能能够将 Ruby 程序加载到 IDE 中,并使用该 IDE 的集成工具运行它们。

Ruby 库文档

《Ruby 编程宝典》涵盖了标准 Ruby 库中的许多类和方法——但绝不是全部!因此,在某个阶段,你将需要参考 Ruby 所使用的所有类的完整文档。幸运的是,Ruby 类库包含了嵌入的文档,这些文档已经被提取并编译成易于浏览的参考,并且以多种格式提供。例如,你可以找到 Ruby 1.9 的在线文档,网址为www.ruby-doc.org/ruby-1.9/index.html。对于 Ruby 1.8,请访问www.ruby-doc.org/ruby-1.8/index.html

好的,前言就到这里——让我们开始工作吧。

第一章。字符串、数字、类和对象

无标题图片

关于 Ruby 的第一件事是它很容易使用。为了证明这一点,让我们看看传统“Hello world”程序的代码:

1helloworld.rb

puts 'hello world'

这就是它的全部内容。程序包含一个方法,puts,和一个字符串,“hello world。”它没有任何头文件或类定义,也没有任何导入部分或“main”函数。这真的是简单到极致。加载代码,*1helloworld.rb*,然后尝试运行。

获取和输出输入

将字符串“输出”到输出(这里是一个命令窗口)后,明显的下一步是“获取”一个字符串。正如你可能猜到的,Ruby 用于此的方法是 gets*2helloname.rb* 程序提示用户输入他的或她的名字——假设它是 Fred——然后显示问候:“Hello Fred。”以下是代码:

2helloname.rb

print( 'Enter your name: ' )
name = gets()
puts( "Hello #{name}" )

虽然这仍然非常简单,但需要解释一些重要的细节。首先,请注意,我使用了 print 而不是 puts 来显示提示。这是因为 puts 在打印的字符串末尾添加一个换行符,而 print 则不会;在这种情况下,我希望光标保持在提示的同一行。

在下一行,我使用 gets() 读取用户按下回车键时输入的字符串。这个字符串被分配给变量 name。我没有预先声明这个变量,也没有指定其类型。在 Ruby 中,你可以在需要时创建变量,解释器“推断”它们的类型。在这个例子中,我将一个字符串分配给 name,这样 Ruby 就知道 name 变量的类型必须是字符串。

注意

Ruby 区分大小写。一个名为 myvar 的变量与一个名为 myVar 的变量不同。在示例项目中,变量如 name 必须以小写字母开头。如果它以大写字母开头,Ruby 会将其视为常量。我将在第六章中更多地讨论常量。

顺便说一下,gets() 后面的括号是可选的,printputs 后面括号包围的字符串也是可选的;如果你去掉它们,代码仍然会正常运行。然而,括号可以帮助解决歧义,并且在某些情况下,如果省略了它们,解释器会警告你。

字符串和嵌入式评估

示例代码的最后一行相当有趣:

puts( "Hello #{name}" )

在这里,name 变量被嵌入到字符串中。你这样做是通过将变量放在由星号(或“数字”或“井号”字符)前缀的两个大括号之间,例如 #{}。这种嵌入式评估仅适用于双引号分隔的字符串。如果你尝试用单引号分隔的字符串这样做,变量将不会被评估,字符串 'Hello #{name}' 将会按原样显示。

你还可以嵌入非打印字符,如换行符("\n")和制表符("\t"),甚至可以嵌入程序代码和数学表达式。例如,假设你有一个名为 showname 的方法,它返回字符串 “Fred.”。以下字符串在评估过程中会调用 showname 方法并显示 “Hello Fred”:

puts "Hello #{showname}"

看看你是否能猜出以下代码会显示什么:

3string_eval.rb

puts( "\n\t#{(1 + 2) * 3}\nGoodbye" )

现在运行 3string_eval.rb 程序,看看你是否正确。

数字

数字的使用与字符串一样简单。例如,假设你想根据某项商品不含税价值或小计来计算销售价格或总金额。为此,你需要将小计乘以适用的税率,并将结果加到小计的价值上。假设小计为 $100,税率为 17.5%,以下 Ruby 程序执行计算并显示结果:

4calctax.rb

subtotal = 100.00
taxrate = 0.175
tax = subtotal * taxrate
puts "Tax on $#{subtotal} is $#{tax}, so grand total is $#{subtotal+tax}"

显然,如果这个程序能够对各种小计进行计算,而不是反复计算相同的值,将会更有用!以下是一个简单的计算器,它会提示用户输入小计:

taxrate = 0.175
print "Enter price (ex tax): "
s = gets
subtotal = s.to_f
tax = subtotal * taxrate
puts "Tax on $#{subtotal} is $#{tax}, so grand total is $#{subtotal+tax}"

这里 s.to_f 是 String 类的一个方法。它尝试将字符串转换为浮点数。例如,字符串 "145.45" 将被转换为浮点数 145.45。如果字符串无法转换,则返回 0.0。例如,"Hello world".to_f 将返回 0.0。

注释

许多与本书一起提供的源代码示例都带有注释,这些注释会被 Ruby 解释器忽略。你可以在散列符号(#)之后放置注释。该符号之后的整行文本都被视为注释:

# this is a comment
puts( "hello" ) # this is also a comment

如果你想注释掉多行文本,可以在开始处放置 =begin,在结尾处放置 =end=begin=end 必须与左边缘对齐):

=begin
 This is a
 multiline
 comment
=end

测试条件:if..then

之前展示的简单税费计算器代码的问题在于它接受负小计并对其计算负税费——这种情况政府可能不会持乐观态度!因此,我需要检查负数,并在找到时将它们设置为零。这是我的新代码版本:

5taxcalculator.rb

taxrate = 0.175
print "Enter price (ex tax): "
s = gets
subtotal = s.to_f

if (subtotal < 0.0)  then
  subtotal = 0.0
end

tax = subtotal * taxrate
puts "Tax on $#{subtotal} is $#{tax}, so grand total is $#{subtotal+tax}"

Ruby 的 if 测试与其他编程语言中的 if 测试类似。注意,然而,括号是可选的,关键字 then 也是可选的。然而,如果你要写以下内容,且测试条件后没有换行,则 then 是必需的:

if (subtotal < 0.0) then subtotal = 0.0 end

将所有内容放在一行中,像这样并不会增加代码的清晰度,这就是为什么我倾向于避免这样做。我对 Pascal 的长期熟悉使我本能地想要在if条件之后添加一个then,但因为这个实际上并不是必需的,你可以把这看作是我的一种任性的古怪行为。终止if块的end关键字是不是可选的。如果你忘记添加它,你的代码将无法运行。

局部和全局变量

在上一个示例中,我给变量如subtotaltaxtaxrate赋值。以小写字母开头的变量,如这些,被称为局部变量。这意味着它们只存在于程序的一个特定部分——换句话说,它们被限制在一个明确的范围内。以下是一个例子:

variables.rb

localvar = "hello"
$globalvar = "goodbye"

def amethod
    localvar = 10
    puts( localvar )
    puts( $globalvar )
end

def anotherMethod
    localvar = 500
    $globalvar = "bonjour"
    puts( localvar )
    puts( $globalvar )
end

在之前的代码中,有两个函数(或方法),amethodanotherMethod,每个都使用关键字def声明,并包含到end关键字之前的代码。有三个名为localvar的局部变量。一个在程序的“主要作用域”内被赋予了"hello"的值;另外两个在两个不同的方法的作用域内被赋予了整数。由于每个局部变量有不同的作用域,这些赋值对在不同作用域中具有相同名称的其他局部变量没有影响。你可以通过依次调用这些方法来验证这一点。以下示例显示了注释后的输出,后跟=>字符。在这本书中,输出或返回值通常会以这种方式表示:

amethod           #=> localvar = 10
anotherMethod     #=> localvar = 500
amethod           #=> localvar = 10
puts( localvar )  #=> localvar = "hello"

另一方面,一个全局变量——以美元符号字符($)开头——具有全局作用域。当在方法内部对一个全局变量进行赋值时,这也会影响程序其他地方该变量的值:

amethod            #=>  $globalvar = "goodbye"
anotherMethod      #=>  $globalvar = "bonjour"
amethod            #=>  $globalvar = "bonjour"
puts( $globalvar ) #=>  $globalvar = "bonjour"

类和对象

我们不一一介绍 Ruby 的所有语法——它的类型、循环、模块等等——而是快速地看看如何创建类和对象。(但别担心,我们很快就会回到那些其他主题。)

基本术语:类、对象和方法

类是对象的蓝图。它定义了对象包含的数据以及它的行为方式。可以从单个类创建许多不同的对象。因此,你可能有一个 Cat ,但三个 cat 对象:tiddles、cuddles 和 flossy。方法就像是在类内部定义的函数或子程序。

说 Ruby 是面向对象的可能看起来没什么大不了的。现在不是所有语言都是这样吗?好吧,在一定程度上是这样的。大多数现代的“面向对象”语言(Java、C++、C#、Object Pascal 等等)都有不同程度的面向对象编程(OOP)特性。然而,Ruby 则是极度面向对象的。实际上,除非你曾经使用过 Smalltalk 或 Eiffel(这些语言对对象的执着甚至超过 Ruby),否则 Ruby 可能是你用过的最面向对象的编程语言。每一块数据——从简单的数字或字符串到更复杂的东西,如文件或模块——都被视为对象。而且,你几乎用对象做的所有事情都是通过方法完成的。甚至像加号(+)和减号()这样的运算符也是方法。考虑以下例子:

x = 1 + 2

在这里,+ 是 Fixnum(整数)对象 1 的一个方法。值 2 被发送到这个方法;结果是 3,然后返回,并分配给对象 x。顺便提一下,赋值运算符(=)是“你用对象做的所有事情都是通过方法完成”的规则中罕见的例外之一。赋值运算符是一个特殊的内置“东西”(我急忙补充,这不是正式术语),它不是任何东西的方法。

现在,你将看到如何创建自己的对象。在大多数其他面向对象编程语言中,Ruby 对象是由类定义的。类就像一个蓝图,从该蓝图构建单个对象。例如,这个类定义了一只狗:

6dogs.rb

class Dog
    def set_name( aName )
       @myname = aName
    end
end

注意,类定义以关键字 class(全部小写)和类名本身开始,类名必须以大写字母开头。该类包含一个名为 set_name 的方法。该方法接受一个传入参数,aName。方法体将 aName 的值赋给一个名为 @myname 的变量。

实例变量

以井号(@)开头的变量是 实例变量,这意味着它们属于类的单个对象(或 实例)。不需要预先声明实例变量。我可以通过调用 new 方法来创建 Dog 类的实例(即“狗对象”)。在这里,我创建了两个狗对象(注意,尽管类名以大写字母开头,但对象名以小写字母开头):

mydog = Dog.new
yourdog = Dog.new

目前,这两只狗还没有名字。所以,接下来我要做的是调用 set_name 方法来给它们命名:

mydog.set_name( 'Fido' )
yourdog.set_name( 'Bonzo' )

从对象中检索数据

给每只狗取了名字后,我需要有一种方法来在以后找出它们的名字。我该如何做呢?我不能在对象内部乱翻来获取 @name 变量,因为每个对象的内部细节只有对象本身才知道。这是“纯”面向对象的一个基本原则:每个对象内部的数据是私有的。每个对象都有精确的进入和退出方式(例如,set_name 方法)和精确的退出方式。只有对象本身可以随意处理其内部状态;外部世界不能。这被称为 数据隐藏,它是 封装 原则的一部分。

封装

封装 描述了对象包含其自己的数据和操作这些数据所需的方法的事实。一些面向对象的语言鼓励或强制执行 数据隐藏,以便对象内部封装的数据不能被该对象之外的其他代码访问。在 Ruby 中,数据隐藏的执行并不像最初看起来那么严格。你可以使用一些非常脏的技巧来在对象内部捣乱,但为了简单起见,我现在会默默地忽略这个语言的这些特性。

由于你需要每只狗都知道自己的名字,让我们为 Dog 类提供一个 get_name 方法:

def get_name
    return @myname
end

这里的 return 关键字是可选的。当它被省略时,Ruby 方法将返回最后一个评估的表达式。然而,为了清晰起见——以及避免比这个更复杂的方法带来的意外结果——我将养成显式返回任何我打算使用的值的习惯。

最后,让我们通过让狗说话来给它一些行为。下面是完成后的类定义:

class Dog
    def set_name( aName )
       @myname = aName
    end

    def get_name
       return @myname
    end

    def talk
       return 'woof!'
    end
end

现在,你可以创建一只狗,给它取名字,显示它的名字,并让它说话:

mydog = Dog.new
mydog.set_name( 'Fido' )
puts(mydog.get_name)
puts(mydog.talk)

我在 6dogs.rb 程序中编写了这个代码的扩展版本。这个版本还包含一个与 Dog 类相似的 Cat 类,除了它的 talk 方法,自然地返回一个 喵喵 而不是 汪汪

当一个变量未分配时会发生什么?

哎呀!看来这个程序包含了一个错误。名为 someotherdog 的对象从未为其 @name 变量分配值,因为它的 set_name() 方法从未被调用。这意味着以下尝试打印其名称的代码无法成功:

puts(someotherdog.get_name)

幸运的是,当你尝试显示这只狗的名字时,Ruby 并不会崩溃。相反,它只是打印出“nil。”你很快就会看到一种简单的方法来确保这种错误不再发生。

消息、方法和多态性

顺便说一句,这个猫和狗的例子是基于一个经典的 Smalltalk 演示程序,它说明了相同的“消息”(例如 talk)可以发送到不同的对象(例如猫和狗),并且每个不同的对象都会用其自己的特殊方法(这里是指 talk 方法)对相同的消息做出不同的响应。具有包含具有相同名称的方法的不同类的能力,可以用花哨的面向对象术语 多态性 来描述。

当你运行一个如 6dogs.rb 的程序时,代码是按顺序执行的。类本身的代码不会执行,直到程序底部的代码创建了这些类的实例(即对象)。你会发现我经常将类定义与在程序运行时执行的“自由”代码块混合。这可能不是你编写主要应用程序的方式,但仅用于尝试事情,它非常方便。

自由代码块?

如果你认为 Ruby 真的是一个面向对象的语言,你可能觉得可以输入“自由浮动”的方法很奇怪。实际上,当你运行一个程序时,Ruby 会创建一个主对象,而你主代码单元(即你加载并运行的主 Ruby 代码文件)中的任何代码实际上都是在那个对象内部运行的。你可以通过创建一个新的源文件并添加以下代码来轻松验证这一点:

puts self
puts self.class

当你运行这个程序时,你会看到以下输出:

main
Object

这个程序的一个明显缺陷是,两个类,猫和狗,非常重复。有一个类,动物(Animal),它有 get_nameset_name 方法,而两个子类,猫和狗,只包含特定物种动物的行为(汪汪叫或喵喵叫)会更合理。我们将在下一章中了解到如何做到这一点。

构造函数:new 和 initialize

让我们来看一个用户自定义类的另一个例子。加载 7treasure.rb。这是一个正在制作中的冒险游戏。它包含两个类,物品(Thing)和宝藏(Treasure)。物品类与之前程序中的猫(Cat)和狗(Dog)类类似——只不过它不会汪汪叫或喵喵叫,就是这样。

7treasure.rb

class Thing
    def set_name( aName )
        @name = aName
    end

    def get_name
        return @name
    end
end

class Treasure
      def initialize( aName, aDescription )
        @name         = aName
        @description  = aDescription
      end

      def to_s # override default to_s method
       "The #{@name} Treasure is #{@description}\n"
      end
end

thing1 = Thing.new
thing1.set_name( "A lovely Thing" )
puts thing1.get_name

t1 = Treasure.new("Sword", "an Elvish weapon forged of gold")
t2 = Treasure.new("Ring", "a magic ring of great power")
puts t1.to_s
puts t2.to_s
# The inspect method lets you look inside an object
puts "Inspecting 1st treasure: #{t1.inspect}"

宝藏类没有 get_nameset_name 方法。相反,它包含一个名为 initialize 的方法,它接受两个参数。这两个值随后被分配给 @name@description 变量。当一个类包含一个名为 initialize 的方法时,当使用 new 方法创建对象时,它将自动被调用。这使得它是一个设置对象实例变量值的方便位置。

这有两个明显的优点,比使用 set_name 这样的方法设置每个实例变量要好。首先,一个复杂的类可能包含许多实例变量,你可以使用单个 initialize 方法设置所有这些变量的值,而不是使用许多单独的“设置”方法;其次,如果变量在对象创建时自动初始化,你就不会得到一个“空”变量(就像在之前程序中尝试显示某个其他狗的名字时返回的“nil”值)。

最后,我创建了一个名为 to_s 的方法,它返回 Treasure 对象的字符串表示形式。方法名 to_s 不是任意的——在标准的 Ruby 对象层次结构中使用了相同的方法名。实际上,to_s 方法是为 Object 类本身定义的,它是 Ruby 中所有其他类的最终祖先(除了 BasicObject 类,你将在下一章中更详细地了解)。通过重新定义 to_s 方法,我添加了更适合 Treasure 类的新行为。换句话说,我 重写 了它的 to_s 方法。

由于 new 方法创建了一个对象,它可以被认为是对象的 构造函数。构造函数是一个为对象分配内存并执行(如果存在)initialize 方法以将指定的值分配给新对象的内部变量的方法。你通常不应该实现自己的 new 方法版本。相反,当你想要执行任何“设置”操作时,应在 initialize 方法中执行。

垃圾回收

在许多语言(如 C++ 和 Delphi for Win32)中,当对象不再需要时,程序员有责任销毁任何创建的对象。换句话说,对象既有 析构函数 也有 构造函数。在 Ruby 中这不是必要的,因为内置的 垃圾回收器 会自动销毁对象并回收它们不再被程序引用时使用的内存。

检查对象

注意,在 7treasure.rb 程序中,我使用了 inspect 方法“查看”了 Treasure 对象 t1 的内部结构:

puts "Inspecting 1st treasure: #{t1.inspect}"

inspect 方法为所有 Ruby 对象定义。它返回一个包含对象人类可读表示的字符串。在本例中,它显示如下:

#<Treasure:0x28962f8 @description="an Elvish weapon forged of gold", @name="Sword">

这以类名 Treasure 开头。接下来是一个数字,这个数字可能与之前显示的数字不同——这是 Ruby 为这个特定对象提供的内部识别码。接下来显示了对象的变量名称和值。

Ruby 还提供了 p 方法作为检查对象和打印其详细信息的快捷方式,如下所示:

p( anobject )

其中 anobject 可以是任何类型的 Ruby 对象。例如,假设你创建了以下三个对象:一个字符串、一个数字和一个 Treasure 对象:

p.rb

class Treasure
    def initialize( aName, aDescription )
      @name         = aName
      @description  = aDescription
    end

    def to_s # override default to_s method
         "The #{@name} Treasure is #{@description}\n"
    end
end

a = "hello"
b = 123
c = Treasure.new( "ring", "a glittery gold thing" )

现在,你可以使用 p 来显示这些对象:

p( a )
p( b )
p( c )

这是 Ruby 显示的内容:

"hello"
123
#<Treasure:0x3489c4 @name="ring", @description="a glittery gold thing">

要了解如何使用 to_s 与各种对象,并测试在没有重写 to_s 方法的情况下 Treasure 对象将如何转换为字符串,请尝试 8to_s.rb 程序。

8to_s.rb

puts(Class.to_s)        #=> Class
puts(Object.to_s)       #=> Object
puts(String.to_s)       #=> String
puts(100.to_s)          #=> 100
puts(Treasure.to_s)     #=> Treasure

正如你所见,当调用 to_s 方法时,Class、Object、String 和 Treasure 等类会简单地返回它们的名称。一个对象,例如 Treasure 对象 t,会返回它的标识符——这个标识符与 inspect 方法返回的标识符相同:

t = Treasure.new( "Sword", "A lovely Elvish weapon" )
puts(t.to_s)
    #=> #<Treasure:0x3308100>
puts(t.inspect)
    #=> #<Treasure:0x3308100 @name="Sword", @description="A lovely Elvish weapon">

尽管 7treasure.rb 程序可能为包含多种不同对象类型的游戏奠定了基础,但其代码仍然重复。毕竟,为什么会有一个包含名称的 Thing 类和一个也包含名称的 Treasure 类?将 Treasure 视为“一种” Thing 更有意义。在一个完整的游戏中,其他对象如 Rooms 和 Weapons 可能是 Thing 的其他类型。显然,是时候开始构建合适的类层次结构了,这正是你将在下一章中要做的。

第二章:类层次结构、属性和类变量

无标题图片

我们在上一章结束时创建了两个新的类:一个名为 Thing 和一个名为 Treasure 的类。尽管这两个类有一些共同的特征(特别是它们都有“名称”这一特征),但它们之间并没有任何联系。

这两个类非常简单,这种微小的重复实际上并不重要。然而,当你开始编写一些复杂程度的真实程序时,你的类将经常包含许多变量和方法,你真的不希望一遍又一遍地重复编写相同的内容。

在创建一个类层次结构中,一个类可能是一个其他(祖先)类的“特殊类型”,在这种情况下,它将自动继承其祖先的特征是有意义的。例如,在我们的简单冒险游戏中,Treasure 是 Thing 的一个特殊类型,因此 Treasure 类应该继承 Thing 类的特征。

注意

在这本书中,我经常会提到子类从它们的父类继承特征。这些术语故意暗示了“相关”类之间的一种家族关系。在 Ruby 中,每个类只有一个父类。然而,它可能从一条漫长而显赫的家族树中衍生出来,拥有许多代父母、祖父母、曾祖父母等等。

事物的普遍行为将在 Thing 类中编码。Treasure 类将自动“继承” Thing 类的所有特征,因此我们不需要再次编写它们;然后它将添加一些针对 Treasure 的特定功能。

作为一般规则,在创建类层次结构时,具有最通用行为的类位于层次结构的较高位置,而具有更多专业行为的类位于较低位置。因此,只有一个名称和描述的 Thing 类将是具有名称、描述和额外价值(value)的 Treasure 类的祖先;Thing 类也可能是具有名称、描述和出口(exits)等特征的某些其他专业类(如 Room)的祖先。

一个父类,多个子类

无标题图片

此图显示了一个具有 namedescription(在 Ruby 程序中,这些可能是内部变量,如 @name@description,以及一些访问它们的方法)的 Thing 类。Treasure 和 Room 类都从 Thing 类派生出来,因此它们自动“继承”了 namedescription。Treasure 类添加了一个新项目 value,因此现在它具有 namedescriptionvalue。Room 类添加了 exits——因此它具有 namedescriptionexits

让我们看看如何在 Ruby 中创建一个子类。加载 1adventure.rb 程序。它从定义一个具有两个实例变量 @name@description 的 Thing 类开始。

1adventure.rb

class Thing
    def initialize( aName, aDescription )
      @name         = aName
      @description  = aDescription
    end

    def get_name
        return @name
    end

    def set_name( aName )
        @name = aName
    end

    def get_description
        return @description
    end

    def set_description( aDescription )
        @description = aDescription
    end
end

当创建一个新的 Thing 对象时,@name@description 变量在 initialize 方法中被赋值。实例变量通常不能(也不应该)从类本身之外直接访问,这是封装原则(如前一章所述)的结果。要获取每个变量的值,你需要一个 get 访问器方法,如 get_name;要分配新值,你需要一个 set 访问器方法,如 set_name

超类和子类

现在看看 Treasure 类,它也在下面的程序中定义:

1adventure.rb

class Treasure < Thing
    def initialize( aName, aDescription, aValue )
        super( aName, aDescription )
        @value = aValue
    end

    def get_value
        return @value
    end

    def set_value( aValue )
        @value = aValue
    end
end

注意 Treasure 类是如何声明的:

class Treasure < Thing

左尖括号 (<) 表示 Treasure 是 Thing 的 子类 或后代,因此它继承了数据(变量)和行为(方法)。由于 get_nameset_nameget_descriptionset_description 这些方法已经在祖先类(Thing)中存在,因此这些方法不需要在后代类(Treasure)中重新编码。

Treasure 类有一项额外的数据,即它的价值 (@value),我为它编写了 getset 访问器。当创建一个新的 Treasure 对象时,它的 initialize 方法会自动被调用。Treasure 有三个变量需要初始化 (@name@description@value),因此它的 initialize 方法接受三个参数。前两个参数使用 super 关键字传递给超类(Thing)的 initialize 方法,这样 Thing 类的 initialize 方法就可以处理它们:

super( aName, aDescription )

当在方法内部使用时,super 关键字调用与当前方法同名的祖先或类中的方法。如果单独使用 super 关键字,没有指定任何参数,则将发送到当前方法的全部参数传递给祖先方法。如果在当前情况下,提供了一个特定的参数列表(这里为 aNameaDescription),则只有这些参数会被传递给祖先类的该方法。

向超类传递参数

在调用超类时括号很重要!如果参数列表为空且没有使用括号,所有参数都会传递给超类。但如果参数列表为空且使用了括号,则不会向超类传递任何参数:

super_args.rb

# This passes a, b, c to the superclass
def initialize( a, b, c, d, e, f )
   super( a, b, c )
end

# This passes a, b, c to the superclass
def initialize( a, b, c )
   super
end

# This passes no arguments to the superclass
def initialize( a, b, c)
   super()
end

注意

要更好地理解 super 的使用,请参阅 深入挖掘 中的 深入挖掘。

访问器方法

尽管在这个假想冒险游戏中的类工作得足够好,但由于所有那些 getset 访问器,它们仍然相当冗长。让我们看看你能做些什么来解决这个问题。

而不是使用两个不同的方法,get_descriptionset_description,像这样访问 @description 实例变量的值:

puts( t1.get_description )
t1.set_description("Some description" )

这样检索和分配值会方便得多,就像从简单变量中检索和分配值一样,如下所示:

puts( t1.description )
t1.description = "Some description"

要能够这样做,你需要修改 Treasure 类的定义。实现这一目标的一种方法是将 @description 的访问器方法重写如下:

accessors1.rb

def description
    return @description
end

def description=( aDescription )
    @description = aDescription
end

我已经在 accessors1.rb 程序中添加了类似的访问器。在这里,get 访问器被命名为 description,而 set 访问器被命名为 description=(即,在对应 get 访问器使用的名称后附加一个等号)。现在可以像这样分配一个新的字符串:

t.description = "a bit faded and worn around the edges"

你可以这样检索值:

puts( t.description )

注意,当你以这种方式编写 set 访问器时,你必须将 = 字符附加到方法名上,而不仅仅是将其放在方法名和参数之间。换句话说,这是正确的:

def name=( aName )

但这会导致错误:

def name   =  ( aName )

属性读取器和写入器

事实上,有一种更简单、更短的方法可以同时创建一对 getset 访问器。你只需要使用两个特殊方法,attr_readerattr_writer,后面跟着一个 symbol(一个以冒号开头的名称):

attr_reader :description
attr_writer :description

你应该像这样在你的类定义中添加此代码:

class Thing
   attr_reader :description
   attr_writer :description
    # maybe some more methods here...
end

使用符号调用 attr_reader 会创建一个 get 访问器(这里命名为 description),用于与符号匹配的实例变量(@description)。

类似地调用 attr_writer 会为实例变量创建一个 set 访问器。实例变量被认为是对象的“属性”,这就是为什么 attr_readerattr_writer 方法被这样命名的原因。

什么是符号?

在 Ruby 中,一个 symbol 是一个以冒号开头的名称(例如,:description)。Symbol 类在 Ruby 类库中定义,用于在 Ruby 解释器内部表示名称。当你将一个或多个符号作为参数传递给 attr_reader(这是 Module 类的一个方法)时,Ruby 会创建一个实例变量和一个 get 访问器方法。这个访问器方法返回相应变量的值;实例变量和访问器方法都将采用符号指定的名称。符号将在 第十一章 中详细讨论。

accessors2.rb 程序包含了一些属性读取器和写入器在实际操作中的示例。这是它的 Thing 类版本:

accessors2.rb

class Thing

     attr_reader :description
      attr_writer :description
     attr_writer :name

      def initialize( aName, aDescription )
          @name         = aName
          @description  = aDescription
      end

         # get accessor for @name
     def name
          return @name.capitalize
      end

end

在这里,Thing类明确地为@name属性定义了一个get方法访问器。编写这样一个完整方法的优点是,它给你提供了做一些额外处理的机会,而不仅仅是读取和写入属性值。get访问器name ![http://atomoreilly.com/source/nostarch/images/860154.png] 使用String.capitalize方法将@name的字符串值的首字母转换为大写。

当为@name属性赋值时,我不需要做任何特殊处理,因此我给它提供了一个属性写入器而不是set访问器方法 ![http://atomoreilly.com/source/nostarch/images/860150.png]。

@description属性根本不需要任何特殊处理,所以我使用attr_readerattr_writer而不是访问器方法来获取和设置@description变量的值 ![http://atomoreilly.com/source/nostarch/images/860146.png]。

注意

它们是属性还是属性?不要被术语搞混。在 Ruby 中,属性相当于许多编程语言中称为属性的东西。

当你想同时读取和写入一个变量时,attr_accessor方法比同时使用attr_readerattr_writer提供更简短的替代方案。我就是这样在Treasure类中访问值属性的:

attr_accessor :value

这相当于以下内容:

attr_reader :value
attr_writer :value

我之前说过,使用符号调用attr_reader实际上创建了一个与符号同名变量。attr_accessor方法也这样做。

Thing类的代码中,这种行为并不明显,因为该类有一个initialize方法,它明确创建了变量。然而,Treasure类在其initialize方法中并没有引用@value变量:

class Treasure < Thing
    attr_accessor :value

    def initialize( aName, aDescription )
        super( aName, aDescription )
    end
end

@value存在唯一的指示是此访问器定义:

attr_accessor :value

我在accessors2.rb源文件底部的代码将每个Treasure对象的值设置为单独的操作,在创建对象本身之后进行,如下所示:

t1.value = 800

即使它从未被正式声明,@value变量确实存在,你可以使用get访问器t1.value来检索其数值。为了确保属性访问器确实创建了@value,你总是可以使用inspect方法查看对象内部。我在这个程序的最后两行代码中就是这样做的:

puts "This is treasure1: #{t1.inspect}"
puts "This is treasure2: #{t2.inspect}"

这显示了t1t2对象内部的数据,包括@value变量:

This is treasure1: #<Treasure:0x33a6c88 @value=100, @name="sword",
 @description="an Elvish weapon forged of gold (now somewhat tarnished)">
This is treasure2: #<Treasure:0x33a6c4c @value=500, @name="dragon horde",
 @description="a huge pile of jewels">

如果你向属性访问器发送一个由逗号分隔的符号列表,它可以同时初始化多个属性,如下所示:

accessors3.rb

attr_reader :name, :description
attr_writer(:name, :description)
attr_accessor(:value, :id, :owner)

如往常一样,括号内的参数是可选的,但在我看来(出于清晰度的原因),最好是使用括号。

现在我们来看看如何在冒险游戏中使用属性读取器和写入器。加载2adventure.rb程序。你会看到我在Thing类中创建了两个可读属性:namedescription。我还使description可写;然而,因为我没有计划更改任何Thing对象的名称,所以name属性是不可写的:

2adventure.rb

attr_reader( :name, :description )
attr_writer( :description )

我创建了一个名为to_s的方法,它返回一个描述Treasure对象的字符串。回想一下,所有 Ruby 类都有一个标准to_s方法。Thing.to_s方法覆盖(并替换)了默认的方法。

def to_s # override default to_s method
    return "(Thing.to_s):: The #{@name} Thing is #{@description}"
end

当你想实现适合特定类类型的新行为时,你可以覆盖现有方法。

调用超类的方法

2adventure.rb中的游戏将有两个从Thing派生的类:Treasure类和Room类。Treasure类添加了一个value属性,它可以被读取和写入。注意,它的initialize方法在初始化新的@value变量之前调用了其超类,以初始化namedescription属性:

super( aName, aDescription )
@value = aValue

如果我省略了对超类的调用,namedescription属性将永远不会被初始化。这是因为Treasure.initialize覆盖了Thing.initialize,所以当创建一个Treasure对象时,Thing.initialize中的代码将不会自动执行。

另一方面,也继承自ThingRoom类目前没有initialize方法,所以当创建一个新的Room对象时,Ruby 会向上搜索类层次结构以找到它。它找到的第一个initialize方法是在Thing中,因此Room对象的namedescription属性在那里被初始化。

类变量

在这个程序中还有一些其他有趣的事情正在发生。在Thing类的最顶部,你会看到这个:

@@num_things = 0

这个变量名@@num_things开头的两个@字符定义了这个变量为类变量。我们到目前为止在类内部使用的变量都是实例变量,前面有一个@,比如@name。而每个新对象(或实例)都会为其自己的实例变量分配自己的值,而所有从特定类派生的对象都共享相同的类变量。我已经将@@num_things变量赋值为 0,以确保它有一个有意义的初始值。

这里,@@num_things类变量被用来跟踪游戏中Thing对象的总数。它通过在每次创建新对象时在其initialize方法中增加类变量(通过给它加 1:+= 1)来实现这一点:

@@num_things += 1

如果你稍后查看代码,你会看到我创建了一个 Map 类来包含一个房间数组。这包括一个to_s方法的版本,它会打印数组中每个房间的信息。现在不必担心 Map 类的实现;我们将在第四章中查看数组和它们的方法。

class Map

    def initialize( someRooms )
        @rooms = someRooms
    end

    def to_s
        @rooms.each {
            |a_room|
            puts(a_room)
        }
    end

end

滚动到文件底部的代码,并运行程序以查看我是如何创建和初始化所有对象以及使用类变量@@num_things来记录已创建的所有 Thing 对象的数量。

类变量和实例变量

无标题图片

此图显示了包含一个类变量@@num_things和一个实例变量@name的 Thing 类(矩形)。三个椭圆形代表“Thing 对象”——即 Thing 类的实例。当这些对象中的任何一个将其实例变量@name赋值时,该值只会影响对象本身的@name变量。所以在这里,每个对象的@name值都不同。但是,当一个对象将值赋给类变量@@num_things时,该值“存在于”Thing 类中,并且被该类的所有实例共享。在这里,@@num_things等于 3,这对于所有 Thing 对象都是正确的。

深入挖掘

你创建的每个类都将从一个或多个其他类继承。在这里,我解释了 Ruby 类层次结构的基本原理。

超类

要了解super关键字的工作原理,请查看示例程序super.rb。这个程序包含五个相关的类。Thing 类是所有其他类的祖先,从 Thing 派生出 Thing2,从 Thing2 派生出 Thing3,从 Thing3 派生出 Thing4,从 Thing4 派生出 Thing5。

super.rb

class Thing
    def initialize( aName, aDescription )
        @name = aName
        @description = aDescription
        puts("Thing.initialize: #{self.inspect}\n\n")
    end

    def aMethod( aNewName )
        @name = aNewName
        puts("Thing.aMethod: #{self.inspect}\n\n")
    end
end

class Thing2 < Thing
    def initialize( aName, aDescription )
        super
        @fulldescription = "This is #{@name}, which is #{@description}"
        puts("Thing2.initialize: #{self.inspect}\n\n")
    end

    def aMethod( aNewName, aNewDescription )
        super( aNewName )
        puts("Thing2.aMethod: #{self.inspect}\n\n")
    end
end

class Thing3 < Thing2
    def initialize( aName, aDescription, aValue )
        super( aName, aDescription )
        @value = aValue
        puts("Thing3.initialize: #{self.inspect}\n\n")
    end

    def aMethod( aNewName, aNewDescription, aNewValue )
        super( aNewName, aNewDescription )
        @value = aNewValue
        puts("Thing3.aMethod: #{self.inspect}\n\n")
    end
end

class Thing4 < Thing3
    def aMethod
        puts("Thing4.aMethod: #{self.inspect}\n\n")
    end
end

class Thing5 < Thing4
end

让我们更仔细地看看这个层次结构中的前三个类:Thing 类有两个实例变量,@name@description。Thing2 还定义了@fulldescription(一个包含@name@description的字符串);Thing3 又添加了一个变量@value

这三个类各自包含一个initialize方法,用于在创建新对象时设置变量的值;它们各自还有一个名为aMethod的方法,该方法会更改一个或多个变量的值。派生类 Thing2 和 Thing3 在其方法中都使用了super关键字。

在这个代码单元的底部,我编写了一个“主循环”,当你运行程序时它会执行。不用担心这个循环的语法;你将在第五章 中学习关于循环的内容。我添加这个循环是为了让你能够轻松运行方法中包含的不同代码片段,从 test1test5。你可以在命令窗口中运行程序,当提示时输入一个数字,1 到 5,或者输入 Q 退出。当你第一次运行时,在提示符下输入 1 并按回车键。这将运行包含这两行代码的 test1 方法:

t = Thing.new( "A Thing", "a lovely thing full of thinginess" )
t.aMethod( "A New Thing" )

这里第一行创建并初始化了一个 Thing 对象,第二行调用它的 aMethod 方法。因为 Thing 类没有从特殊的东西继承,所以这里没有发生什么非常新或有趣的事情。事实上,与所有 Ruby 类一样,Thing 从 Object 类继承,Object 类是所有其他类的祖先(唯一的例外是 Ruby 1.9 中的 BasicObject 类,如本章后面所述)。输出使用 inspect 方法在调用 Thing.initializeThing.aMethod 方法时显示对象的内部结构。这是结果:

Thing.initialize: #<Thing:0x28e0290 @name="A Thing",
 @description="a lovely thing full of thinginess">
Thing.aMethod: #<Thing:0x28e0290 @name="A New Thing", @description="a
 lovely thing full of thinginess">

inspect 方法可以与所有对象一起使用,并且是一个非常有价值的调试辅助工具。在这里,它显示一个十六进制数,该数标识了特定的对象,后面跟着 @name@description 变量的字符串值。

现在在提示符下输入 2 来运行 test2,它包含以下代码:

t2 = Thing2.new( "A Thing2", "a Thing2 thing of great beauty" )
t2.aMethod( "A New Thing2", "a new Thing2 description" )

这创建了一个 Thing2 对象 t2,并调用 t2.aMethod。仔细观察输出。你会看到,尽管 t2 是一个 Thing2 对象,但首先调用的是 Thing 类的 initialize 方法。然后才是 Thing2 类的 initialize 被调用。

Thing.initialize: #<Thing2:0x2a410a0 @name="A Thing2",
 @description="a Thing2 thing of great beauty">

Thing2.initialize: #<Thing2:0x2a410a0 @name="A Thing2", @description="a Thing2
 thing of great beauty", @fulldescription="This is A Thing2, which is a Thing2
 thing of great beauty">

要理解为什么是这样,请查看 Thing2 类的 initialize 方法的代码:

def initialize( aName, aDescription )
   super
   @fulldescription = "This is #{@name}, which is #{@description}"
   puts("Thing2.initialize: #{self.inspect}\n\n")
end

这使用 super 关键字来调用 Thing2 的祖先或超类的 initialize 方法。如你所见,Thing2 的超类是 Thing:

class Thing2 < Thing

在 Ruby 中,当单独使用 super 关键字(即不带任何参数)时,它会将当前方法(此处为 Thing2.initialize)的所有参数传递给其超类中具有相同名称的方法(此处为 Thing.initialize)。或者,你可以在 super 后面显式指定一个参数列表。因此,在这种情况下,以下代码会产生相同的效果:

super( aName, aDescription )

虽然单独使用 super 关键字是允许的,但为了清晰起见,通常最好明确指定传递给超类的参数列表。如果你只想传递当前方法收到的有限数量的参数,则需要一个显式的参数列表。例如,Thing2 的 aMethod 方法只将 aName 参数传递给其超类 Thing1 的 initialize 方法:

super( aNewName )

这解释了为什么在调用 Thing2.aMethod 时,@description 变量不会被改变。

现在如果你查看 Thing3,你会看到这增加了一个变量,@value。在其 initialize 方法的实现中,它将两个参数,aNameaDescription,传递给其超类 Thing2。然后,正如你已经看到的,Thing2 的 initialize 方法将这些相同的参数传递给其超类的 initialize 方法,即 Thing。

当程序运行时,在提示符处输入 3 以查看输出。以下代码将被执行:

t3 = Thing3.new("A Thing3", "a Thing3 full of Thing and Thing2iness",500)
t3.aMethod( "A New Thing3", "and a new Thing3 description",1000)

注意执行流程是如何直接沿着层次结构向上,使得 Thing 的 initializeaMethod 方法中的代码在 Thing2 和 Thing3 中匹配的方法之前执行。

没有必要像我在前面的例子中所做的那样覆盖超类的方法。这只有在你想添加一些新行为时才是必需的。Thing4 省略了 initialize 方法,但实现了 aMethod 方法。

在提示符处输入 4 以执行以下代码:

t4 = Thing4.new( "A Thing4", "the nicest Thing4 you will ever see", 10 )
t4.aMethod

当你运行它时,请注意,当创建 Thing4 对象时,将调用第一个可用的 initialize 方法。这恰好是 Thing3.initialize,它再次调用其祖先类 Thing2 和 Thing 的 initialize 方法。然而,Thing4 实现的 aMethod 方法没有调用其超类,所以它立即执行,并且忽略祖先类中任何其他 aMethod 方法中的代码:

def aMethod
    puts("Thing4.aMethod: #{self.inspect}\n\n")
end

最后,Thing5 从 Thing4 继承而来,没有引入任何新的数据或方法。在提示符处输入 5 以执行以下操作:

t5 = Thing5.new( "A Thing5", "a very simple Thing5", 40 )
t5.aMethod

这次,你会看到对 new 的调用导致 Ruby 回溯到类层次结构,直到找到第一个 initialize 方法。这个方法恰好属于 Thing3(它也调用了 Thing2 和 Thing 的 initialize 方法)。然而,aMethod 的第一个实现发生在 Thing4 中,并且没有调用 super,所以追踪就结束了。

所有类的根源

如我之前提到的,我们所有的 Ruby 类最终都将从 Object 类派生。你可以把 Object 视为 Ruby 层次结构的“根”或“基础”类。在 Ruby 1.8 中,这确实是真实的——没有类是从 Object 本身派生的。然而,在 Ruby 1.9 中,Object 是从一个新的类 BasicObject 派生的。这个新类是为了给程序员提供一个非常轻量级的类——它只提供创建对象、测试相等性和操作称为 单例 的特殊方法所需的最基本的方法。(我将在第七章第七章中更多地讨论单例。)

Ruby 1.9 的 Object 类从 BasicObject 继承方法,并添加了一些自己的新方法。BasicObject 在 Ruby 1.8 中不存在,Object 类提供了 Ruby 1.9 中 BasicObject 和 Object 组合提供的所有方法。由于所有正常的 Ruby 类(包括 Ruby 1.8 和 Ruby 1.9)都从 Object 继承,您通常可以将 Object 视为所有其他类的“根”。只需记住,在 Ruby 1.9 中,所有类的最终祖先都是 BasicObject。

根类本身没有超类,任何尝试定位其超类的操作都将返回 nil。您可以通过运行 superclasses.rb 来亲自查看这一点。这个脚本调用 superclass 方法从 Three 类向上遍历到 Object 或 BasicObject 类。在循环的每次迭代中,变量 x 被分配给 x 的直接父类的类,直到 x 等于 nil。在这里,classsuperclass 是返回 Ruby 类引用的方法,而不是从这些类创建的对象。begin..until 块是 Ruby 的循环结构之一,您将在第五章中更详细地了解它。第五章。

superclasses.rb

class One
end

class Two < One
end

class Three < Two
end

# Create ob as instance of class Three
# and display the class name
ob = Three.new
x = ob.class
puts( x )

# now climb back through the hierarchy to
# display all ancestor classes of ob
begin
    x = x.superclass
    puts(x.inspect)
end until x == nil

之前的代码显示了以下输出:

Three
Two
One
Object
BasicObject    # Ruby 1.9 only!
nil

类内部的常量

有时候,您可能需要访问在类内部声明的常量(以大写字母开头的标识符,用于存储不变的值)。假设您有这个类:

classconsts.rb

class X
   A = 10

   class Y
   end
end

要访问常量 A,您需要使用特殊的范围解析运算符 ::,如下所示:

X::A

类名是常量,因此这个运算符同样可以让你访问其他类内部的类。这使得从“嵌套”类(如类 Y 在类 X 内部)创建对象成为可能:

ob = X::Y.new

部分类

在 Ruby 中,定义一个类并不一定要在一个地方完成。如果您愿意,可以在程序的各个部分定义单个类。当一个类从特定的超类继承时,每个后续的部分(或 开放)类定义可以可选地使用 < 运算符在其定义中重复超类。

在这里,我创建了一个类 A,以及从它继承的另一个类 B:

partial_classes.rb

class A
   def a
      puts( "a" )
   end
end

class B < A
   def ba1
      puts( "ba1" )
   end
end

class A
   def b
      puts( "b" )
   end
end

class B < A
   def ba2
      puts( "ba2" )
   end
end

现在,如果创建一个 B 对象,它将可以使用 A 和 B 的所有方法:

ob = B.new
ob.a
ob.b
ob.ba1
ob.ba2

您还可以使用部分类定义来向 Ruby 的标准类(如 Array)添加功能:

class Array
   def gribbit
      puts( "gribbit" )
   end
end

这将 gribbit 方法添加到 Array 类中,因此现在可以执行以下代码:

[1,2,3].gribbit

第三章:字符串和范围

无标题图片

我在许多程序中使用了字符串。事实上,书中第一个程序就展示了字符串。这里再次展示:

puts 'hello world'

虽然第一个程序使用了单引号内的字符串,但我的第二个程序使用了双引号字符串:

print('Enter your name: ' )
name = gets()
puts( "Hello #{name}" )

双引号字符串比单引号字符串做的工作更多。特别是,它们能够将自身的一部分当作编程代码来评估。要评估某个内容,你需要将其放置在以井号(#)开头的一对花括号之间。

在前面的例子中,双引号字符串中的 #{name} 告诉 Ruby 获取 name 变量的值并将其插入到字符串本身中。代码的第二行调用 gets() 方法获取一些用户输入,然后将其分配给变量 name。如果用户输入了 Fred,代码的最后一行将评估嵌入的变量 #{name},并显示字符串“Hello Fred”。1strings.rb 示例程序提供了双引号字符串中嵌入评估的更多示例。例如,这里我从一个自定义类 MyClass 创建了一个对象 ob,并使用嵌入评估来显示其 namenumber 属性的值:

1strings.rb

class MyClass
    attr_accessor :name
    attr_accessor :number

    def initialize( aName, aNumber )
        @name    = aName
        @number = aNumber
    end

    def ten
        return 10
    end

end

ob = MyClass.new( "James Bond", "007" )
puts( "My name is #{ob.name} and my number is #{ob.number}" )

当代码的最后一行执行时,会显示以下内容:

My name is James Bond and my number is 007

双引号字符串还可以评估表达式,如 2*3,代码片段,如方法调用 ob.ten(其中 ten 是方法名),以及转义字符,如 \n\t(代表换行符和制表符)。单引号字符串不进行此类评估。然而,单引号字符串可以使用反斜杠来指示下一个字符应该被原样使用。当单引号字符串包含单引号字符时,这很有用,如下所示:

'It\'s my party'

假设名为 ten 的方法返回值 10,你可能编写以下代码:

puts( "A tab\ta new line\na calculation #{2*3} and method-call #{ob.ten}" )

因为这是一个双引号字符串,所以嵌入的元素会被评估,并显示以下内容:

A tab        new line
calculation 6 and method-call 10

现在让我们看看使用单引号字符串会发生什么:

puts( 'A tab\tnew line\na calculation #{2*3} and method-call #{ob.ten}' )

这次没有进行嵌入评估,所以显示的内容如下:

A tab\tnew line\ncalculation #{2*3} and method-call #{ob.ten}

用户定义的字符串定界符

如果出于某种原因,单引号和双引号不方便——例如,如果你的字符串中包含很多引号字符,你不想在它们前面不断放置反斜杠——你也可以用许多其他方式来定界字符串。

双引号字符串的标准替代定界符是 %Q/%//,而单引号字符串的定界符是 %q/。因此,……

2strings.rb

%Q/This is the same as a double-quoted string./
%/This is also the same as a double-quoted string./
%q/And this is the same as a single-quoted string/

你甚至可以定义自己的字符串定界符。它们必须是非字母数字字符,并且可以包括非打印字符,如换行符或制表符,以及通常在 Ruby 中具有特殊意义的各种字符,如井号 (#)。你应该在 %q%Q 后放置你选择的字符,并且确保用相同的字符终止字符串。如果你的定界符是一个开方括号,那么在字符串末尾应该使用相应的闭方括号,如下所示:

3strings.rb

%Q[This is a string]

你可以在示例程序 3strings.rb 中找到各种用户选择的字符串定界符的例子。这里有两个例子,使用 %Q 后跟一个星号 (*) 而不是双引号字符串,以及使用 %q 后跟一个感叹号 (!) 而不是单引号字符串:

puts( %Q*a)Here's a tab\ta new line\na calculation using \*
 #{2*3} and a method-call #{ob.ten}* )
puts( %q!b)Here's a tab\ta new line\na calculation using \* #{2*3} and a
 method-call #{ob.ten}! )

在这里,就像上一个程序一样,ob 是一个用户定义的对象,其名为 ten 的方法返回整数 10。前面的代码产生了以下输出:

a)Here's a tab    a new line
a calculation using * 6 and a method-call 10
b)Here's a tab\ta new line\na calculation using \* #{2*3} and a method-call #{ob.ten}

虽然在某些情况下,使用一些晦涩的字符(如换行符或星号)来界定字符串可能是有用的,但在许多情况下,这种古怪做法带来的不利(包括心理痛苦和困惑)可能会大大超过其优势。

反引号

另一种值得特别提及的字符串类型是:由反引号包围的字符串——即通常藏在键盘右上角向内指的引号字符:`.

Ruby 将任何由反引号包围的内容视为可以由操作系统通过 printputs 等方法执行的命令。到现在为止,你可能已经猜到 Ruby 提供了不止一种方法来做这件事。结果是 %x/some command/`somecommand` 以及 %x{some command} 有相同的效果。例如,在 Windows 操作系统中,下面显示的三行中的每一行都会将命令 dir 传递给操作系统,导致目录列表被显示:

4backquotes.rb

puts(`dir`)
puts(%x/dir/)
puts(%x{dir})

你也可以像这样在双引号字符串中嵌入命令:

print( "Goodbye #{%x{calc}}" )

如果这样做,请小心。命令本身首先会被评估。然后你的 Ruby 程序会等待启动的进程结束。在这个例子中,计算器会弹出。现在你可以自由地进行一些计算,如果你愿意的话。只有当你关闭计算器时,才会显示字符串“再见”。

字符串处理

在离开字符串主题之前,你将快速浏览一些常见的字符串操作。

连接

你可以使用 <<+ 或只是通过在它们之间放置空格来连接字符串。以下是三个字符串连接的例子;在每种情况下,s 被分配了字符串“Hello world”:

hello_world_concat.rb

s = "Hello " << "world"
s = "Hello " + "world"
s = "Hello "  "world"

注意,当你使用 << 方法时,你可以追加范围在 0 到 255 之间的 Fixnum 整数,在这种情况下,这些整数会被转换成具有该字符码的字符。字符码 65 到 90 被转换成大写字母 AZ,97 到 122 被转换成小写字母 az,其他码被转换成标点符号、特殊字符和非打印字符。然而,如果你想打印数字本身,你必须使用 to_s 方法将其转换为字符串。当使用 + 方法或空格连接 Fixnums 时,to_s 方法是强制性的;不使用 to_s 尝试连接一个数字会导致错误。以下程序打印出介于 0 到 126 之间的值对应的字符和数字码,这些值包括标准的西方字母数字和标点符号:

char_codes.rb

i = 0
begin
    s = "[" << i << ":" << i.to_s << "]"
    puts(s)
    i += 1
end until i == 126

关于使用 <<+ 或空格进行连接的示例,请参阅 string_contact.rb

string_contact.rb

s1 = "This " << "is" << " a string " << 36 # char 36 is '$'
s2 = "This "  + "is" + " a string "  + 36.to_s
s3 = "This "  "is"  " a string "  + 36.to_s

puts("(s1):" << s1)
puts("(s2):" << s2)
puts("(s3):" << s3)

前一个程序产生了以下输出:

(s1):This is a string $
(s2):This is a string 36
(s3):This is a string 36

关于逗号的问题?

你有时可能会看到使用逗号分隔字符串和其他数据类型的 Ruby 代码。在某些情况下,这些逗号似乎具有连接字符串的效果。例如,以下代码乍一看可能似乎创建并显示了一个由三个子字符串和一个整数组成的字符串:

s4 = "This " , "is" , " not a string!", 10
print("print (s4):" , s4, "\n")

事实上,由逗号分隔的列表创建了一个数组——原始字符串的有序列表。string_concat.rb 程序包含了一些示例,证明了这一点:

x = "This " , "is" , " not a string!", 36
print("print (x):" , x, "\n")
puts("puts(x):", x)
puts("puts x.class is: " << (x.class).to_s )

print("print(x):" , x, "\n")
puts("puts(x):", x)
puts("puts x.class is: " << (x.class).to_s )

前一个代码导致以下内容被显示:

print (x):This is not a string!36
puts(x):
This
is
 not a string!
36
puts x.class is: Array

这里的第一个 print 语句看起来像是在显示一个单独的字符串。这是因为数组 x 中的每个后续项都与前一项在同一行上打印。当你使用 puts 而不是 print 时,你可以看到每个项都在单独的一行上打印。这是因为 puts 会依次打印每个项,并在其后添加一个换行符。当你要求 Ruby 打印 x 对象的类时,可以确认你处理的是一个数组而不是字符串。它显示为 Array。你将在下一章中更深入地了解数组。

字符串赋值

Ruby 字符串类提供了一些有用的字符串处理方法。这些方法中的大多数都会创建新的字符串对象。因此,例如,在以下代码中,第二行赋值左侧的 s 与右侧的 s 不是同一个对象:

s = "hello world"
s = s + "!"

一些字符串方法实际上会修改字符串本身而不创建新对象。这些方法通常以感叹号结尾(例如,capitalize! 方法会改变原始字符串,而 capitalize 方法则不会)。此外,当你将一个字符赋值给字符串的索引时,字符串本身也会被修改——不会创建新的字符串。例如,s[1] = 'A' 会将字符 A 放置在字符串 s 的索引 1(第二个字符)处。

如果有疑问,你可以使用 object_id 方法来检查对象的身份。我在 string_assign.rb 程序中提供了一些操作示例,这些操作会创建新的字符串,以及不会创建新的字符串。运行此代码,并在每次字符串操作完成后检查 sobject_id

string_assign.rb

s = "hello world"
print( "1) s='#{s}' and s.object_id=#{s.object_id}\n" )
s = s + "!"            # this creates a new string object
print( "2) s='#{s}' and s.object_id=#{s.object_id}\n" )
s = s.capitalize       # this creates a new string object
print( "3) s='#{s}' and s.object_id=#{s.object_id}\n" )
s.capitalize!          # but this modifies the original string object
print( "4) s='#{s}' and s.object_id=#{s.object_id}\n" )
s[1] = 'A'             # this also modifies the original string object
print( "5) s='#{s}' and s.object_id=#{s.object_id}\n" )

这会产生类似于下面显示的输出。实际的对象 ID 值可能不同,但重要的是要注意,连续的值保持不变,表明字符串对象 s 保持不变,而当它们改变时,表明已创建了一个新的字符串对象 s

1) s='hello world' and s.object_id=29573230
2) s='hello world!' and s.object_id=29573190
3) s='Hello world!' and s.object_id=29573160
4) s='Hello world!' and s.object_id=29573160
5) s='HAllo world!' and s.object_id=29573160

字符串索引

在之前的某个示例中,我将字符串视为字符数组,并在方括号内指定一个整数作为字符索引:s[1]。在 Ruby 中,字符串和数组都是从索引 0 的第一个字符开始计数的。所以,例如,要在字符串 s(当前包含“Hello world”)中将字符 e 替换为 u,你应该将新字符赋值给索引 1:

s[1] = 'u'

如果你按索引访问字符串以查找特定位置的字符,其行为会根据你使用的 Ruby 版本而有所不同。Ruby 1.8 返回字符的 ASCII 码的数值,而 Ruby 1.9 返回字符本身。

s = "Hello world"
puts( s[1] )    #=> Ruby 1.8 displays 101; Ruby 1.9 displays 'e'

要从 Ruby 1.8 返回的数值中获取实际的字符,你可以使用双索引来打印单个字符,从索引 1 开始:

s = "Hello world"
puts( s[1,1] ) # prints out 'e'

另一方面,如果你需要 Ruby 1.9 返回的字符的数值,你可以像这样使用 ord 方法:

puts( s[1].ord)

Ruby 1.8 中不存在 ord 方法,因此之前的代码会导致“未定义方法”错误。为了确保 Ruby 1.8 和 1.9 之间的兼容性,你应该使用双索引技术,其中第一个索引表示起始位置,第二个索引表示字符数。例如,这将返回位置 1 处的一个字符:s[1,1]。你可以在 char_in_string.rb 程序中看到更多示例:

char_in_string.rb

s = "Hello world"
puts( s[1] )
achar=s[1]
puts( achar )
puts( s[1,1] )
puts( achar.ord )

当你运行此代码时,Ruby 1.9 显示的是:

e
e
e
101

而 Ruby 1.8 显示的是:

101
101
e
undefined method `ord' for 101:Fixnum (NoMethodError)

你也可以使用双索引来返回多个字符。如果你想从位置 1 开始返回三个字符,你应该输入这个:

puts( s[1,3] )     # prints 'ell'

这告诉 Ruby 从位置 1 开始,并返回接下来的三个字符。或者,你也可以使用两个点范围表示法:

puts( s[1..3] )     # also prints 'ell'

注意

范围将在本章后面更详细地讨论。

字符串也可以使用负值进行索引,在这种情况下,-1 是最后一个字符的索引,并且,同样,你可以指定要返回的字符数:

string_index.rb

puts( s[-1,1] )     # prints 'd'
puts( s[-5,5] )     # prints 'world'

当使用负索引指定范围时,必须同时为起始索引和结束索引使用负值:

string_methods.rb

puts( s[-5..5] )    # this prints an empty string!
puts( s[-5..-1] )   # prints 'world'

最后,你可能想尝试一些用于操作字符串的标准方法。这些方法包括改变字符串的大小写、反转字符串、插入子字符串、删除重复字符等。我在 string_methods.rb 中提供了一些示例。方法名通常描述了它们的功能。然而,请注意,像 reverse(结尾没有 !)这样的方法返回一个新的字符串,但不会修改原始字符串,而 reverse!(结尾有 !)则会修改原始字符串。你之前也看到了 capitalizecapitalize! 方法有类似的行为。

insert 方法接受两个参数,一个索引和一个字符串,它会在字符串 s 的指定索引处插入字符串参数。squeeze 方法返回一个移除了任何重复字符的字符串,例如在 “Hello” 中的第二个相邻 lsplit 方法将字符串分割成一个数组。当我讨论第六章(条件语句)中的正则表达式时,我会更多地讨论 split。以下示例假设 s 是字符串 “Hello world”,输出显示在 #=> 注释中。在本书代码存档提供的程序中,你也可以使用更长的字符串进行实验:

s.length            #=> 11
s.reverse!          #=> Hello world
s.reverse           #=> dlrow olleH
s.upcase            #=> HELLO WORLD
s.capitalize        #=> Hello world
s.swapcase          #=> hELLO WORLD
s.downcase          #=> hello world
s.insert(7,"NOT ")  #=> hello wNOT orld
s.squeeze           #=> helo wNOT orld
s.split             #=> ["helo", "wNOT", "orld"]

移除换行符:chopchomp

几个实用的字符串处理方法值得特别提及。chopchomp 方法可以用来从字符串末尾移除字符。chop 方法返回一个移除了最后一个字符的字符串,或者如果字符串末尾有回车换行符(\r\n),则移除这些字符。chomp 方法返回一个移除了终止的回车换行符(或两者都移除,如果两者都存在)的字符串。

这些方法在你需要移除用户输入的换行符或从文件中读取的换行符时很有用。例如,当你使用 gets 读取一行文本时,这会返回包括终止的 记录分隔符 的行,默认情况下,这是换行符。

记录分隔符:$/

Ruby 预定义了一个变量,$/,作为记录分隔符。这个变量被 getschomp 等方法使用。gets 方法读取一个字符串,直到并包括记录分隔符。chomp 方法返回一个字符串,从末尾移除记录分隔符(如果存在),否则返回未修改的原始字符串。如果你想重新定义记录分隔符,可以这样做:

$/= "*"        # the "*" character is now the record separator

当你重新定义记录分隔符时,这个新的字符(或字符串)现在将被 getschomp 等方法使用。以下是一个示例:

$/= "world"
s = gets()     # user enters "Had we but world enough and time..."
puts( s )      # displays "Had we but world"

record_separator.rb

您可以使用chopchomp来删除换行符。在大多数情况下,chomp更可取,因为它不会删除最后一个字符,除非它是记录分隔符(通常是换行符),而chop将删除最后一个字符,无论它是什么。以下是一些示例:

chop_chomp.rb

# NOTE: s1 includes a carriage return and linefeed
s1 = "Hello world
"
s2 = "Hello world"
s1.chop           # returns "Hello world"
s1.chomp          # returns "Hello world"
s2.chop           # returns "Hello worl" - note the missing 'd'!
s2.chomp          # returns "Hello world"

chomp方法还允许您指定用作分隔符的字符或字符串:

s2.chomp('rld')   # returns "Hello wo"

格式化字符串

Ruby 提供了printf方法来打印包含以百分号(%)开头的指定符的“格式化字符串”。格式化字符串后面可以跟一个或多个由逗号分隔的数据项;数据项的列表应与格式指定符的数量和类型相匹配。实际数据项将替换字符串中的匹配指定符,并相应地进行格式化。以下是一些常见的格式指定符:

%d - decimal number
%f - floating-point number
%o - octal number
%p - inspect object
%s - string
%x - hexadecimal number

您可以通过在浮点格式指定符%f之前放置点数来控制浮点精度。例如,这将显示浮点值到六位数字(默认值)后跟一个回车符("\n"):

string_printf.rb

printf( "%f\n", 10.12945 )        #=> 10.129450

以下将显示浮点值到两位小数("%0.02f")。是否在浮点指定符前包含前导 0 纯粹是风格上的偏好,"%0.2f"是等效的。

printf( "%0.02f\n", 10.12945 )     #=> 10.13

这里有一些更多的示例:

printf("d=%d f=%f o=%o x=%x s=%s\n", 10, 10, 10, 10, 10)

这将输出d=10 f=10.000000 o=12 x=a s=10

printf("0.04f=%0.04f : 0.02f=%0.02f\n", 10.12945, 10.12945)

这将输出0.04f=10.1295 : 0.02f=10.13

范围

在 Ruby 中,范围是一个类,它表示由起始值和结束值定义的值集。通常范围使用整数定义,但它也可以使用其他有序值定义,例如浮点数或字符。值可以是负数,但您应该小心,确保您的起始值低于您的结束值!

这里有一些示例:

ranges.rb

a = (1..10)
b = (-10..-1)
c = (-10..10)
d = ('a'..'z')

您也可以使用三个点而不是两个点来指定范围;这将创建一个省略最后一个值的范围:

d = ('a'..'z')         # this two-dot range = 'a'..'z'
e = ('a'...'z')        # this three-dot range = 'a'..'y'

您可以使用to_a方法创建由范围定义的值的数组,如下所示:

(1..10).to_a

注意,to_a方法对于浮点数没有定义,简单的理由是两个浮点数之间可能值的数量不是有限的。

字符串范围

您甚至可以创建字符串范围——尽管这样做需要格外小心,因为您可能会得到比预期更多的结果。例如,看看您是否能找出这个范围指定的值:

str_range.rb

str_range = ('abc'..'def')

初看起来,从'abc''def'的范围可能看起来不多。事实上,这定义了一个不少于 2,110 个值的范围!它们按以下顺序排列:abcabdabe,等等,直到as 的末尾;然后您开始于bs:baababbac,等等。简而言之,这种类型的范围可能相当罕见,最好非常谨慎或根本不使用。

使用范围迭代

你可以使用范围从起始值迭代到结束值。例如,以下是一种打印从 1 到 10 的所有数字的方法:

for_to.rb

for i in (1..10) do
    puts( i )
end

深入挖掘

在这里,你将学习如何创建和迭代范围,使用 heredoc 编写多行字符串,以及定义你自己的字符串定界符。

Heredocs

虽然你可以在单引号或双引号之间写多行字符串,但许多 Ruby 程序员更喜欢使用一种称为heredoc的字符串类型。heredoc 是一块文本,它首先指定一个结束标记,这只是一个你选择的标识符。在这里,我指定EODOC作为结束标记:

heredoc.rb

hdoc1 = <<EODOC

这告诉 Ruby,从上一行之后的任何内容都是一个单独的字符串,直到遇到结束标记时结束。这个字符串被分配给变量hdoc1。以下是一个完整的 heredoc 分配示例:

hdoc1 = <<EODOC
I wandered lonely as a #{"cloud".upcase},
That floats on high o'er vale and hill...
EODOC

默认情况下,heredoc 被视为双引号字符串,所以像#{"cloud".upcase}这样的表达式将被评估。如果你想将 heredoc 视为单引号字符串,请在单引号中指定其结束标记:

hdoc2 = <<'EODOC'
I wandered lonely as a #{"cloud".upcase},
That floats on high o'er vale and hill...
EODOC

默认情况下,heredoc 的结束标记必须与左边缘对齐。如果你想缩进它,你应该在分配结束标记时使用<<-而不是<<

hdoc3 = <<-EODOC
I wandered lonely as a #{"cloud".upcase},
That floats on high o'er vale and hill...
    EODOC

选择合适的结束标记取决于你。甚至使用保留词也是合法的(尽管可能不是特别明智!):

hdoc4 = <<def
I wandered lonely as a #{"cloud".upcase},
That floats on high o'er vale and hill...
def

被分配给 heredoc 的变量可以像任何其他字符串变量一样使用:

puts( hdoc1 )

字符串字面量

如本章前面所述,你可以选择使用%q//来定界单引号字符串,或者使用%Q//%//来定界双引号字符串。

Ruby 提供了类似的定界方法,用于定界反引号字符串、正则表达式、符号,以及单引号或双引号字符串的数组。以这种方式定义字符串数组的能力特别有用,因为它避免了为每个项目输入字符串定界符的需要。以下是这些字符串字面量定界符的参考:

%q/    /    # single-quoted
%Q/    /    # double-quoted
%/     /    # double-quoted
%w/    /    # array
%W/    /    # array double-quoted
%r|    |    # regular expression
%s/    /    # symbol
%x/    /    # operating system command

注意,你可以选择使用哪些定界符。我除了在正则表达式中使用了|(因为/是“正常”的正则表达式定界符)之外,还使用了斜杠/,星号*,和与号&,或者其他符号(例如,%W*dog cat #{1+2}*%s&dog&)。以下是一些这些字面量在使用的示例:

literals.rb

p %q/dog cat #{1+2}/        #=> "dog cat \#{1+2}"
p %Q/dog cat #{1+2}/        #=> "dog cat 3"
p %/dog cat #{1+2}/         #=> "dog cat 3"
p %w/dog cat #{1+2}/        #=> ["dog", "cat", "\#{1+2}"]
p %W/dog cat #{1+2}/        #=> ["dog", "cat", "3"]
p %r|^[a-z]*$|              #=> /^[a-z]*$/
p %s/dog/                   #=> :dog
p %x/vol/                   #=> " Volume in drive C is OS [etc...]"

第四章:数组和哈希

无标题图片

到目前为止,你通常一次只使用一个对象。在本章中,你将了解如何创建一个对象列表。你将从查看最常见的列表结构类型:数组开始。

数组

一个数组是一个按顺序排列的项目集合,其中每个项目都可以通过索引来访问。在 Ruby(与许多其他语言不同),一个数组可以包含混合数据类型的项目,例如字符串、整数和浮点数,甚至是一个返回某些值的函数调用。例如,这里显示的 a1 的最后一个元素调用了我的方法 array_length,它返回数组的长度 a0

array0.rb

def array_length( anArray )
    return anArray.length
end

a0 = [1,2,3,4,5]
a1 = [1,'two', 3.0, array_length( a0 ) ]
p( a1 )        #=>[1, "two", 3.0, 5]

数组中的第一个项目具有索引 0,这意味着最后一个项目的索引等于数组中项目总数减 1。给定前面显示的数组 a1,这是获取第一个和最后一个项目值的方法:

a1[0]        # returns 1st item (at index 0)
a1[3]        # returns 4th item (at index 3)

你已经使用过数组几次——例如,在 2adventure.rb 中,你使用数组存储 Room 对象的映射:

mymap = Map.new([room1,room2,room3])

创建数组

与许多其他编程语言一样,Ruby 使用方括号来界定数组。你可以轻松地创建一个数组,用一些以逗号分隔的值填充它,并将其赋给一个变量:

arr = ['one','two','three','four']

就像 Ruby 中的大多数其他事物一样,数组是对象。它们由 Array 类定义,就像字符串一样,它们从 0 开始索引。你可以通过放置其索引在方括号中来引用数组中的项目。如果索引无效,则返回 nil

array1.rb

arr = ['a', 'b', 'c']
puts(arr[0])      # shows 'a'
puts(arr[1])      # shows 'b'
puts(arr[2])      # shows 'c'
puts(arr[3])      # nil

显示 nil

当你尝试使用 printputs 显示 nil 值时,Ruby 1.8 显示 nil,而 Ruby 1.9 显示一个空字符串。如果你想确保显示 nil 的字符串表示,请使用 pinspect 方法而不是 print。你也可以显示它的类(nilNilClass 的实例)或使用 nil? 方法来测试它是否为 nil

puts(arr[3].inspect)     #=> nil
puts(arr[3].class)       #=> NilClass
p(arr[3])                #=> nil
puts(arr[3].nil?)        #=> true

array1.rb

一个数组可能包含产生值的表达式。假设你已经创建了以下方法:

array2.rb

def hello
  return "hello world"
end

你现在可以声明这个数组:

x = [1+2, hello, `dir`]

在这里,第一个元素是一个产生整数 3 的数学表达式,第二个是字符串 “hello world”(由 hello 方法返回)。如果你在 Windows 上运行它,第三个数组元素将是一个包含目录列表的字符串。这是因为 `dir` 是一个反引号字符串,它由操作系统执行(见 第三章)。因此,数组的最后一个“槽位”被 dir 命令返回的值填充,它恰好是一系列文件名。如果你在不同的操作系统上运行,你可能需要在此处替换一个适当的命令。(例如,如果你在类 Unix 操作系统上运行,你可以用 `ls` 替换以获取类似的一串文件名。)

创建文件名数组

Ruby 的一些类有返回值数组的函数。例如,Dir类,用于在磁盘目录上执行操作,有entries方法。将目录名传递给该方法,它将返回一个包含文件的数组:

Dir.entries( 'C:\\' )  # returns an array of files in C:\

dir_array.rb

如果你想要创建一个单引号字符串的数组,但又懒得输入所有的引号,可以使用在%w(或使用大写的%W来表示双引号字符串,如第三章中解释的)前带有括号的空格分隔的未引用文本的快捷方式。

array2.rb

y = %w( this is an array of strings )

以下代码将显示在变量y旁边的数组分配:

["this", "is", "an", "array", "of", "strings"]

你也可以使用通常的对象构造方法new来创建数组。可选地,你可以向new传递一个整数来创建一个特定大小的空数组(每个元素设置为nil),或者你可以传递两个参数:第一个用于设置数组的大小,第二个用于指定要放置在数组每个索引处的元素,如下所示:

a = Array.new                     # an empty array
a = Array.new(2)                  # [nil,nil]
a = Array.new(2,"hello world")    # ["hello world","hello world"]

多维数组

要创建一个多维数组,你可以创建一个数组,然后将其“槽位”中添加其他数组。例如,这创建了一个包含两个元素的数组,每个元素本身都是一个包含两个元素的数组:

a = Array.new(2)
a[0]= Array.new(2,'hello')
a[1]= Array.new(2,'world')

注意

你还可以通过将数组作为参数传递给new方法来创建一个数组对象。但请注意:如果你在new方法和开方括号之间没有留空格,Ruby 会将其视为语法错误。换句话说,这有效:a = Array.new [1,2,3]。然而,这不行:a = Array.new[1,2,3]。但是使用括号总是有效的,无论你放空格的位置在哪里:a = Array.new([1,2,3])

你也可以使用方括号在数组内部嵌套数组。这创建了一个包含四个数组的数组,每个数组都包含四个整数:

a = [    [1,2,3,4],
    [5,6,7,8],
    [9,10,11,12],
    [13,14,15,16]  ]

在之前的代码中,我将四个“子数组”放在了不同的行上。这不是强制性的,但它确实有助于通过将每个子数组显示为类似电子表格中的行来澄清多维数组的结构。当谈论数组中的数组时,将每个嵌套数组称为“外部”数组的一行是很方便的。

为了查看使用多维数组的更多示例,请加载multi_array.rb程序。该程序首先创建一个包含两个其他数组的数组multiarr。这些数组中的第一个位于multiarr的索引 0 处,第二个位于索引 1 处:

multi_array.rb

multiarr = [['one','two','three','four'],[1,2,3,4]]

接下来,你需要找到一种方法来定位数组中的单个元素,这些数组本身又包含在其他数组中。你将在下一节中考虑这个问题。

遍历数组

您可以通过使用for循环遍历数组元素来访问数组元素。在许多编程语言中,for循环从起始数字(例如 0)开始计数,遍历固定数量的元素,直到结束数字(例如 10),并在每次循环迭代中递增计数器变量(例如i)。因此,在其他语言中,您可能习惯于编写类似这样的循环:for i = 1 to 10

在 Ruby 中,正常的for循环遍历集合中的所有项目,可能被称为for..in循环。其计数器变量在每次循环迭代中逐个分配给集合中的每个对象。其语法可以总结为for anObject in aCollection,在每次循环迭代中,变量anObject从集合aCollection中分配一个新的项目,直到没有更多项目为止。下面显示的循环遍历两个元素,即索引 0 和 1 的两个子数组:

for i in multiarr
    puts(i.inspect)
end

这将显示以下内容:

["one", "two", "three", "four"]
[1, 2, 3, 4]

那么,您如何遍历两个子数组中的项目(字符串和整数)?如果有固定数量的项目,您可以为每个指定不同的迭代器变量,在这种情况下,每个变量将分配与匹配数组索引的值。

这里您有四个子数组槽位,因此您可以使用四个变量,如下所示:

for (a,b,c,d) in multiarr
    print("a=#{a}, b=#{b}, c=#{c}, d=#{d}\n" )
end

您还可以使用for循环逐个遍历每个子数组中的所有项目:

multi_array2.rb

for s in multiarr[0]
   puts(s)
end
for s in multiarr[1]
   puts(s)
end

这两种技术(多个迭代器变量和多个for循环)有两个要求:您知道网格数组中的“行”或“列”中有多少个项目,并且每个子数组包含的项目数量与其他子数组相同。

为了更灵活地遍历多维数组,您可以使用嵌套for循环。外层循环遍历每一行(子数组),内层循环遍历当前行中的每个项目。这种技术在子数组具有不同数量的项目时也有效:

for row  in multiarr
   for item in row
     puts(item)
   end
end

在下一章中,您将更深入地了解for循环和其他迭代器。

数组索引

与字符串(见第三章 #=> hello (or) ["h", "e", "l", "l", "o"]
print( arr[-5,5 ] ) #=> world (or) ["w", "o", "r", "l", "d"]
print( arr[0..4] ) #=> hello (or) ["h", "e", "l", "l", "o"]
print( arr[-5..-1] ) #=> world (or) ["w", "o", "r", "l", "d"]


注意,`print`或`puts`显示的输出可能因 Ruby 版本的不同而有所差异。当 Ruby 1.8 显示数组中的元素时,它们一个接一个地显示,看起来像单个字符串,例如`hello`。然而,Ruby 1.9 却以数组格式显示项目,例如`["h", "e", "l", "l", "o"]`。

如果您使用`p`而不是`print`来检查数组,Ruby 1.8 和 1.9 将显示相同的结果:

p( arr[0,5] ) #=> ["h", "e", "l", "l", "o"]
p( arr[0..4] ) #=> ["h", "e", "l", "l", "o"]


与字符串一样,当你提供两个整数以从数组中返回连续的项目数量时,第一个整数是起始索引,而第二个是项目的 *数量*(*不是* 索引):

arr[0,5] # returns 5 chars - ["h", "e", "l", "l", "o"]


你也可以通过索引数组来进行赋值。例如,我首先创建一个空数组,然后将项目放入索引 0、1 和 3。索引 2 的“空”槽位将被 `nil` 值填充:

*array_assign.rb*

arr = []

arr[0] = [0]
arr[1] = ["one"]
arr[3] = ["a", "b", "c"]

arr now contains:

[[0], ["one"], nil, ["a", "b", "c"]]


再次强调,你可以使用起始-结束索引、范围和负索引值:

arr2 = ['h','e','l','l','o',' ','w','o','r','l','d']

arr2[0] = 'H'
arr2[2,2] = 'L', 'L'
arr2[4..6] = 'O','-','W'
arr2[-4,4] = 'a','l','d','o'

arr2 now contains:

["H", "e", "L", "L", "O", "-", "W", "a", "l", "d", "o"]


## 复制数组

注意,当你使用赋值运算符(`=`)将一个数组变量赋值给另一个变量时,你实际上是在赋值一个 *引用* 到数组;你并没有创建一个副本。例如,如果你将一个名为 `arr1` 的数组赋值给另一个名为 `arr2` 的数组,对任意一个变量的任何更改也会改变另一个变量的值,因为 *两个变量都引用了同一个数组*。如果你想使变量引用两个不同的数组,你可以使用 `clone` 方法来创建一个新的副本:

*array_copy.rb*

arr1=['h','e','l','l','o',' ','w','o','r','l','d']
arr2=arr1 # arr2 is now the same as arr1.
# Change arr1 and arr2 changes too!
arr3=arr1.clone
# arr3 is a copy of arr1.
# Change arr3 and arr2 is unaffected


## 测试数组是否相等

数组的比较运算符是 `<=>`。这比较两个数组——我们可以称它们为 `arr1` 和 `arr2`。如果 `arr1` 小于 `arr2`,则返回 −1;如果 `arr1` 和 `arr2` 相等,则返回 0;如果 `arr2` 大于 `arr1`,则返回 1。但是 Ruby 如何确定一个数组是否“大于”或“小于”另一个数组呢?它会比较一个数组中的每个项目与另一个数组中相应项目的比较结果。当两个值不相等时,返回它们的比较结果。换句话说,如果进行如下比较:

[0,10,20] <=> [0,20,20]


会返回值 −1。这意味着第一个数组“小于”第二个数组,因为第一个数组索引 1 的整数(10)小于第二个数组索引 1 的整数(20)。

如果你想要根据数组的长度而不是其元素的值进行比较,你可以使用 `length` 方法:

Here [2,3,4].length is less than [1,2,3,4].length

p([1,2,3].length<=>[1,2,3,4].length) #=> −1
p([2,3,4].length<=>[1,2,3,4].length) #=> −1


如果你正在比较字符串数组,那么比较是基于构成这些字符串的字符的 ASCII 值进行的。如果一个数组比另一个数组长,并且两个数组中的元素都相等,那么较长的数组被认为是“大于”。然而,如果比较了这样的两个数组,并且较短的数组中的一个元素大于较长的数组中相应的元素,那么被认为是“大于”的是 *较短的* 数组。

*array_compare.rb*

p([1,2,3]<=>[2,3,4]) #=> −1 (array 1 < array 2)
p([2,3,4]<=>[1,2,3]) #=> 1 (array 1 > array 2)
p([1,2,3,4]<=>[1,2,3]) #=> 1 (array 1 > array 2)all
p([1,2,3,4]<=>[100,200,300]) #=> −1 (array 1 < array 2)
p([1,2,3]<=>["1","2","3"]) #=> nil (invalid comparison)


## 数组排序

`sort`方法使用比较运算符`<=>`比较相邻的数组元素。这个运算符为许多 Ruby 类定义,包括 Array、String、Float、Date 和 Fixnum。然而,这个运算符并不是为所有类定义的(也就是说,它不是为所有其他类派生的 Object 类定义的)。这个不幸的后果之一是,它不能用于包含`nil`值的数组排序。然而,可以通过定义自己的排序例程来克服这种限制。这是通过向`sort`方法发送一个*块*来完成的。你将在第十章中详细了解块,但就现在而言,你需要知道的是,块是一段由花括号或`do`和`end`关键字分隔的代码块。以下块确定了`sort`方法使用的比较方式:

arr.sort{
|a,b|
a.to_s <=> b.to_s
}


在这里`arr`是一个数组对象,变量`a`和`b`代表两个连续的数组元素。我已经使用`to_s`方法将每个变量转换为字符串;这会将`nil`转换为空字符串,这将按“低”排序。请注意,尽管我的排序块定义了数组项的排序顺序,但它并没有改变数组项本身。因此,`nil`将保持为`nil`,整数将保持为整数。字符串转换仅用于实现比较,而不是改变数组项。

*array_sort.rb*

arr = ['h','e','l','l','o',' ',nil,'w','o','r','l','d',1,2,3,nil,4,5]

sort ascending from nil upwards

sorted_arr = arr.sort{
|a,b|
a.to_s <=> b.to_s
}

p(sorted_arr )


这是前一段代码创建并显示的数组:

[nil, nil, " ", 1, 2, 3, 4, 5, "d", "e", "h", "l", "l", "l", "o", "o", "r", "w"]


代码存档中提供的*array_sort.rb*程序还包含一个用于降序排序的方法。这是通过改变比较运算符两边的项目顺序来实现的:

reverse_sorted_arr = arr.sort{
|a,b|
b.to_s <=> a.to_s
}


## 比较值

比较运算符“操作符”`<=>`(实际上是一个方法)定义在名为`Comparable`的 Ruby 模块中。目前,你可以将模块视为一种可重用的代码库。你将在第十二章中更详细地了解模块。

你可以在自己的类中包含`Comparable`模块。这让你可以重写`<=>`方法,以便你能够精确地定义特定对象类型之间的比较方式。例如,你可能想创建一个 Array 的子类,以便比较仅基于两个数组的长度,而不是基于数组中每个项的值(这是默认情况,如 See Testing Arrays for Equality 中所述)。以下是你可以这样做的示例:

*comparisons.rb*

class MyArray < Array
include Comparable

def <=> ( anotherArray )
self.length <=> anotherArray.length
end
end


现在,你可以这样初始化两个 MyArray 对象:

myarr1 = MyArray.new([0,1,2,3])
myarr2 = MyArray.new([1,2,3,4])


你可以使用 MyArray 中定义的`<=>`方法进行比较:

Two MyArray objects

myarr1 <=> myarr2 #=> 0


这个比较返回 0,表示两个数组相等(因为我们的`<=>`方法仅根据长度来评估相等性)。另一方面,如果你用完全相同的整数值初始化两个标准数组,数组类的自身`<=>`方法将执行比较:

Two Array objects

arr1 <=> arr2 #=> −1


这里比较返回 -1,这表示第一个数组评估为“小于”第二个数组,因为 Array 类的 `<=>` 方法比较 `arr1` 中每个项目的数值,这些数值小于 `arr2` 中相同索引处的项目值。

但如果你想要使用传统的编程符号进行“小于”、“等于”和“大于”的比较呢?

< # less than
== # equal to

            # greater than

在 MyArray 类中,你可以进行此类比较而无需编写任何额外的代码。这是因为 `Comparable` 模块已被包含在 MyArray 类中,并自动提供这三个比较方法;每个方法都基于 `<=>` 方法的定义进行比较。由于我们的 `<=>` 方法基于数组中项目的数量进行评估,因此当第一个数组比第二个数组短时,`<` 方法评估为 true,当两个数组长度相等时,`==` 评估为 true,当第二个数组比第一个数组长时,`>` 评估为 true:

p( myarr1 < myarr2 ) #=> false
p( myarr1 == myarr2 ) #=> true


标准的 Array 类不包含 `Comparable` 模块。所以如果你尝试使用 `<`、`==` 或 `>` 比较两个普通数组,Ruby 将显示一个错误消息,告诉你该方法未定义。

然而,很容易将这些三个方法添加到 Array 的子类中。你只需要包含 Comparable,如下所示:

class Array2 < Array
include Comparable
end


Array2 类现在将根据 Array 的 `<=>` 方法进行比较——也就是说,通过测试存储在数组中的项目的值,而不是仅仅测试数组的长度。假设 Array2 对象 `arr1` 和 `arr2` 使用与之前用于 `myarr1` 和 `myarr2` 相同的数组初始化,你现在会看到以下结果:

p( arr1 < arr2 ) #=> true
p( arr1 > arr2 ) #=> false


## 数组方法

一些标准数组方法会修改数组本身,而不是返回数组的修改副本。这些包括带有终止感叹号标记的方法,例如 `sort!`、`reverse!`、`flatten!` 和 `compact!`。这些还包括 `<<` 方法,它通过向其左侧的数组添加右侧的数组来修改左侧的数组;`clear`,它从给定的数组中移除所有元素;以及 `delete` 和 `delete_at`,它们移除选定的元素。表 4-1 展示了一些更常用的数组方法。

表 4-1. 常用数组方法

| Array | 任务 |
| --- | --- |
| `&` | 返回两个数组的公共元素,不包含重复项 |
| `+` | 返回连接两个数组的数组 |
| `-` | 返回从第一个数组中移除第二个数组中项目的数组 |
| `<<` | 通过从第二个数组中追加项目来修改第一个数组 |
| `clear` | 通过移除所有元素来修改数组 |
| `compact` | 返回移除 `nil` 项的数组 |
| `compact!` | 通过移除 `nil` 项来修改数组 |
| `delete( object )` | 通过删除对象来修改数组 |
| `delete_at( index )` | 通过删除索引处的项目来修改数组 |
| `flatten` | 解包嵌套数组元素并返回数组 |
| `flatten!` | 通过解包嵌套数组元素修改数组 |
| `length` | 返回数组中的元素数量 |
| `reverse` | 返回元素顺序相反的数组 |
| `reverse!` | 通过反转元素顺序修改数组 |
| `sort` | 使用 `<=>` 返回排序后的数组 |
| `sort!` | 通过 `<=>` 修改排序后的数组 |

你可以在 *array_methods.rb* 示例程序中尝试之前的方法。以下是一些示例:

*array_methods.rb*

arr1 = [1,1,2,2,3,3]
arr2 = [1,2,3,4,5,6,7,8,9]
arr3 = ['h','e','l','l','o',' ',nil,'w','o','r','l','d']

p(arr1&arr2 ) #=> [1, 2, 3]
p(arr1+arr2) #=> [1, 1, 2, 2, 3, 3, 1, 2, 3, 4, 5, 6, 7, 8, 9]
p(arr1-arr2) #=> []
p(arr2-arr1) #=> [4, 5, 6, 7, 8, 9]
arr1<<arr2
p(arr1) #=> [1, 1, 2, 2, 3, 3, [1, 2, 3, 4, 5, 6, 7, 8, 9]]
arr1.clear
p(arr1) #=>[]


虽然大多数数组方法的行为可以从其名称中推断出来,但 `flatten` 和 `compact` 方法需要一些解释。当数组不包含子数组时,我们说它被 *扁平化*。所以如果你有一个像 `[1,[2,3]]` 这样的数组,你可以调用 `[1,[2,3]].flatten` 来返回这个数组:`[1,2,3]`。

当数组不包含 `nil` 项时,我们说它被 *压缩*。所以如果你有一个像 `[1,2,nil,3]` 这样的数组,你可以调用 `[1,2,nil,3].compact` 来返回这个数组:`[1,2,3]`。可以通过将一个方法调用直接放在另一个方法调用之后来将 Array 的方法链接在一起:

*flatten_compact.rb*

p( [1,nil,[2,nil,3]].flatten.compact ) #=> [1,2,3]


# 哈希

尽管数组提供了按数字索引项目集合的好方法,但有时按其他方式索引它们会更方便。例如,如果你正在创建一个食谱集合,按名称索引每个食谱(例如,“丰富的巧克力蛋糕”和“红酒炖牛肉”)比按数字更有意义。

Ruby 有一个类允许你这样做,称为 *哈希*。这相当于其他语言中称为字典或关联数组的等效物。就像真正的字典一样,每个条目都通过一个唯一的 *键*(在现实生活中的字典中,这将是一个单词)来索引,该键与一个值(在字典中,这将是一个单词的定义)相关联。

## 创建哈希

就像数组一样,你可以通过创建 Hash 类的新实例来创建一个哈希:

*hash1.rb*

h1 = Hash.new
h2 = Hash.new("Some kind of ring")


之前的两个示例都创建了一个空的 Hash 对象。Hash 对象始终有一个默认值——也就是说,当在给定的索引处找不到特定值时返回的值。在这些示例中,`h2` 使用默认值 `"Some kind of ring"` 初始化;`h1` 没有初始化值,因此它的默认值将是 `nil`。

创建了 Hash 对象后,你可以使用类似数组的语法向其中添加项——也就是说,通过放置索引在方括号内并使用 `=` 来赋值。明显的区别是,在数组中,索引(或 *键*)必须是整数;在哈希中,它可以是任何唯一的数据项:

h2['treasure1'] = 'Silver ring'
h2['treasure2'] = 'Gold ring'
h2['treasure3'] = 'Ruby ring'
h2['treasure4'] = 'Sapphire ring'


通常,键可能是一个数字,或者在之前的代码中是一个字符串。然而,原则上,键可以是任何类型的对象。例如,给定某个类 X,以下赋值是完全合法的:

x1 = X.new('my Xobject')
h2[x1] = 'Diamond ring'


唯一键?

在将键分配给哈希时请小心。如果你在哈希中使用相同的键两次,你最终会覆盖原始值。这就像在数组中相同的索引处两次赋值一样。考虑以下示例:

h2['treasure1'] = 'Silver ring'
h2['treasure2'] = 'Gold ring'
h2['treasure3'] = 'Ruby ring'
h2['treasure1'] = 'Sapphire ring'


这里使用了两次键 `'treasure1'`。因此,原始值 `'Silver ring'` 被替换为 `'Sapphire ring'`,导致以下哈希:

{"treasure1"=>"Sapphire ring", "treasure2"=>"Gold ring", "treasure3"=>"Ruby ring"}


创建哈希并使用键值对初始化它们有一个简写方式。只需添加一个键后跟 `=>` 和其关联的值;每个键值对应该用逗号分隔,并将所有内容放在一对大括号内:

h1 = { 'room1'=>'The Treasure Room',
'room2'=>'The Throne Room',
'loc1'=>'A Forest Glade',
'loc2'=>'A Mountain Stream' }


## 哈希索引

要访问一个值,请将其键放在方括号内:

puts(h1['room2']) #=> 'The Throne Room'


如果你指定了一个不存在的键,将返回默认值。回想一下,你没有为 `h1` 指定默认值,但你已经为 `h2` 指定了:

p(h1['unknown_room']) #=> nil
p(h2['unknown_treasure']) #=> 'Some kind of ring'


使用 `default` 方法获取默认值,并使用 `default=` 方法设置它(有关 `get` 和 `set` 访问器方法的更多信息,请参阅第二章):

p(h1.default)
h1.default = 'A mysterious place'


## 复制哈希

与数组一样,你可以将一个哈希变量赋给另一个,在这种情况下,两个变量都将引用同一个哈希,并且使用任一变量进行的更改将影响该哈希:

*hash2.rb*

h4 = h1
h4['room1']='A new Room'
puts(h1['room1']) #=> 'A new Room'


如果你想让两个变量在不同的哈希对象中引用相同的项,请使用 `clone` 方法创建一个新的副本:

h5 = h1.clone
h5['room1'] = 'An even newer Room'
puts(h1['room1']) #=> 'A new room' (i.e., its value is unchanged)


## 哈希顺序

哈希中元素的顺序根据你使用的 Ruby 版本而有所不同。在 Ruby 1.8 中,哈希通常按照其键定义的顺序存储,例如,键 1 小于键 2。当添加新项时,这些项将按键顺序插入。在 Ruby 1.9 中,哈希按照定义的顺序存储。当添加新项时,这些项将附加到哈希的末尾。

作为一般原则,最好不要对哈希中元素的顺序做出假设。大多数编程语言将哈希或字典视为无序集合。如果你假设哈希顺序是不可预测的,不仅会避免在运行不同 Ruby 实现的程序时可能出现的错误,而且还会避免当键的类型不同时可能出现的麻烦。记住,单个哈希可能包含整数、字符串和浮点键的混合,它们的相对顺序可能不明显。

*hash_order.rb*

h = {2=>"two", 1=>"one", 4=>"four" }
p( h )
h[3] = "three"
p( h )
h2 = {"one"=>1, 2=>"two", 4.5=>"four" }
p (h2)


当运行此代码时,Ruby 1.8 产生以下输出:

{1=>"one", 2=>"two", 4=>"four"}
{1=>"one", 2=>"two", 3=>"three", 4=>"four"}


但 Ruby 1.9 显示如下:

{2=>"two", 1=>"one", 4=>"four"}
{2=>"two", 1=>"one", 4=>"four", 3=>"three"}


## 对哈希进行排序

如果你想确保哈希的元素按特定顺序排列,你可以对它们进行排序。与数组类一样,你可能会发现哈希的 `sort` 方法存在一些问题。它期望处理相同数据类型的键,因此,例如,如果你合并两个数组,其中一个使用整数键,另一个使用字符串键,你将无法对合并后的哈希进行排序。解决这个问题的方法,就像数组一样,是编写一些代码来执行自定义类型的比较,并将其传递给 `sort` 方法。你可能给它一个方法,如下所示:

*hash_sort.rb*

def sorted_hash( aHash )
return aHash.sort{
|a,b|
a.to_s <=> b.to_s
}
end


这是对哈希中每个键的字符串表示(`to_s`)进行排序。实际上,哈希的 `sort` 方法将哈希转换为嵌套数组,其中包含 `[key, value]` 数组,并使用数组 `sort` 方法进行排序。

## 哈希方法

哈希类有众多内置方法。例如,要使用其键从哈希中删除项,请使用 `delete` 方法:

aHash.delete( someKey )To test if a key or value exists, use the
has_key? and has_value? methods:aHash.has_key?( someKey )
aHash.has_value?( someValue )


要合并两个哈希,请使用 `merge` 方法:`hash1.merge(hash2)`。

要使用原始哈希的值作为键和键作为值来创建新的哈希,请使用 `aHash.invert`。要返回包含哈希键或值的数组,请使用 `aHash.keys` 和 `aHash.values`。

这里有一个使用这些方法的一些示例:

*hash_methods.rb*

h1 = {
'room1'=>'The Treasure Room',
'room2'=>'The Throne Room',
'loc1'=>'A Forest Glade',
'loc2'=>'A Mountain Stream'
}

h2 = {1=>'one', 2=>'two', 3=> 'three'}

h1['room1'] = 'You have wandered into a dark room'
h1.delete('loc2')
p(h1)
#=> {"room1"=>"You have wandered into a dark room",
#=> "room2"=>"The Throne Room",
#=> "loc1"=>"A Forest Glade"}
p(h1.has_key?('loc2')) #=> false
p(h2.has_value?("two")) #=>true
p(h2.invert) #=> {"one"=>1, "two"=>2, "three"=>3}
p(h2.keys) #=>[1, 2, 3]
p(h2.values) #=>["one", "two", "three"]


如果你想要在哈希中查找项的位置,请使用 Ruby 1.8 中的 `index` 方法或 Ruby 1.9 中的 `key` 方法。`index` 方法在 Ruby 1.9 中仍然存在,但已弃用,因此可能在未来的版本中删除:

h2.index("two") # use this with Ruby 1.8
h2.key("two") # use this Ruby 1.9


深入挖掘

在本节中,你将学习更多操作数组和方法以及矩阵、向量和集合的基础知识。

将哈希视为数组

哈希的 `keys` 和 `values` 方法每个都返回一个数组,因此你可以使用各种数组方法来操作它们。以下是一些简单的示例(请记住,键和值的顺序可能会根据使用的 Ruby 版本而有所不同):

*hash_ops.rb*

h1 = {'key1'=>'val1', 'key2'=>'val2', 'key3'=>'val3', 'key4'=>'val4'}
h2 = {'key1'=>'val1', 'KEY_TWO'=>'val2', 'key3'=>'VALUE_3', 'key4'=>'val4'}

p( h1.keys & h2.keys ) # set intersection (keys)

=> ["key1", "key3", "key4"]

p( h1.values & h2.values ) # set intersection (values)

=> ["val1", "val2", "val4"]

p( h1.keys+h2.keys ) # concatenation

=> [ "key1", "key2", "key3", "key4", "key1", "key3", "key4", "KEY_TWO"]

p( h1.values-h2.values ) # difference

=> ["val3"]

p( (h1.keys << h2.keys) ) # append

=> ["key1", "key2", "key3", "key4", ["key1", "key3", "key4", "KEY_TWO"]]

p( (h1.keys << h2.keys).flatten.reverse ) # 'un-nest' arrays and reverse

=> ["KEY_TWO", "key4", "key3", "key1", "key4", "key3", "key2", "key1"]


追加与连接

请注意使用 `+` 来添加第二个数组的 *值* 到第一个数组中连接和使用 `<<` 来添加第二个 *数组* 本身作为第一个数组的最后一个元素的差异:

*append_concat.rb*

a =[1,2,3]
b =[4,5,6]
c = a + b #=> c=[1, 2, 3, 4, 5, 6] a=[1, 2, 3]
a << b #=> a=[1, 2, 3, [4, 5, 6]]


此外,`<<` 修改第一个(即 *接收者*)数组,而 `+` 返回一个新数组,但不会更改接收者数组。

### 注意

在面向对象术语中,方法所属的对象称为 *接收者*。其思想是,与过程式语言中调用函数不同,向对象发送“消息”。例如,可以将消息 `+ 1` 发送到整数对象,而消息 `reverse` 可以发送到字符串对象。接收消息的对象会尝试找到一种方式(即一种 *方法*)来响应该消息。例如,字符串对象有一个 `reverse` 方法,因此能够响应 `reverse` 消息,而整数对象没有这样的方法,因此不能响应。

你可以使用 `flatten` 方法清理使用 `<<` 组合的两个数组,如下所示:

a=[1, 2, 3, [4, 5, 6]]
a.flatten #=> [1, 2, 3, 4, 5, 6]


向量和矩阵

为了数学家的利益,Ruby 提供了 Vector 类和 Matrix 类。一个 *向量* 是一个有序元素集合,可以对它执行某些数学运算。一个 *矩阵* 是行和列的集合,每一行本身也是一个向量。矩阵允许你执行矩阵操作,这是本书范围之外的主题,并且可能只对数学程序员感兴趣。然而,你将在这里查看一些简单的示例。

首先,给定两个矩阵对象,`m1` 和 `m2`,你可以使用加号将矩阵中对应单元格的值相加,如下所示:`m3 = m1+m2`。为了使用它,你必须使用 `require` 指令导入 Matrix:

*matrix.rb*

require "Matrix" # This is essential!

m1 = Matrix[ [1,2,3,4],
[5,6,7,8],
[9,10,11,12],
[13,14,15,16] ]

m2 = Matrix[ [10,20,30,40],
[50,60,70,80],
[90,100,110,120],
[130,140,150,160] ]

m3 = m1+m2
p(m3)


这将输出以下矩阵:

Matrix[[11, 22, 33, 44], [55, 66, 77, 88], [99, 110, 121, 132], [143, 154, 165, 176]]


以下示例从两个向量创建一个矩阵。通过将向量传递给 `Matrix.columns()` 方法,你构建一个矩阵,其行是数组的数组。这里矩阵有两个列,由向量 `v` 和 `v2` 创建,每行包含两个项目,一个来自每个列:

v = Vector[1,2,3,4,5]
v2 = Vector[6,7,8,9,10]
m4 = Matrix.columns([v,v2])
p( m4 )


这将输出以下内容:

Matrix[[1, 6], [2, 7], [3, 8], [4, 9], [5, 10]]


另一方面,如果你将相同的两个向量传递给 `Matrix.rows()` 方法,你将创建一个包含两行的矩阵,每行都是一个向量:

m5 = Matrix.rows([v,v2])
p( m5 )


这将输出以下内容:

Matrix[Vector[1, 2, 3, 4, 5], Vector[6, 7, 8, 9, 10]]


集合

Set 类实现了一个无序值集合,其中没有重复项。你可以使用值数组初始化一个 Set,在这种情况下,将忽略重复项:

*sets.rb*

s1 = Set.new( [1,2,3,4,5,2] )
s2 = Set.new( [1,1,2,3,4,4,5,1] )
s3 = Set.new( [1,2,100] )
weekdays = Set.new( %w( Monday, Tuesday, Wednesday, Thursday,
Friday, Saturday, Sunday ) )


你可以使用 `add` 方法添加新值:

s1.add( 1000 )


`merge` 方法将一个集合的值与另一个集合的值合并:

s1.merge(s2)


你可以使用 `==` 来测试相等性。包含相同值(记住,当创建集合时将删除重复项)的两个集合被认为是相等的:

p( s1 == s2 ) #=> true


如果你显示集合的内容,顺序可能会根据所使用的 Ruby 版本而有所不同。如果顺序很重要,你可以使用 `to_a` 方法将集合转换为数组,并使用标准或自定义排序,如 排序数组 中所述:

p( weekdays.to_a.sort ) # sort alphabetically

=> ["Friday,", "Monday,", "Saturday,", "Sunday", "Thursday,", "Tuesday,",

"Wednesday,"]



# 第五章。循环和迭代器

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860138.png.jpg)

大多数编程都涉及重复。也许你希望程序响铃 10 次,只要还有更多行要读取,就读取文件中的行,或者显示警告直到用户按下键。Ruby 提供了多种执行此类重复的方法。

# `for`循环

在许多编程语言中,当你想要运行一段代码一定次数时,你只需将其放在`for`循环中即可。在大多数语言中,你给`for`循环一个变量,该变量初始化为起始值,并在每次循环迭代中增加 1,直到它达到某个特定的结束值。当达到结束值时,`for`循环停止运行。

这里是这种传统类型的`for`循环的 Pascal 版本:

(* This is Pascal code, not Ruby! *)
for i := 1 to 3 do
writeln( i );


你可能还记得,在上一章中,Ruby 的`for`循环根本不是这样工作的!你不需要给它起始和结束值,你只需要给`for`循环一个项目列表,它会逐个迭代它们,将每个值依次分配给循环变量,直到到达列表的末尾。

例如,这里是一个遍历数组中项的`for`循环,依次显示每个项:

*for_loop.rb*

This is Ruby code...

for i in [1,2,3] do
puts( i )
end


`for`循环更像是某些其他编程语言提供的“for each”迭代器。循环迭代的项不必是整数。这也同样有效:

for s in ['one','two','three'] do
puts( s )
end


Ruby 的作者将`for`描述为`each`方法的“语法糖”,该方法由数组、集合、哈希和字符串(字符串实际上是一组字符的集合)等集合类型实现。为了比较,这是之前显示的`for`循环之一,使用`each`方法重写的版本:

*each_loop.rb*

[1,2,3].each do |i|
puts( i )
end


如你所见,实际上并没有太大的区别。要将`for`循环转换为`each`迭代器,我只需要删除`for`和`in`,并在数组后附加`.each`。然后我在`do`之后将迭代器变量`i`放在一对竖线之间。比较这些其他示例,看看`for`循环和`each`迭代器有多么相似。

*for_each.rb*

--- Example 1 ---

i) for

for s in ['one','two','three'] do
puts( s )
end

ii) each

['one','two','three'].each do |s|
puts( s )
end

--- Example 2 ---

i) for

for x in [1, "two", [3,4,5] ] do puts( x ) end

ii) each

[1, "two", [3,4,5] ].each do |x| puts( x ) end


顺便提一下,在跨越多行的`for`循环中,`do`关键字是可选的,但当它写在单行时则是必需的:

Here the 'do' keyword can be omitted

for s in ['one','two','three']
puts( s )
end

But here it is required

for s in ['one','two','three'] do puts( s ) end


这个例子展示了`for`和`each`都可以用来遍历范围内的值:

*for_each2.rb*

for

for s in 1..3
puts( s )
end

each

(1..3).each do |s|
puts(s)
end


如何编写“常规”的`for`循环

如果你错过了传统的`for`循环类型,你总是可以在 Ruby 中使用`for`循环遍历一个范围内的值来模拟它。例如,这就是如何使用`for`循环变量从 1 计数到 10,并在每次循环迭代中显示其值:

for i in (1..10) do
puts( i )
end


*for_to.rb*

顺便说一下,当与 `each` 方法一起使用时,例如 `1..3` 这样的范围表达式必须放在括号内,否则 Ruby 会假设你试图将 `each` 作为最终整数(Fixnum)的方法,而不是整个表达式(Range)的方法。当在 `for` 循环中使用范围时,括号是可选的。

# 块和块参数

在 Ruby 中,迭代器的主体被称为 *块*,而在块顶部竖线之间声明的任何变量被称为 *块参数*。从某种意义上说,块就像一个函数,而块参数就像函数的参数列表。`each` 方法运行块内的代码,并将由集合(如数组 `multiarr`)提供的参数传递给它。在上一节中的示例中,`each` 方法反复将一个包含四个元素的数组传递给块,这些元素初始化了四个块参数 `a`、`b`、`c`、`d`。除了遍历集合之外,块还可以用于其他目的。

Ruby 还有一种用于定义块的替代语法。你不需要使用 `do..end`,而是可以使用这样的花括号 `{..}`:

*block_syntax.rb*

do..end

[[1,2,3],[3,4,5],[6,7,8]].each do
|a,b,c|
puts( "#{a}, #{b}, #{c}" )
end

curly brackets

[[1,2,3],[3,4,5],[6,7,8]].each{
|a,b,c|
puts( "#{a}, #{b}, #{c}" )
}


无论你使用哪种块定界符,都必须确保开界定符 `{` 或 `do` 放在与 `each` 方法相同的行上。在 `each` 和开块界定符之间插入换行符是一个语法错误。我将在第十章 中有更多关于块的内容要讲。

# 迭代到和倒序迭代

如果你需要从一个特定的低值计数到高值,你可以使用整数的 `upto()` 方法。如果你想在每次迭代中显示值,可以使用可选的块参数:

*upto_downto.rb*

0.upto(10) do
| i |
puts( i )
end


之前的代码显示了从 0 到 10 的整数。你也可以使用 `downto()` 方法从高值向下计数到低值:

10.downto(0) do
| i |
puts( i )
end


如你所能猜到的,这段代码显示了从 10 到 0。

# 多个迭代器参数

在上一章中,你使用了一个包含多个循环变量的 `for` 循环来遍历一个多维数组。在 `for` 循环的每次迭代中,一个变量从外数组中分配了一行(即一个“子数组”):

*multi_array.rb*

Here multiarr is an array containing two 'rows'

(subarrays) at index 0 and 1

multiarr = [ ['one','two','three','four'],
[1,2,3,4]
]

This for loop runs twice (once for each 'row' of multiarr)

for (a,b,c,d) in multiarr
print("a=#{a}, b=#{b}, c=#{c}, d=#{d}\n" )
end


之前的循环打印出以下内容:

a=one, b=two, c=three, d=four
a=1, b=2, c=3, d=4


然而,你也可以使用 `each` 方法通过传递四个块参数——`a`、`b`、`c`、`d`——在每次迭代中通过 `do` 和 `end` 定界的块来遍历这个包含四个元素的数组:

multiarr.each do |a,b,c,d|
print("a=#{a}, b=#{b}, c=#{c}, d=#{d}\n" )
end


当然,还有使用花括号分隔的替代块语法,它同样有效:

multiarr.each{ |a,b,c,d|
print("a=#{a}, b=#{b}, c=#{c}, d=#{d}\n" )
}


之前的两个例子都将`multiarr`数组中的两个元素传递给迭代块。第一个元素本身是一个包含四个字符串的数组:`['one','two','three','four']`。由于块在竖线`|a,b,c,d|`之间声明了四个参数,这四个字符串被分配给匹配的参数,然后使用`print`语句打印出来。然后`each`方法将`multiarr`的第二个元素传递给块。这是一个包含整数的四个元素数组:`[1,2,3,4]`。这些也被分配给块参数`|a,b,c,d|`,`print`语句显示它们。请注意,输出与使用 for 循环时的输出相同:

a=one, b=two, c=three, d=four
a=1, b=2, c=3, d=4


# while 循环

Ruby 还有一些其他的循环结构。这是如何进行 while 循环的:

while tired
sleep
end


或者,可以这样表达:

sleep while tired


尽管这两个例子的语法不同,但它们执行的功能相同。在第一个例子中,`while`和`end`之间的代码(这里是对名为`sleep`的方法的调用)只要布尔条件(在这种情况下,是名为`tired`的方法返回的值)评估为 true 就会执行。与 for 循环一样,当测试条件和要执行的代码出现在不同的行上时,可以在它们之间可选地放置`do`关键字;当测试条件和要执行的代码出现在同一行上时,`do`关键字是必需的。

## while 循环修饰符

在循环的第二个版本(`sleep while tired`)中,要执行的代码(`sleep`)位于测试条件(`while tired`)之前。这种语法称为 while 修饰符。当你想使用这种语法执行多个表达式时,你可以将它们放在 begin 和 end 关键字之间:

begin
sleep
snore
end while tired


这里有一个示例,展示了各种不同的语法:

*1loops.rb*

$hours_asleep = 0

def tired
if $hours_asleep >= 8 then
$hours_asleep = 0
return false
else
$hours_asleep += 1
return true
end
end

def snore
puts('snore....')
end

def sleep
puts("z" * $hours_asleep )
end

while tired do sleep end # a single-line while loop

while tired # a multiline while loop
sleep
end

sleep while tired # single-line while modifier

begin # multiline while modifier
sleep
snore
end while tired


上一段代码中的最后一个例子(多行的 while 循环修饰符)需要仔细考虑,因为它引入了一些重要的新行为。当一个由 begin 和 end 分隔的代码块位于 while 测试之前时,该代码总是至少执行一次。在其他类型的 while 循环中,如果布尔条件最初评估为 false,则代码可能根本不会执行。

## 确保 while 循环至少执行一次

通常,while 循环执行零次或多次,因为布尔测试是在循环执行之前评估的;如果测试一开始就返回 false,则循环内的代码永远不会运行。然而,当 while 测试跟在由 begin 和 end 括起来的代码块之后时,循环会根据布尔表达式的评估执行一次或多次,因为布尔表达式是在循环内的代码执行之后评估的。

这些例子应该有助于澄清:

*2loops.rb*

x = 100

# The code in this loop never runs

while (x < 100) do puts('x < 100') end

# The code in this loop never runs

puts('x < 100') while (x < 100)

# But the code in loop runs once

begin puts('x < 100') end while (x < 100)


# until 循环

Ruby 还有一个 `until` 循环,可以将其视为 *while not* 循环。其语法和选项与 `while` 相同——也就是说,测试条件和要执行的代码可以放在同一行(在这种情况下,`do` 关键字是强制性的)或放在不同的行(在这种情况下,`do` 是可选的)。还有一个 `until` 修饰符,允许你将代码放在测试条件之前,并有一个选项将代码放在 `begin` 和 `end` 之间,以确保代码块至少运行一次。

这里有一些 `until` 循环的简单示例:

*until.rb*

i = 10

until i == 10 do puts(i) end # never executes

until i == 10 # never executes
puts(i)
i += 1
end

puts(i) until i == 10 # never executes

begin # executes once
puts(i)
end until i == 10


`while` 和 `until` 循环都可以像 `for` 循环一样,用于遍历数组和其他集合。例如,以下代码展示了两种遍历数组中所有元素的方法:

*array_iterate.rb*

arr= [1,2,3,4,5]
i = 0

while i < arr.length
puts(arr[i])
i += 1
end

i=0
until i == arr.length
puts(arr[i])
i +=1
end


# loop

与 `for` 和 `while` 不同,`loop` 命令不会评估测试条件以确定是否继续循环。要跳出循环,你必须显式地使用 `break` 关键字,如下面的示例所示:

*3loops.rb*

i=0
loop do
puts(arr[i])
i+=1
if (i == arr.length) then
break
end
end

loop {
puts(arr[i])
i+=1
if (i == arr.length) then
break
end
}


这些使用 `loop` 方法重复执行随后的代码块。这些块与之前使用 `each` 方法时使用的迭代器块类似。再次强调,你有选择块定界符,要么是花括号,要么是 `do` 和 `end`。

在每种情况下,代码通过递增计数器变量 `i` 来遍历数组 `arr`,并在 `(i == arr.length)` 条件评估为 true 时跳出循环。注意,如果没有 `break`,这些循环将永远进行下去。

深入挖掘

Ruby 提供了多种遍历数组、范围等结构中项的方法。在这里,我们发现了枚举和比较的内部细节。

Enumerable 模块

Hashes、Arrays、Ranges 和 Sets 都包含一个名为 `Enumerable` 的 Ruby 模块。它为这些数据结构提供了一些有用的方法,例如 `include?`,如果找到特定值则返回 true;`min`,返回最小值;`max`,返回最大值;以及 `collect`,它创建一个由块返回的值组成的新结构。在以下代码中,你可以看到一些这些函数在数组上的使用:

*enum.rb*

x = (1..5).collect{ |i| i }
p( x ) #=> [1, 2, 3, 4, 5]

arr = [1,2,3,4,5]
y = arr.collect{ |i| i }
p( y ) #=> [1, 2, 3, 4, 5]
z = arr.collect{ |i| i * i }
p( z ) #=> [1, 4, 9, 16, 25]

p( arr.include?( 3 ) ) #=> true
p( arr.include?( 6 ) ) #=> false
p( arr.min ) #=> 1
p( arr.max ) #=> 5


这些相同的方法也适用于其他包含 `Enumerable` 的集合类。以下是一个使用 Hash 类的示例:

*enum2.rb*

h = {'one'=>'for sorrow',
'two'=>'for joy',
'three'=>'for a girl',
'four'=>'for a boy'}

y = h.collect{ |i| i }
p( y )


此代码输出以下内容:

[["one", "for sorrow"], ["two", "for joy"], ["three", "for a
girl"], ["four", "for a boy"]]


注意,由于哈希存储方式的变化,当此代码运行时,Ruby 1.8 和 Ruby 1.9 显示的项目顺序不同。还要记住,哈希中的项目不是按顺序索引的,所以当你使用 `min` 和 `max` 方法时,这些返回根据它们的数值最低和最高的项目——这里的项是字符串,数值由键中字符的 ASCII 码确定。

p( h.min ) #=> ["one", "for sorrow"]
p( h.max ) #=> ["two", "for joy"]


自定义比较

如果你想让 `min` 和 `max` 根据某些其他标准(比如字符串的长度)返回项目,最简单的方法是在一个块中定义比较的性质。这与我在第四章中定义的排序块的方式类似。你可能还记得,你是通过将一个块传递给 `sort` 方法来对哈希(这里变量为 `h`)进行排序的,如下所示:

h.sort{ |a,b| a.to_s <=> b.to_s }


两个参数 `a` 和 `b` 代表哈希中的两个项目,它们使用 `<=>` 比较方法进行比较。你可以类似地传递块给 `max` 和 `min` 方法:

h.min{ |a,b| a[0].length <=> b[0].length }
h.max{|a,b| a[0].length <=> b[0].length }


当哈希将项目传递给一个块时,它是以数组的形式进行的,每个数组都包含一个键值对。所以,如果一个哈希包含如下项目:

{'one'=>'for sorrow', 'two'=>'for joy'}


然后,两个块参数 `a` 和 `b` 将初始化为两个数组:

a = ['one', 'for sorrow']
b = ['two', 'for joy']


这解释了为什么我定义了 `max` 和 `min` 方法的自定义比较的两个块特别比较了两个块参数的第一个元素,即索引 0 处的元素:

a[0].length <=> b[0].length


这确保了比较是基于哈希中的 *键* 的。然而,这里有一个潜在的陷阱。正如前一章所解释的,Ruby 1.8 和 Ruby 1.9 中哈希的默认排序方式不同。这意味着如果你按键的长度排序,就像我之前使用自定义比较器所做的那样,并且有多个键具有相同的长度,那么在不同版本的 Ruby 中返回的第一个匹配项将不同。例如,在我的哈希中,前两个键(“one”和“two”)的长度相同。所以当我使用基于键长度的比较进行 `min` 操作时,结果将在 Ruby 版本 1.8 和 1.9 中不同:

p( h.min{|a,b| a[0].length <=> b[0].length } )


Ruby 1.8 显示以下内容:

["two", "for joy"]


Ruby 1.9 显示以下内容:

["one", "for sorrow"]


这又是说明为什么始终不要对哈希中元素的排序做出假设的另一个例子。现在假设你想比较值而不是键。在前面的例子中,你可以通过将数组索引从 0 改为 1 来简单地做到这一点:

*enum3.rb*

p( h.min{|a,b| a[1].length <=> b[1].length } )
p( h.max{|a,b| a[1].length <=> b[1].length } )


最短长度的值是“for joy”,最长长度的值是“for a secret never to be told”,因此之前的代码显示以下内容:

["two", "for joy"]
["seven", "for a secret never to be told"]


当然,你可以在你的块中定义其他类型的自定义比较。假设,例如,你想按照你说话的顺序评估字符串“one”,“two”,“three”,等等。实现这一点的其中一种方法就是创建一个有序的字符串数组:

str_arr=['one','two','three','four','five','six','seven']


现在,如果一个哈希 `h` 包含这些字符串作为键,一个块可以使用 `str_array` 作为参考来确定最小和最大值。这也确保了我们无论使用哪个版本的 Ruby 都能获得相同的结果:

h.min{|a,b| str_arr.index(a[0]) <=> str_arr.index(b[0])}
h.max{|a,b| str_arr.index(a[0]) <=> str_arr.index(b[0])}


这显示了以下内容:

["one", "for sorrow"]
["seven", "for a secret never to be told"]


所有的前一个示例都使用了 Array 和 Hash 类的 `min` 和 `max` 方法。记住,这些方法是由 `Enumerable` 模块提供的,该模块被“包含”在 Array 和 Hash 类中。

有时候,可能需要将 `Enumerable` 方法(如 `max`、`min` 和 `collect`)应用于不继承自实现这些方法的现有类(如 Array)的类。你可以通过在你的类中包含 `Enumerable` 模块,然后编写一个名为 `each` 的迭代器方法来实现这一点:

*include_enum1.rb*

class MyCollection
include Enumerable

def initialize( someItems )
@items = someItems
end

def each
@items.each{ |i|
yield( i )
}
end
end


在这里,你使用一个数组初始化一个 MyCollection 对象,该数组将被存储在实例变量 `@items` 中。当你调用 `Enumerable` 模块提供的方法之一(如 `min`、`max` 或 `collect`)时,这将调用 `each` 方法逐个获取数据。因此,这里的 `each` 方法将 `@items` 数组中的每个值传递到包含该项目的代码块中,该项目被分配给代码块参数 `i`。关键字 `yield` 是 Ruby 中的一个特殊功能,它运行传递给 `each` 方法的代码块。当我讨论第十章(第十章)中的 Ruby 块时,你将更深入地了解这一点。

现在,你可以使用 `Enumerable` 方法与你的 MyCollection 对象一起使用:

*include_enum2.rb*

things = MyCollection.new(['x','yz','defgh','ij','klmno'])

p( things.min ) #=> "defgh"
p( things.max ) #=> "yz"
p( things.collect{ |i| i.upcase } )
#=> ["X", "YZ", "DEFGH", "IJ", "KLMNO"]


你同样可以使用你的 MyCollection 类来处理数组,如哈希的键或值。目前,`min` 和 `max` 方法采用默认行为:它们基于数值进行比较。这意味着根据字符的 ASCII 值,“xy”被认为比“abcd”的值“更高”。如果你想执行其他类型的比较——比如说,根据字符串长度,使得“abcd”被认为比“xz”的值更高——你可以重写 `min` 和 `max` 方法:

def min
@items.to_a.min{|a,b| a.length <=> b.length }
end

def max
@items.to_a.max{|a,b| a.length <=> b.length }
end


这里是完整的类定义及其 `each`、`min` 和 `max` 的版本:

*include_enum3.rb*

class MyCollection
include Enumerable

def initialize( someItems )
    @items = someItems
end

def each
    @items.each{ |i| yield i }
end

def min
    @items.to_a.min{|a,b| a.length <=> b.length }
end

def max
    @items.to_a.max{|a,b| a.length <=> b.length }
end

end


现在可以创建一个 MyCollection 对象,并且可以使用以下方式使用其重写的方法:

things = MyCollection.new(['z','xy','defgh','ij','abc','klmnopqr'])
x = things.collect{ |i| i }
p( x ) #=> ["z", "xy", "defgh", "ij", "abc", "klmnopqr"]
y = things.max
p( y ) #=> "klmnopqr"
z = things.min
p( z ) #=> "z"


each 和 yield

当 `Enumerable` 模块中的方法使用你编写的 `each` 方法时,实际上发生了什么?结果是 `Enumerable` 方法(如 `min`、`max`、`collect` 等)将一个代码块传递给 `each` 方法。这个代码块期望一次接收一个数据项(即某个集合中的每个项目)。你的 `each` 方法通过一个代码块参数(如这里的参数 `i`)提供这个项目:

def each
@items.each{ |i|
yield( i )
}
end


如前所述,关键字 `yield` 告诉代码运行传递给 `each` 方法的块——也就是说,运行由 `Enumerable` 模块的 `min`、`max` 或 `collect` 方法提供的代码。这意味着这些方法中的代码可以与各种不同类型的集合一起使用。你所要做的就是将 `Enumerable` 模块包含到你的类中,并编写一个 `each` 方法,以确定哪些值将由 `Enumerable` 方法使用。


# 第六章。条件语句

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860138.png.jpg)

计算机程序,就像生活本身一样,充满了等待做出的艰难决定。比如“如果我待在床上,我会睡得更香,否则我不得不去工作;如果我去工作,我会赚些钱,否则我会失去我的工作”,等等。你已经在之前的程序中执行了许多 `if` 测试。以一个简单的例子来说,这是来自第一章中的税计算器(第一章):

if (subtotal < 0.0) then
subtotal = 0.0
end


在这个程序中,用户被提示输入一个值,`subtotal`,然后用来计算其上的税额。如果用户在疯狂中输入一个小于 0 的值,`if` 测试会检测到这一点,因为测试 `(subtotal < 0.0)` 评估为真,这会导致 `if` 测试和 `end` 关键字之间的代码块被执行;在这里,这会将 `subtotal` 的值设置为 0。

# if..then..else

这样的简单测试只有两种可能的结果之一。要么运行一小段代码,要么不运行,这取决于测试是否评估为真。通常,你需要超过两种可能的结果。假设,例如,如果你的程序需要根据一天是工作日还是周末采取不同的行动方案。你可以在 `if` 部分之后添加一个 `else` 部分,如下所示:

*if_else.rb*

if aDay == 'Saturday' or aDay == 'Sunday'
daytype = 'weekend'
else
daytype = 'weekday'
end


### 注意

与许多其他编程语言一样,Ruby 使用一个等号(`=`)来赋值,使用两个等号(`==`)来测试值。

这里的 `if` 条件很简单。它测试两种可能的情况:如果变量 `aDay` 的值等于字符串“Saturday”,或者 `aDay` 的值等于字符串“Sunday”。如果这两个条件中的任何一个为真,则执行下一行代码 `daytype = 'weekend'`;在其他所有情况下,执行 `else` 之后的代码 `daytype = 'weekday'`。

当将 `if` 测试和要执行的代码放在不同的行上时,`then` 关键字是可选的。当测试和代码放在同一行上时,`then` 关键字是必需的:

*if_then.rb*

if x == 1 then puts( 'ok' ) end # with 'then'
if x == 1 puts( 'ok' ) end # syntax error!


在 Ruby 1.8 中,冒号字符(`:`)被允许作为 `then` 的替代。这种语法在 Ruby 1.9 中不受支持:

if x == 1 : puts( 'ok' ) end # This works with Ruby 1.8 only


`if` 测试不仅限于评估两个条件。假设,例如,你的代码需要确定某一天是工作日还是假日。所有工作日都是工作日;所有周六都是假日,但周日只有在你不加班的情况下才是假日。这是我第一次尝试编写一个测试来评估所有这些条件:

*and_or_wrong.rb*

working_overtime = true
if aDay == 'Saturday' or aDay == 'Sunday' and not working_overtime
daytype = 'holiday'
puts( "Hurrah!" )
else
daytype = 'working day'
end


不幸的是,这并没有达到预期的效果。记住,周六总是假日。但这段代码坚持认为周六是工作日。这是因为 Ruby 将测试理解为“如果这一天是周六并且我没有加班,或者如果这一天是周日并且我没有加班”,而我的真正意思是“如果这一天是周六,或者如果这一天是周日并且我没有加班”。解决这种歧义的最简单方法是将任何要作为单个单元评估的代码用括号括起来,如下所示:

*and_or.rb*

if aDay == 'Saturday' or (aDay == 'Sunday' and not working_overtime)


# 且,或,非

顺便说一下,Ruby 有两种不同的语法来测试布尔(真/假)条件。在先前的例子中,我使用了英语风格的运算符:`and`、`or` 和 `not`。如果你愿意,你可以使用类似于许多其他编程语言中使用的替代运算符,即 `&&`(且)、`||`(或)和 `!`(非)。

虽然如此,但这两组运算符并不完全可互换。首先,它们有不同的优先级,这意味着当在单个测试中使用多个运算符时,测试的不同部分可能会根据你使用的运算符以不同的顺序进行评估。例如,看看这个测试:

*days.rb*

if aDay == 'Saturday' or aDay == 'Sunday' and not working_overtime
daytype = 'holiday'
end


假设布尔变量 `working_overtime` 为真,如果将变量 `aDay` 初始化为字符串 “Saturday”,这个测试会成功吗?换句话说,如果 `aDay` 是 “Saturday”,`daytype` 会被赋值为 “holiday” 吗?答案是不会,不会成功。只有当 `aDay` 是 “Saturday” 或 “Sunday” 且 `working_overtime` 不为真时,测试才会成功。因此,当在之前的代码中使用 `or` 时,周六会被视为工作日。

现在考虑这个测试:

if aDay == 'Saturday' || aDay == 'Sunday' && !working_overtime
daytype = 'holiday'
end


表面上看,这个测试和上一个测试是相同的;唯一的区别是这次我使用了运算符的替代语法。然而,这个变化不仅仅是表面的,因为如果 `aDay` 是 “Saturday”,这个测试会评估为真,并且 `daytype` 会被初始化为值 “holiday”。这是因为 `||` 运算符的优先级高于 `or` 运算符。所以,这个测试在 `aDay` 是 “Saturday” 或 `aDay` 是 “Sunday” 且 `working_overtime` 不为真时都会成功。所以,当在之前的代码中使用 `||` 时,周六会被视为假日。

有关更多信息,请参阅 Digging Deeper。作为一个一般原则,你最好决定你更喜欢哪一组运算符——坚持使用它们,并使用括号来避免歧义。

# 否定

在前面的例子中,我在表达式 `!working_overtime` 中使用了否定运算符 (`!`),这可以读作“not working_overtime”。否定运算符可以用在表达式的开头;作为替代,你可以在表达式的左右两侧使用“不等于” (`!=`) 运算符:

*negation.rb*

!(1==1) #=> false
1!=1 #=> false


或者,你可以使用`not`而不是`!`:

not( 1==1 ) #=> false


# if..elsif

毫无疑问,会有一些时候你需要根据几个不同的条件执行多个不同的操作。实现这一点的其中一种方法是通过评估一个`if`条件,然后跟随一系列放在`elsif`关键字之后的测试条件。然后必须使用`end`关键字来结束整个结构。

例如,这里我在一个`while`循环中反复从用户那里获取输入。一个`if`条件测试用户是否输入了“q”(我已经使用了`chomp()`来移除输入中的回车符)。如果没有输入“q”,第一个`elsif`条件测试输入的整数值是否大于 800;如果这个测试失败,下一个`elsif`条件测试它是否小于或等于 800:

*if_elsif.rb*

while input != 'q' do
puts("Enter a number between 1 and 1000 (or 'q' to quit)")
print("?- ")
input = gets().chomp()
if input == 'q'
puts( "Bye" )
elsif input.to_i > 800
puts( "That's a high rate of pay!" )
elsif input.to_i <= 800
puts( "We can afford that" )
end
end


这个程序的问题在于,尽管它要求用户输入一个介于 1 到 1,000 之间的值,但它接受小于 1(顺便说一句,如果你真的想要负数的薪水,我很乐意给你提供一份工作!)和大于 1,000(在这种情况下,不要期待从我这里得到工作!)的值。

你可以通过重写两个`elsif`条件并添加一个在所有前面的测试失败时执行的`else`部分来修复这个问题:

*if_elsif2.rb*

if input == 'q'
puts( "Bye" )
elsif input.to_i > 800 && input.to_i <= 1000
puts( "That's a high rate of pay!" )
elsif input.to_i <= 800 && input.to_i > 0
puts( "We can afford that" )
else
puts( "I said: Enter a number between 1 and 1000!" )
end


if..then..else 的简写表示法

Ruby 还有一个`if..then..else`的简写表示法,其中问号(`?`)替换了`if..then`部分,冒号(`:`)充当`else`。正式来说,这可以被称为*三元运算符*或*条件运算符*。

< Test Condition > ? <if true do this> : <else do this>


例如:

x == 10 ? puts("it's 10") : puts( "it's some other number" )


当测试条件复杂(如果它使用了`and`s 和`or`s)时,你应该将其括起来。如果测试和代码跨越多行,`?`必须放在先前的条件所在的同一行上,而`:`必须放在紧随其后的代码所在的同一行上。换句话说,如果你在`?`或`:`之前放置换行符,你会生成一个语法错误。这是一个有效的多行代码块的例子:

(aDay == 'Saturday' or aDay == 'Sunday') ?
daytype = 'weekend' :
daytype = 'weekday'


*if_else_alt.rb*

这里是另一个较长的`if..elsif`部分序列的例子,后面跟着一个通配的`else`部分。这次触发值`i`是一个整数:

*days2.rb*

def showDay( i )
if i == 1 then puts("It's Monday" )
elsif i == 2 then puts("It's Tuesday" )
elsif i == 3 then puts("It's Wednesday" )
elsif i == 4 then puts("It's Thursday" )
elsif i == 5 then puts("It's Friday" )
elsif (6..7) === i then puts( "Yippee! It's the weekend! " )
else puts( "That's not a real day!" )
end
end


注意,我使用了范围`(6..7)`来匹配周六和周日的两个整数值。`===`方法(即三个`=`字符)测试一个值(在这里是`i`)是否是该范围的成员。在先前的例子中,以下内容:

(6..7) === i


可以重写为以下内容:

(6..7).include?(i)


`===`方法由 Object 类定义,并在子类中被覆盖。它的行为根据类而异。你很快就会看到,它的一个基本用途是为`case`语句提供有意义的测试。

# unless

Ruby 还可以执行`unless`测试,这是`if`测试的完全相反:

*unless.rb*

unless aDay == 'Saturday' or aDay == 'Sunday'
daytype = 'weekday'
else
daytype = 'weekend'
end


将 `unless` 视为表达“如果不”的另一种方式。以下代码与之前的代码等价;两者都将周六和周日视为周末,其他天视为工作日:

if !(aDay == 'Saturday' or aDay == 'Sunday')
daytype = 'weekday'
else
daytype = 'weekend'
end


# if 和 unless 修饰符

你可能还记得在 第五章 中提到的 `while` 循环的替代语法。而不是这样写:

while tired do sleep end


你可以写成这样:

sleep while tired


这种将 `while` 关键字放置在要执行的代码和测试条件之间的替代语法称为 *while 修饰符*。实际上,Ruby 还有 `if` 和 `unless` 修饰符。以下是一些示例:

*if_unless_mod.rb*

sleep if tired

begin
sleep
snore
end if tired

sleep unless not tired

begin
sleep
snore
end unless not tired


当你需要重复执行一些定义良好的操作时,这种语法的简洁性是有用的。例如,如果有一个名为 `DEBUG` 的常量是 true,你可能会在代码中添加调试输出:

puts( "somevar = #{somevar}" ) if DEBUG


# 情况语句

当你需要根据单个变量的值执行多种不同的操作时,多个 `if..elsif` 测试既冗长又重复。

一个更简洁的替代方案是 `case` 语句。它以单词 `case` 开头,后面跟着要测试的变量名。然后是一系列 `when` 部分,每个部分指定一个“触发”值和一些代码。

只有当测试变量等于触发值时,此代码才会执行:

*case.rb*

case( i )
when 1 then puts("It's Monday" )
when 2 then puts("It's Tuesday" )
when 3 then puts("It's Wednesday" )
when 4 then puts("It's Thursday" )
when 5 then puts("It's Friday" )
when (6..7) then puts( "Yippee! It's the weekend! " )
else puts( "That's not a real day!" )
end


常量

从原则上讲,常量是值永远不会改变的对象。例如,Ruby 的 `Math` 模块中的 `PI` 是一个常量。Ruby 中的常量以大写字母开头。类名也是常量。你可以使用 `constants` 方法获取所有定义的常量的列表:

Object.constants


Ruby 提供了 `const_get` 和 `const_set` 方法来获取和设置以符号指定的命名常量的值(例如,以冒号开头的标识符 `:RUBY_VERSION`)。请注意,与许多其他编程语言中的常量不同,Ruby 的常量可以分配新的值:

RUBY_VERSION = "1.8.7"
RUBY_VERSION = "2.5.6"


上次对 `RUBY_VERSION` 常量的重新赋值产生了一个“已初始化的常量”警告,但没有错误!你甚至可以重新赋值 Ruby 标准类库中声明的常量。例如,这里我重新赋值了 `PI` 的值。尽管这会显示一个警告,但赋值仍然成功:

puts Math::PI #=> 3.141592653589793
Math::PI = 100 #=> warning: already initialized constant PI
puts Math::PI #=> 100


你需要意识到 Ruby 常量的不变性是一种编程 *约定*,而不是严格强制执行的 *规则*。自然地,重新赋值常量不是好的编程实践。

*constants.rb*

*math_pi.rb*

在前面的例子中,我使用了 `then` 关键字来将每个 `when` 测试与要执行的代码分开。在 Ruby 1.8 中,就像前面提到的 `if` 测试一样,你可以使用冒号作为替代,但这种语法在 Ruby 1.9 中不受支持:

when 1 : puts("It's Monday" ) # This works in Ruby 1.8 only!


如果测试和要执行的代码位于不同的行上,则可以省略 `then`。与 C 类语言中的 `case` 语句不同,当匹配成功时,无需输入 `break` 关键字以防止执行渗透到其余部分。在 Ruby 中,一旦匹配成功,`case` 语句就会退出:

*case_break.rb*

def showDay( i )
case( i )
when 5 then puts("It's Friday" )
puts("...nearly the weekend!")
when 6 then puts("It's Saturday!" )
# the following never executes
when 5 then puts( "It's Friday all over again!" )
end
end

showDay( 5 )
showDay( 6 )


这将显示以下内容:

It's Friday
...nearly the weekend!
It's Saturday!


您可以在每个 `when` 条件之间包含多行代码,并且可以使用逗号分隔的多个值来触发单个 `when` 块,如下所示:

when 6, 7 then puts( "Yippee! It's the weekend! " )


`case` 语句中的条件不一定是简单变量;它可以是一个像这样的表达式:

*case2.rb*

case( i + 1 )


您还可以使用非整数类型,例如字符串。如果在 `when` 部分指定了多个触发值,它们可能是不同类型的——例如,字符串和整数:

when 1, 'Monday', 'Mon' then puts( "Yup, '#{i}' is Monday" )


这里有一个更长的示例,说明了前面提到的某些语法元素:

*case3.rb*

case( i )
when 1 then puts("It's Monday" )
when 2 then puts("It's Tuesday" )
when 3 then puts("It's Wednesday" )
when 4 then puts("It's Thursday" )
when 5 then puts("It's Friday" )
puts("...nearly the weekend!")
when 6, 7
puts("It's Saturday!" ) if i == 6
puts("It's Sunday!" ) if i == 7
puts( "Yippee! It's the weekend! " )
# the following never executes
when 5 then puts( "It's Friday all over again!" )
else puts( "That's not a real day!" )
end


## === 方法

如前所述,`when` 在 `case` 语句中测试的对象使用的是 `===` 方法。因此,例如,就像 `===` 方法在整数构成范围的一部分时返回 true 一样,`when` 测试在 `case` 语句中的整数变量构成范围表达式的一部分时也返回 true:

when (6..7) then puts( "Yippee! It's the weekend! " )


如果对特定对象的 `===` 方法的效果有疑问,请参考该对象类的 Ruby 文档。Ruby 的标准类在核心 API 中有文档说明:[`www.ruby-doc.org/`](http://www.ruby-doc.org/)。

## 交替的 Case 语法

`case` 语句中的条件不一定是简单变量;它可以是一个像这样的表达式:

*case4.rb*

salary = 2000000
season = 'summer'

happy = case
when salary > 10000 && season == 'summer' then
puts( "Yes, I really am happy!" )
'Very happy'
when salary > 500000 && season == 'spring' then 'Pretty happy'
else puts( 'miserable' )
end

puts( happy ) #=> 'Very happy'


深入挖掘

Ruby 的比较运算符比表面上要复杂。在这里,您将了解它们的效果和副作用,并学习如何在满足条件时退出代码块。

布尔运算符

在 Ruby 中,以下运算符可用于测试可能返回真或假值的表达式。

| `and` 和 `&&` | 这些运算符评估左侧表达式;只有当结果为真时,才会评估右侧表达式。`and` 的优先级低于 `&&`。 |
| --- | --- |
| `or` 和 `&#124;&#124;` | 这些运算符评估左侧表达式;如果结果为假,则评估右侧表达式。`or` 的优先级低于 `&#124;&#124;`。 |
| `not` 和 `!` | 这些运算符否定布尔值;换句话说,当为假时返回 true,当为真时返回 false。 |

使用替代布尔运算符时要小心。由于优先级不同,条件将按不同的顺序评估,并可能产生不同的结果。

考虑以下内容:

*boolean_ops.rb*

Example 1

if ( 13 ) and (21) || (3==3) then
puts('true')
else
puts('false')
end

Example 2

if ( 13 ) and (21) or (3==3) then
puts('true')
else
puts('false')
end


这些看起来可能相同。实际上,示例 1 打印 “false”,而示例 2 打印 “true”。这完全是由于 `or` 的优先级低于 `||`。因此,示例 1 测试的是“如果 1 等于 3 [*false*] 并且(2 等于 1 或 3 等于 3)[*true*]。”由于这两个必要条件中有一个是假的,整个测试返回 false。

现在看看示例 2。它测试的是“(如果 1 等于 3 并且 2 等于 1)[*false*] 或 3 等于 3 [*true*]。”这次,只需要两个测试中的一个成功;第二个测试评估为 true,因此整个测试返回 true。

在这种测试中,运算符优先级带来的副作用可能会导致非常难以发现的错误。您可以通过使用括号来明确测试的含义来避免这些问题。在这里,我重新编写了示例 1 和 2;在每种情况下,添加一对括号都反转了测试最初返回的布尔值:

Example 1 (b) - now returns true

if (( 13 ) and (21)) || (3==3) then
puts('true')
else
puts('false')
end

Example 2 (b) - now returns false

if ( 13 ) and ((21) or (3==3)) then
puts('true')
else
puts('false')
end


布尔运算符的怪癖

警告:Ruby 的布尔运算符有时可能会表现出一种奇特且不可预测的行为。例如:

*eccentricities.rb*

puts( (not( 11 )) ) # This is okay
puts( not( 1
1 ) ) # Syntax error in Ruby 1.8
# but okay in Ruby 1.9

puts( true && true && !(true) ) # This is okay
puts( true && true and !(true) ) # This is a syntax error

puts( ((true) and (true)) ) # This is okay
puts( true && true ) # This is okay
puts( true and true ) # This is a syntax error


在许多情况下,通过坚持使用一种运算符风格(即 `and`、`or` 和 `not` 或 `&&`、`||` 和 `!`)而不是混合使用两种运算符,您可以避免问题。此外,推荐大量使用括号!

捕获和抛出

Ruby 提供了一对方法,`catch` 和 `throw`,当满足某些条件时,可以用来跳出代码块。这是 Ruby 在某些其他编程语言中 `goto` 的近似等效。该块必须以 `catch` 开头,后跟一个符号(即一个由冒号前缀的唯一标识符),例如 `:done` 或 `:finished`。块本身可以由花括号或 `do` 和 `end` 关键字界定,如下所示:

think of this as a block called :done

catch( :done ){

some code here

}

and this is a block called :finished

catch( :finished ) do

some code here

end


在块内部,您可以使用符号作为参数调用 `throw`。通常,您会在满足某些特定条件时调用 `throw`,这使得跳过块中剩余的所有代码变得可行。例如,假设块中包含一些代码,提示用户输入一个数字,将某个值除以该数字,然后继续进行一系列复杂的计算。显然,如果用户输入 0,那么后续的所有计算都无法完成,因此您会希望跳过它们,直接跳出块并继续执行其后的任何代码。这是实现这一目标的一种方法:

*catch_throw.rb*

catch( :finished) do
print( 'Enter a number: ' )
num = gets().chomp.to_i
if num == 0 then
throw :finished # if num is 0, jump out of the block
end
# Here there may be hundreds of lines of
# calculations based on the value of num
# if num is 0 this code will be skipped
end
# the throw method causes execution to
# jump to here - outside of the block
puts( "Finished" )


实际上,您可以在块外部调用 `throw`,如下所示:

def dothings( aNum )
i = 0
while true
puts( "I'm doing things..." )
i += 1
throw( :go_for_tea ) if (i == aNum )
# throws to end of go_to_tea block
end
end

catch( :go_for_tea ){ # this is the :go_to_tea block
dothings(5)
}


您还可以将 `catch` 块嵌套在其他 `catch` 块内部,如下所示:

catch( :finished) do
print( 'Enter a number: ' )
num = gets().chomp.to_i
if num == 0 then throw :finished end
puts( 100 / num )

catch( :go_for_tea ){
dothings(5)
}

puts( "Things have all been done. Time for tea!" )
end


与其他编程语言中的`goto`跳转一样,Ruby 中的`catch`和`throw`应该谨慎使用,因为它们会破坏你代码的逻辑,并且可能引入难以发现的错误。


# 第七章。方法

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860138.png.jpg)

你在这本书中使用了无数的方法。总的来说,它们并不特别复杂,所以你可能想知道为什么关于方法的这一章这么长。正如你将发现的,方法远比表面看起来要复杂得多。

# 类方法

你到目前为止使用的方法都是 *实例方法*。实例方法属于类的特定实例——换句话说,属于单个对象。你也可以编写 *类方法*。(其他一些语言将这种方法称为静态方法。)类方法属于类本身。要定义类方法,必须在方法名前加上类名和句点。

*class_methods1.rb*

class MyClass
def MyClass.classMethod
puts( "This is a class method" )
end

def instanceMethod
puts( "This is an instance method" )
en
end


当调用类方法时,你应该使用类名:

MyClass.classMethod


特定的对象不能调用类方法。同样,类也不能调用实例方法:

MyClass.instanceMethod #=> Error! This is an 'undefined method'
ob.classMethod #=> Error! This is an 'undefined method'


# 类方法的作用是什么?

但是,你可能会合理地问,为什么你想要创建一个类方法而不是更常见的实例方法?主要有两个原因:首先,类方法可以用作“即用函数”而无需麻烦地创建一个对象来使用,其次,它可以在需要在使用对象之前就运行方法的情况下使用。

举几个将方法作为“即用函数”使用的例子,可以考虑 Ruby 的 File 类。它的大多数方法都是类方法。这是因为大多数时候,你会使用它们对现有文件进行操作或返回有关文件的信息。你不需要创建一个新的 File 对象来做这件事;相反,你将文件名作为参数传递给 File 类的方法。你将在第十三章(第十三章。文件和 IO)中更详细地了解 File 类。以下是它的一些类方法使用示例:

*file_methods.rb*

fn = 'file_methods.rb'
if File.exist?(fn) then
puts(File.expand_path(fn))
puts(File.basename(fn))
puts(File.dirname(fn))
puts(File.extname(fn))
puts(File.mtime(fn))
puts("#{File.size(fn)} bytes")
else
puts( "Can't find file!")
end


这会输出类似以下的内容:

C:/bookofruby2/ch7/file_methods.rb
file_methods.rb
.
.rb
2010-10-05 16:14:53 +0100
300 bytes


在需要在使用对象之前就使用一个方法时,类方法至关重要。这个例子中最重要的是 `new` 方法。

你每次创建对象时都会调用 `new` 方法。在对象被创建之前,显然你不能调用其实例方法——因为只能从已经存在的对象中调用实例方法。当你使用 `new` 时,你是在调用类本身的方法,并告诉类创建一个新的实例。

# 类变量

类方法可能会让你想起之前使用的类变量(即,以 `@@` 开头的变量)。你可能记得,你曾在简单的冒险游戏中使用类变量(见 属性读取器和写入器 中的 *2adventure.rb*)来统计游戏中对象的总数;每次创建一个新的 Thing 对象时,`@@num_things` 类变量就增加 1:

class Thing
@@num_things = 0

def initialize( aName, aDescription )
@@num_things +=1
end

end


与实例变量(即属于从类创建的特定对象的变量)不同,类变量在首次声明时必须赋予一个值:

@@classvar = 1000 # class variables must be initialized


在类体内部初始化实例或类变量只会影响类本身存储的值。类变量对类本身以及从该类创建的对象都是可用的。然而,每个实例变量都是唯一的;每个对象都有其自己的任何实例变量的副本——*类本身也可能有自己的实例变量*。

类变量、实例变量和方法:总结

实例变量以 `@` 开头:

@myinstvar # instance variable


类变量以 `@@` 开头:

@@myclassvar # class variable


实例方法通过 `def` *`MethodName`* 定义:

def anInstanceMethod
# some code
end


类方法通过 `def` *`ClassName`*.*`MethodName`* 定义:

def MyClass.aClassMethod
# some code
end


要了解一个类可能有哪些实例变量,请参考 *class_methods2.rb* 程序。该程序定义了一个包含一个类方法和一个实例方法的类:

*class_methods2.rb*

class MyClass
@@classvar = 1000
@instvar = 1000

def MyClass.classMethod
    if @instvar == nil then
        @instvar = 10
    else
        @instvar += 10
    end

    if @@classvar == nil then
        @@classvar = 10
    else
        @@classvar += 10
    end
end

def instanceMethod
    if @instvar == nil then
        @instvar = 1
    else
        @instvar += 1
    end

    if @@classvar == nil then
        @@classvar = 1
    else
        @@classvar += 1
    end

end

def showVars
    return "(instance method) @instvar = #{@instvar}, @@classvar = #{@@classvar}"
end

def MyClass.showVars
    return "(class method) @instvar = #{@instvar}, @@classvar = #{@@classvar}"
end

end


注意,它分别声明并初始化了一个类变量和一个实例变量,`@@classvar` 和 `@instvar`。它的类方法 `classMethod` 将这两个变量都增加 10,而它的实例方法 `instanceMethod` 将这两个变量各增加 1。注意,我已经为类变量和实例变量都赋值了:

@@classvar = 1000
@instvar = 1000


我之前说过,通常不会以这种方式为实例变量分配初始值。这个规则的例外是当你为类的实例变量而不是从该类派生的对象分配一个值时。这个区别很快就会变得清楚。

我编写了一些代码,创建了 MyClass 的三个实例(`ob` 变量在循环的每次迭代中初始化为一个新的实例),然后调用了类方法和实例方法:

for i in 0..2 do
ob = MyClass.new
MyClass.classMethod
ob.instanceMethod
puts( MyClass.showVars )
puts( ob.showVars )
end


类方法 `MyClass.showVars` 和实例方法 `showVars` 在每次循环迭代中显示 `@instvar` 和 `@@classvar` 的值。当你运行代码时,显示的值如下:

(class method) @instvar = 1010, @@classvar = 1011
(instance method) @instvar = 1, @@classvar = 1011
(class method) @instvar = 1020, @@classvar = 1022
(instance method) @instvar = 1, @@classvar = 1022
(class method) @instvar = 1030, @@classvar = 1033
(instance method) @instvar = 1, @@classvar = 1033


你可能需要仔细查看这些结果,以便了解这里发生了什么。总结来说,这是正在发生的事情:在类方法 `MyClass.classMethod` 和实例方法 `instanceMethod` 中的代码都会增加类变量和实例变量 `@@classvar` 和 `@instvar` 的值。

你可以清楚地看到,这两个方法(类方法在创建新对象时将 10 加到`@@classvar`上,而实例方法将其加 1)都会增加类变量。然而,每当创建一个新对象时,它的实例变量都会由`instanceMethod`初始化为 1。这是预期的行为,因为每个对象都有自己的实例变量副本,但所有对象共享一个唯一的类变量。可能不那么明显的是,类本身也有自己的实例变量,`@instvar`。这是因为,在 Ruby 中,类是一个对象,因此可以包含实例变量,就像任何其他对象一样。`MyClass`变量`@instvar`是由类方法`MyClass.classMethod`增加的:

@instvar += 10


当实例方法`showVars`打印`@instvar`的值时,它打印的是存储在特定对象`ob`中的值;`ob`的`@instvar`的初始值是`nil`(不是`MyClass`变量`@instvar`初始化时的 1,000),这个值在`instanceMethod`中被增加 1。

当类方法`MyClass.showVars`打印`@instvar`的值时,它打印的是存储在类本身中的值(换句话说,MyClass 的`@instvar`与`ob`的`@instvar`是不同的变量)。但当任一方法打印类变量`@@classvar`的值时,值是相同的。

只需记住,类变量只有一个副本,但实例变量可能有多个副本。如果这仍然让你感到困惑,请查看`*inst_vars.rb*`程序:

*inst_vars.rb*

class MyClass
@@classvar = 1000
@instvar = 1000

def MyClass.classMethod
if @instvar == nil then
@instvar = 10
else
@instvar += 10
end
end

def instanceMethod
if @instvar == nil then
@instvar = 1
else
@instvar += 1
end
end
end

ob = MyClass.new
puts MyClass.instance_variable_get(:@instvar)
puts( '--------------' )
for i in 0..2 do

MyClass.classMethod

ob.instanceMethod
puts( "MyClass @instvar=#{MyClass.instance_variable_get(:@instvar)}")
puts( "ob @instvar= #{ob.instance_variable_get(:@instvar)}" )
end


这次,你不再在循环的每次迭代中创建新的对象实例,而是在一开始就创建一个单独的实例(`ob`)。当调用`ob.instanceMethod`时,`@instvar`增加 1。

在这里,我使用了一个小技巧来查看类和方法内部,并使用 Ruby 的`instance_variable_get`方法检索`@instvar`的值(当我介绍动态规划时,我会回到这个话题第二十章):

puts( "MyClass @instvar= #{MyClass.instance_variable_get(:@instvar)}" )
puts( "ob @instvar= #{ob.instance_variable_get(:@instvar)}" )


因为只有当你增加属于对象`ob`的`@instvar`时,它的`@instvar`值才会从 1 增加到 3,这是`for`循环执行的结果。但属于`MyClass`类的`@instvar`永远不会增加;它保持在初始值 1,000:

1000

MyClass @instvar= 1000
ob @instvar= 1
MyClass @instvar= 1000
ob @instvar= 2
MyClass @instvar= 1000
ob @instvar= 3


但现在让我们取消注释这一行:

MyClass.classMethod


这调用了一个类方法,该方法将`@instvar`增加 10。这次当你运行程序时,你会看到,就像之前一样,`ob`的`@instvar`变量在每次循环迭代时增加 1,而`MyClass`的`@instvar`变量增加 10:

1000

MyClass @instvar= 1010
ob @instvar= 1
MyClass @instvar= 1020
ob @instvar= 2
MyClass @instvar= 1030
ob @instvar= 3


类是一个对象

要理解类的实例变量,只需记住一个类是一个对象(实际上,它是 `Class` 类的一个实例!)MyClass “类对象”有自己的实例变量(`@instvar`),就像 `ob` 对象有自己的实例变量一样(这里也恰好也叫做 `@instvar`)。尽管名字相同,但这两个变量是不同的:一个属于类本身;另一个属于从该类创建的每个对象内部。实例变量始终是对象实例独有的,因此没有任何两个对象(甚至是一个像 MyClass 这样的对象,它也恰好是一个类!)可以共享单个实例变量。

# Ruby 构造器:new 还是 initialize?

在 第一章 中,我对 `new` 和 `initialize` 方法进行了简要的解释。在那个阶段,你还没有检查 Ruby 的类方法和实例方法以及变量的区别,因此无法全面讨论 `new` 和 `initialize` 如何协同工作。因为这些方法非常重要,所以现在我们将更详细地研究它们。

负责创建对象的那个方法被称为 *构造器*。在 Ruby 中,构造器方法叫做 `new`。`new` 方法是一个类方法,一旦它创建了一个对象,如果存在这样的方法,它将运行一个名为 `initialize` 的实例方法。

简而言之,`new` 方法是构造器,而 `initialize` 方法用于在对象创建后立即初始化任何变量的值。但为什么你不能只是编写自己的 `new` 方法并在其中初始化变量呢?好吧,让我们试试:

*new.rb*

class MyClass
def initialize( aStr )
@avar = aStr
end

def MyClass.new( aStr )
super
@anewvar = aStr.swapcase
end
end

ob = MyClass.new( "hello world" )
puts( ob )
puts( ob.class )


在这里,我编写了一个以 `super` 关键字开始的 `MyClass.new` 方法,以调用其超类的 `new` 方法。然后我创建了一个字符串实例变量,`@anewvar`。那么我最终得到了什么?不是,正如你可能想象的那样,一个包含字符串变量的新 MyClass 对象。记住,在 Ruby 中,方法最后评估的表达式是该方法的返回值。这里 `new` 方法最后评估的表达式是一个字符串。我评估这个:

ob = MyClass.new( "hello world" )


我展示了新创建的 `ob` 对象及其类:

puts( ob )
puts( ob.class )


这就是输出结果:

HELLO WORLD
String


这证明了 `MyClass.new` 返回一个字符串,并且这个字符串(*而不是* MyClass 对象)被分配给了变量 `ob`。如果你觉得这很困惑,不要慌张。这个故事的意义在于,覆盖 `new` *确实* 是令人困惑的,通常不是一个好主意。除非你有非常充分的理由这样做,否则你应该避免尝试覆盖 `new` 方法。

# 单例方法

单例方法是一个属于单个对象而不是整个类的方法。Ruby 类库中的许多方法都是单例方法。这是因为,如前所述,每个类都是 Class 类型的一个对象。或者,简单地说,每个类的类是 Class。这对所有类都适用——无论是你自己定义的还是 Ruby 类库提供的:

*class_classes.rb*

class MyClass
end

puts( MyClass.class ) #=> Class
puts( String.class ) #=> Class
puts( Object.class ) #=> Class
puts( Class.class ) #=> Class
puts( IO.class ) #=> Class


现在,一些类也有类方法——即属于 Class 对象本身的方法。从某种意义上说,这些是 Class 对象的单例方法。确实,如果你评估以下内容,你会看到一个与 IO 类方法名称匹配的方法名称数组:

p( IO.singleton_methods )


这将显示以下内容:

[:new, :open, :sysopen, :for_fd, :popen, :foreach, :readlines,
:read, :binread, :select, :pipe, :try_convert, :copy_stream]


如前所述,当你编写自己的类方法时,你通过在方法名前加上类的名称来这样做:

def MyClass.classMethod


结果表明,在创建特定对象的单例类时,你可以使用类似的语法。这次,你需要在方法名前加上对象名:

def myObject.objectMethod


查找对象的祖先类

最终,所有类都从 Object 类派生。在 Ruby 1.9 中,Object 类本身是从 BasicObject 类派生的(参见第二章)。即使是`Class`类也是如此!为了证明这一点,尝试运行*class_hierarchy.rb*程序:

def showFamily( aClass )
if (aClass != nil) then
puts( "#{aClass} :: about to recurse with aClass.superclass =

{aClass.superclass.inspect}" )

    showFamily( aClass.superclass )
end

end


将类名传递给此方法以追踪其祖先类的家族树。例如,尝试这样做:

showFamily(File)


在 Ruby 1.9 中,这将显示以下内容:

File :: about to recurse with aClass.superclass = IO
IO :: about to recurse with aClass.superclass = Object
Object :: about to recurse with aClass.superclass = BasicObject
BasicObject :: about to recurse with aClass.superclass = nil


*class_hierarchy.rb*

让我们来看一个具体的例子。假设你有一个包含许多不同物种的 Creature 对象的程序(也许你是一位兽医,动物园的负责人,或者像本书的作者一样,是一位热情的冒险游戏玩家);每个生物都有一个名为`talk`的方法,用于显示每个生物通常发出的声音。

这是我的 Creature 类和一些生物对象:

*singleton_meth1.rb*

class Creature
def initialize( aSpeech )
@speech = aSpeech
end

def talk
puts( @speech )
end
end

cat = Creature.new( "miaow" )
dog = Creature.new( "woof" )
budgie = Creature.new( "Who's a pretty boy, then!" )
werewolf = Creature.new( "growl" )


突然间,你意识到这些生物中只有一个具有额外的特殊行为。在满月之夜,狼人不仅会说话(“咆哮”),还会嗥叫(“How-oo-oo-oo-oo!”)。它真的需要一个`howl`方法。

你可以将这样的方法添加到 Creature 类中,但最终你会得到嗥叫的狗、猫和鹦鹉——这并不是你想要的。你可以创建一个新的从 Creature 派生的 Werewolf 类,但你将永远只有一个狼人(遗憾的是,它们是一种濒危物种),所以为什么你需要一个整个类来代表它呢?难道不是更有意义有一个与所有其他生物对象相同的狼人*对象*,除了它还有一个`howl`方法吗?好吧,让我们通过给狼人一个它自己的单例方法来实现这一点。下面是操作步骤:

def werewolf.howl
puts( "How-oo-oo-oo-oo!" )
end


哎,你可以做得更好!它只在满月时嗥叫,所以让我们确保,如果月亮是新的,它只是咆哮。这是我的完成方法:

def werewolf.howl
if FULLMOON then
puts( "How-oo-oo-oo-oo!" )
else
talk
end
end


注意,尽管这个方法是在 Creature 类外部声明的,但它仍然能够调用实例方法`talk`。这是因为`howl`方法现在“位于”狼人对象内部,因此在那个对象内部与`talk`方法具有相同的范围。然而,它并不位于狼人同伴的任何地方;`howl`方法只属于他一个人。尝试调用`budgie.howl`,Ruby 会告诉你`howl`是一个未定义的方法。

现在,如果你正在为自己的代码进行调试,由于未定义的方法而导致程序崩溃可能是可以接受的;然而,如果你的程序在“最终用户”的广阔、恶劣的世界中这样做,这绝对是不可以接受的。

如果你认为未定义的方法可能会成为问题,你可以在尝试使用它之前通过测试单例方法是否存在来采取避免措施。Object 类有一个`singleton_methods`方法,它返回一个包含单例方法名称的数组。你可以使用 Array 类的`include?`方法测试一个方法名称是否包含。例如,在*singleton_meth2.rb*中,我编写了一个“打开盒子”游戏,其中包含多个 Box 对象,只有一个盒子打开时包含奖品。我给这个特殊盒子对象命名为`starprize`,并给它添加了一个名为`congratulate`的单例方法:

*singleton_meth2.rb*

starprize = Box.new( "Star Prize" )
def starprize.congratulate
puts( "You've won a fabulous holiday in Grimsby!" )
end


当`starprize`盒子被打开时,应该调用`congratulate`方法。这段代码(其中`item`是一个 Box 对象)确保当打开其他盒子时,不会调用这个方法(它不存在于任何其他对象中):

if item.singleton_methods.include?("congratulate") then
item.congratulate
end


检查方法有效性的另一种方式是将该方法名称作为符号(一个以冒号开头的标识符)传递给 Object 类的`respond_to?`方法:

if item.respond_to?( :congratulate ) then
item.congratulate
end


### 注意

你将在第二十章中看到处理不存在方法的另一种方法。

# 单例类

单例方法是一个属于单个对象的方法。另一方面,单例类是一个定义单个对象的类。困惑?我也是。让我们更仔细地看看。

假设你创建了数十个对象,每个对象都是 Object 类的一个实例。自然地,它们都可以访问继承的方法,例如`inspect`和`class`。但现在你决定你只想有一个特殊对象(为了多样性,让我们称他为`ob`),它有一个特殊的方法(让我们称它为`blather`)。

你不想为这个单一对象定义一个全新的类,因为你永远不会再次创建任何具有`blather`方法的更多对象。所以,你创建了一个专门为小`ob`设计的类。

你不需要给这个类命名。你只需通过在`class`关键字和对象名称之间放置一个`<<`来告诉它将自己附加到`ob`上。然后你以通常的方式向这个类中添加代码:

*singleton_class.rb*

ob = Object.new
# singleton class
class << ob
def blather( aStr )
puts("blather, blather #{aStr}")
end
end


现在 `ob`,只有 `ob`,不仅具有 Object 类的所有常规方法;它还具有它自己的特殊匿名类的方法(这里只是 `blather` 方法,原则上可以有更多):

ob.blather( "weeble" ) #=> "blather, blather weeble"


如果你一直很注意,你可能已经注意到单例类似乎在做与单例方法非常相似的事情。使用单例类,我可以创建一个对象,然后添加封装在匿名类中的额外方法。使用单例方法,我可以创建一个对象,然后逐个添加方法:

*singleton_class2.rb*

ob2 = Object.new

def ob2.blather( aStr ) # <= this is a singleton method
puts( "grippity, grippity #{aStr}" )
end

ob2.blather( "ping!" ) #=> grippity, grippity ping!


同样,我可以重写“star prize”程序。在前一个版本中,我为名为 `starprize` 的对象添加了一个单例方法 `congratulate`。我同样可以创建一个包含 `congratulate` 方法的单例类:

starprize = MyClass.new( "Star Prize" )

class << starprize
def congratulate
puts( "You've won a fabulous holiday in Grimsby!" )
end
end


实际上,这种相似性远不止表面。前述代码的最终结果是 `congratulate` 成为了 `starprize` 的单例方法。我可以通过检查 `item` 对象可用的单例方法数组是否包含名称 `congratulate` 来验证这一点:

if item.singleton_methods.include?(:congratulate) # Ruby 1.9


在 Ruby 1.9 中,`singleton_methods` 方法返回一个表示方法名的符号数组。这就是为什么我在前面的代码中使用符号 `:congratulate`。然而,在 Ruby 1.8 中,`singleton_methods` 返回一个字符串数组。所以,如果你使用 Ruby 1.8,你应该确保使用以下使用字符串参数 `"congratulate"` 的测试:

if item.singleton_methods.include?("congratulate") # Ruby 1.8


### 注意

单例方法和单例类之间的区别是什么?简短的回答是,没有太多区别。这两个语法提供了向特定对象添加方法的不同方式,而不是将这些方法构建到其定义类中。

# 重写方法

有时候你可能想重新定义某个类中已经存在的方法。你之前已经这样做过了,例如,当你创建了具有自己的 `to_s` 方法的类以返回字符串表示时。从 Object 类向下,每个 Ruby 类都有一个 `to_s` 方法。Object 类的 `to_s` 方法返回类名和对象的唯一标识符的十六进制表示。然而,许多 Ruby 类都有自己的特殊版本的 `to_s`。例如,`Array.to_s` 连接并返回数组中的值。

当一个类中的方法替换了祖先类中同名的那个方法时,我们称其为*重写*该方法。你可以重写标准类库中定义的方法,如`to_s`,以及你自己的类中定义的方法。如果你需要向现有方法添加新行为,记得在重写方法的开头使用`super`关键字调用超类的方法。

这里有一个例子:

*override.rb*

class MyClass
def sayHello
return "Hello from MyClass"
end

def sayGoodbye
return "Goodbye from MyClass"
end
end

class MyOtherClass < MyClass
def sayHello #overrides (and replaces) MyClass.sayHello
return "Hello from MyOtherClass"
end

    # overrides MyClass.sayGoodbye   but first calls that method
    # with super. So this version "adds to" MyClass.sayGoodbye
def sayGoodbye
    return super << " and also from MyOtherClass"
end

    # overrides default to_s method
def to_s
    return "I am an instance of the #{self.class} class"
end

end


# 公共、受保护和私有方法

在某些情况下,你可能想限制你方法的“可见性”,以确保它们不能被出现在方法中的类外部的代码调用。

这可能在你定义的类需要执行某些不打算公开消费的功能时很有用。通过限制这些方法的访问权限,你可以防止程序员为了自己的恶意目的使用它们。这意味着你可以在以后阶段更改这些方法的实现,而不用担心会破坏其他人的代码。

Ruby 提供了三种方法可访问级别:

+   public

+   protected

+   private

如其名所示,公共方法是可访问性最高的,而私有方法是可访问性最低的。除非你明确指定,否则所有方法都是公共的。当一个方法是公共的,它就可以被定义其类的对象之外的世界使用。

当一个方法是私有的,它只能由定义其类的对象内部的其他方法使用。

受保护的方法通常与私有方法以相同的方式工作,但有一个微小但重要的区别:除了对当前对象的方法可见外,当第二个对象在第一个对象的作用域内时,受保护的方法对同一类型的对象也是可见的。

当你看到工作示例时,私有和受保护方法之间的区别可能更容易理解。考虑这个类:

*pub_prot_priv.rb*

class MyClass

private
    def priv
         puts( "private" )
    end

protected
    def prot
         puts( "protected" )
    end

public
def pub
puts( "public" )
end

    def useOb( anOb )
         anOb.pub
         anOb.prot
         anOb.priv
    end

end


我声明了三个方法,每个可访问级别一个。这些级别是通过在 `private`、`protected` 或 `public` 前放置一个或多个方法来设置的。指定的可访问级别对所有后续方法都有效,直到指定了另一个访问级别。

### 注意

`public`、`private` 和 `protected` 可能看起来像关键字。但实际上,它们是 Module 类的方法。

最后,我的类有一个公共方法 `useOb`,它接受一个 `MyOb` 对象作为参数,并调用该对象的三个方法 `pub`、`prot` 和 `priv`。现在,让我们看看如何使用 `MyClass` 对象。首先,我将创建该类的两个实例:

myob = MyClass.new
myob2 = MyClass.new


现在,我尝试依次调用这三个方法:

myob.pub # This works! Prints out "public"
myob.prot # This doesn't work! I get an error
myob.priv # This doesn't work either - another error


从前面的例子中,看起来公共方法(正如预期的那样)可以从对象外部看到。但私有和受保护的方法都是不可见的。既然如此,受保护的方法有什么用?另一个例子应该有助于澄清这一点:

myob.useOb( myob2 )


这次,我正在调用 `myob` 对象的公共方法 `useOb`,并将第二个对象 `myob2` 作为参数传递给它。需要注意的是,`myob` 和 `myob2` 是同一类的实例。现在,回想一下我之前说过的话:*除了对当前对象的方法可见外,当第二个对象在第一个对象的作用域内时,受保护的方法对同一类型的对象也是可见的*。

这可能听起来像是胡言乱语。让我们看看我是否能从中找出一些道理。在程序中,当 `myob2` 作为参数传递给 `myob` 的一个方法时,第一个 MyClass 对象(这里 `myob`)在其作用域内有一个第二个 MyClass 对象。当这种情况发生时,你可以将 `myob2` 视为存在于 `myob` “内部”。现在 `myob2` 与“包含”对象 `myob` 共享作用域。在这种情况下——当两个相同类的对象位于该类定义的作用域内时——该类中任何对象的受保护方法都变得可见。

在当前情况下,对象 `myob2` 的受保护方法 `prot`(或者至少是“接收”`myob2` 的参数,这里称为 `anob`)变得可见并可执行。然而,它的私有参数是不可见的:

def useOb( anOb )
anOb.pub
anOb.prot # protected method can be called
anOb.priv # calling a private method results in an error
end


深入挖掘

在这里,你将学习更多关于方法内部代码的可见性以及定义单例方法的另一种方式。

后代类中的受保护和私有方法

本章中描述的相同访问规则也适用于调用祖先和后代对象的方法。也就是说,当你将一个对象传递给一个方法(作为参数)时,该方法的接收对象(换句话说,方法所属的对象)与参数对象具有相同的类,参数对象可以调用该类的公共和受保护方法,但不能调用其私有方法。

例如,查看 *protected.rb* 程序。在这里,我创建了一个名为 `myob` 的 MyClass 对象和一个名为 `myotherob` 的 MyOtherClass 对象,其中 MyOtherClass 继承自 MyClass:

*protected.rb*

class MyClass

private
    def priv( aStr )
        return aStr.upcase
    end

protected
    def prot( aStr )
        return aStr << '!!!!!!'
    end

public

    def exclaim( anOb )  # calls a protected method
        puts( anOb.prot( "This is a #{anOb.class} - hurrah" ) )
    end

    def shout( anOb )    # calls a private method
        puts( anOb.priv( "This is a #{anOb.class} - hurrah" ) )
    end

end

class MyOtherClass < MyClass

end

class MyUnrelatedClass

end


现在,我创建了这三个类中的每一个对象,并尝试将 `myotherob` 作为参数传递给 `myob` 的公共方法 `shout`:

myob = MyClass.new
myotherob = MyOtherClass.new
myunrelatedob = MyUnrelatedClass.new


如果你从代码存档中加载此程序,你会看到其中包含许多行代码,这些代码中的三个对象试图执行 `shout` 和 `exclaim` 方法。其中许多尝试注定会失败,因此已被注释掉。然而,在测试代码时,你可能希望逐个取消注释每个方法调用以查看结果。这是我的第一次尝试:

myob.shout( myotherob ) # fails


在这里,`shout` 方法在参数对象上调用私有方法 `priv`:

def shout( anOb ) # calls a private method
puts( anOb.priv( "This is a #{anOb.class} - hurrah" ) )
end


这将不起作用!Ruby 抱怨 `priv` 方法是私有的。

类似地,如果我将顺序反过来——也就是说,通过传递祖先对象 `myob` 作为参数并在后代对象上调用 `shout` 方法——我也会遇到相同的错误:

myotherob.shout( myob ) # fails


MyClass 类还有一个公共方法,`exclaim`。这个方法调用受保护的方法,`prot`:

def exclaim( anOb ) # calls a protected method
puts( anOb.prot( "This is a #{anOb.class} - hurrah" ) )
end


现在,我可以将 MyClass 对象 `myob` 或 MyOtherClass 对象 `myotherob` 作为参数传递给 `exclaim` 方法,当调用受保护的方法时不会发生错误:

myob.exclaim( myotherob ) # This is OK
myotherob.exclaim( myob ) # And so is this...
myob.exclaim( myunrelatedob ) # But this won't work


不言而喻,这只有在两个对象(点左侧的接收对象和传递给方法的参数)共享相同的继承线时才有效。如果你传递一个无关的对象作为参数,无论它们的保护级别如何,你都无法调用接收类的任何方法。

侵犯私有方法的隐私

私有方法整个点在于它不能从属于它的对象的作用域之外被调用。所以,这不会起作用:

*send.rb*

class X
private
def priv( aStr )
puts("I'm private, " << aStr)
end
end

ob = X.new
ob.priv( "hello" ) # This fails


然而,结果证明,Ruby 提供了一种“退出条款”(或者我或许应该说“进入条款”)的形式,即名为 `send` 的方法。

`send` 方法调用与符号(以冒号开始的标识符,如 `:priv`)名称匹配的方法,该符号作为第一个参数传递给 `send`,如下所示:

ob.send( :priv, "hello" ) # This succeeds


在符号(如字符串“hello”)之后提供的任何参数都按正常方式传递给指定的方法。

使用 `send` 来获取对私有方法的公共访问通常不是一个好主意。毕竟,如果你需要访问某个方法,为什么一开始要将其设置为私有呢?谨慎使用此技术或根本不使用。

单例类方法

之前,我通过将方法名附加到类名上来创建类方法,如下所示:

def MyClass.classMethod


有一种“快捷”语法来做这件事。以下是一个示例:

*class_methods3.rb*

class MyClass

def MyClass.methodA
puts("a")
end

class << self
def methodB
puts("b")
end

  def methodC
     puts("c")
  end

end

end


这里,`methodA`、`methodB` 和 `methodC` 都是 MyClass 的类方法;`methodA` 是使用之前的方法声明的语法:

def ClassName.methodname


但 `methodB` 和 `methodC` 是使用实例方法的语法声明的:

def methodname


所以,为什么它们最终成为类方法呢?这完全是因为方法声明被放置在这个代码中:

class << self
# some method declarations
end


这可能会让你想起声明单例类所使用的语法。例如,在 *singleton_class.rb* 程序中,你可能还记得我首先创建了一个名为 `ob` 的对象,然后给它添加了一个名为 `blather` 的方法:

class << ob
def blather( aStr )
puts("blather, blather #{aStr}")
end
end


这里 `blather` 方法是 `ob` 对象的单例方法。同样,在 *class_methods3.rb* 程序中,`methodB` 和 `methodC` 方法是 `self` 的单例方法——而 `self` 正好是 MyClass 类。你可以通过使用 `<<` 后跟类名,从类定义外部添加单例方法,如下所示:

class << MyClass
def methodD
puts( "d" )
end
end


最后,代码通过首先打印所有可用的单例方法名称,然后调用它们来检查所有四个方法确实都是单例方法:

puts( MyClass.singleton_methods.sort )
MyClass.methodA
MyClass.methodB
MyClass.methodC
MyClass.methodD


这会显示以下内容:

methodA
methodB
methodC
methodD
a
b
c
d


嵌套方法

你可以嵌套方法;也就是说,你可以编写包含其他方法的方法。这为你提供了一种将长方法分成可重用块的方法。例如,如果方法 `x` 需要在几个不同的点执行计算 `y`,你可以在 `x` 方法(以下示例中的方法称为 `outer_x`、`nested_y` 和 `nested_z` 以便清晰)中放置 `y` 方法:

*nested_methods.rb*

class X

def outer_x
print( "x:" )

  def nested_y
     print("ha! ")
  end

  def nested_z
     print( "z:" )
     nested_y
  end

  nested_y
  nested_z

end

end


嵌套方法最初在它们定义的作用域之外是不可见的。所以,在上面的例子中,尽管`nested_y`和`nested_z`可以从`outer_x`内部调用,但它们可能无法被任何其他代码调用:

ob = X.new
ob.outer_x #=> x:ha! z:ha!


如果在之前的代码中,你调用的是`ob.outer_x`而不是`ob.nested_y`或`ob.nested_z`,你会看到一个错误信息,因为在这个阶段`nested_y`和`nested_z`方法将不可见。然而,当你运行一个包含嵌套方法的方法时,那些嵌套方法*将会*被带到该方法的外部作用域中!

*nested_methods2.rb*

class X
def x
print( "x:" )
def y
print("y:")
end

  def z
     print( "z:" )
     y
  end

end
end

ob = X.new
ob.x #=> x:
puts
ob.y #=> y:
puts
ob.z #=> z:y:


为了看到这个的另一个例子,尝试再次运行*nested_methods.rb*代码,但这次取消注释所有三个方法调用。这次,当`outer_x`方法执行时,它会将`nested_y`和`nested_z`带到作用域中,因此对这两个嵌套方法的调用现在成功了:

ob.outer_x #=> x:ha! z:ha!
ob.nested_y #=> ha!
ob.nested_z #=> z:ha!


方法名

最后一点,值得提一下的是,Ruby 中的方法名几乎总是以小写字母开头,就像这样:

def fred


然而,这只是一个*约定*,并不是*义务*。也可以以大写字母开头命名方法名,就像这样:

def Fred


由于`Fred`方法看起来像是一个常量(它以大写字母开头),在调用它时你需要通过添加括号来告诉 Ruby 它是一个方法:

*method_names.rb*

Fred # <= Ruby complains 'uninitialized constant'
Fred() # <= Ruby calls the Fred method


总体来说,坚持使用以小写字母开头的方法名约定会更好。


# 第八章。传递参数和返回值

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860138.png.jpg)

在本章中,你将了解许多传递参数和从方法返回值的效果(以及副作用)。首先,我将花一点时间总结一下你到目前为止所使用的方法类型。

# 总结实例、类和单例方法

实例方法是在类定义内部声明的,并旨在由特定的对象或类的“实例”使用,如下所示:

*methods.rb*

class MyClass
# declare instance method
def instanceMethod
puts( "This is an instance method" )
end
end

# create object

ob = MyClass.new
# use instance method
ob.instanceMethod


类方法可以在类定义内部声明,在这种情况下,方法名可能前面有类名,或者一个 `class << self` 块可以包含一个“正常”的方法定义。无论哪种方式,类方法都是为类本身使用,而不是为特定的对象使用,如下所示:

class MyClass
# a class method
def MyClass.classmethod1
puts( "This is a class method" )
end

# another class method
class << self
    def classmethod2
         puts( "This is another class method" )
    end
end

end

# call class methods from the class itself

MyClass.classmethod1
MyClass.classmethod2


单例方法是添加到单个对象中的方法,不能被其他对象使用。单例方法可以通过将方法名附加到对象名后跟一个点来定义,或者将一个“正常”的方法定义放在一个 *`ObjectName`* `<< self` 块中,如下所示:

create object

ob = MyClass.new

# define a singleton method

def ob.singleton_method1
puts( "This is a singleton method" )
end

# define another singleton method

class << ob
def singleton_method2
puts( "This is another singleton method" )
end
end

# use the singleton methods

ob.singleton_method1
ob.singleton_method2


# 返回值

在许多编程语言中,区分了返回值给调用代码的函数或方法和不返回值的函数或方法。例如,在 Pascal 中,一个 *函数* 会返回一个值,但一个 *过程* 不会。Ruby 中没有这样的区分。所有方法总是返回一个值,尽管当然你不必使用它。

当未指定返回值时,Ruby 方法返回最后评估的表达式的结果。考虑这个方法:

*return_vals.rb*

def method1
a = 1
b = 2
c = a + b # returns 3
end


最后评估的表达式是 `a + b`,它恰好返回 3,因此这是此方法返回的值。可能经常会有你不想返回最后评估的表达式的情况。在这种情况下,你可以使用 `return` 关键字指定返回值:

def method2
a = 1
b = 2
c = a + b
return b # returns 2
end


方法不一定要进行任何赋值以返回一个值。如果简单数据恰好是方法中最后评估的内容,那么它将是方法返回的值。如果没有内容被评估,则返回 `nil`:

def method3
"hello" # returns "hello"
end

def method4
a = 1 + 2
"goodbye" # returns "goodbye"
end

def method5
end # returns nil


我的编程偏见是尽可能编写清晰且无歧义的代码。因此,每当我计划使用方法返回的值时,我更喜欢使用 `return` 关键字来指定它;只有在我不打算使用返回值时,我才省略这个关键字。然而,这并非强制性的——Ruby 将选择权留给了你。

# 返回多个值

但是,当你需要一种方法返回多个值时怎么办?在其他程序语言中,你可能可以通过传递引用(原始数据项的指针)而不是值(数据的副本)来实现“伪造”这一功能;当你改变“引用”参数的值时,你会改变原始值,而无需明确地将任何值返回给调用代码。

Ruby 不会区分“按引用”和“按值”,因此这项技术对你不可用(尽管你很快就会看到一些规则的例外)。然而,Ruby 能够一次性返回多个值,如下所示:

*return_many.rb*

def ret_things
greeting = "Hello world"
a = 1
b = 2.0
return a, b, 3, "four", greeting, 6 * 10
end


多个返回值被放入一个数组中。如果你要评估 `ret_things.class`,Ruby 会告诉你返回的对象是一个数组。然而,你可以显式地返回不同的集合类型,例如一个哈希表:

def ret_hash
return {'a'=>'hello', 'b'=>'goodbye', 'c'=>'fare thee well'}
end


# 默认值和多个参数

Ruby 允许你为参数指定默认值。默认值可以在方法参数列表中使用常规赋值运算符进行分配:

def aMethod( a=10, b=20 )


如果将未分配的变量传递给该方法,则将分配默认值。但是,如果传递已分配的变量,则分配的值将优先于默认值。在这里,我使用 `p()` 方法来检查和打印返回值:

def aMethod( a=10, b=20 )
return a, b
end

p( aMethod ) #=> displays: [10, 20]
p( aMethod( 1 )) #=> displays: [1, 20]
p( aMethod( 1, 2 )) #=> displays: [1, 2]


在某些情况下,一个方法可能需要能够接收不确定数量的参数——比如,例如处理可变长度项目列表的方法。在这种情况下,你可以通过在最后一个参数前加上星号来“清除”任何数量的尾随项:

*default_args.rb*

def aMethod( a=10, b=20, c=100, *d )
return a, b, c, d
end

p( aMethod( 1,2,3,4,6 ) ) #=> displays: [1, 2, 3, [4, 6]]


# 赋值和参数传递

大多数情况下,Ruby 方法有两个访问点——就像进入和离开房间的门。参数列表提供了进入的方式;返回值提供了离开的方式。对输入参数所做的修改不会影响原始数据,简单的理由是当 Ruby 评估一个表达式时,该评估的结果会创建一个新的对象,因此对参数所做的任何更改只会影响新的对象,而不会影响原始数据。但这个规则也有例外,我现在会向你展示。

让我们从最简单的情况开始看起——一个接受一个命名参数并返回另一个值的方法:

*in_out.rb*

def change( x )
x += 1
return x
end


表面上看,你可能会认为你在这里处理的是一个单一的对象 `x`:对象 `x` 进入 `change` 方法,并且相同的对象 `x` 被返回。实际上并非如此。一个对象进入(参数),另一个不同的对象出来(返回值)。你可以通过使用 `object_id` 方法来轻松验证这一点,该方法显示一个唯一标识程序中每个对象的数字:

num = 10
puts( "num.object_id=#{num.object_id}" )
num = change( num )
puts( "num.object_id=#{num.object_id}" )


变量标识符 `num` 在调用 `change` 方法前后是不同的。这表明尽管变量名保持不变,但 `change` 方法返回的 `num` 对象与发送给它的 `num` 对象是不同的。

方法调用本身与对象的改变无关。你可以通过运行 *method_call.rb* 来验证这一点。这仅仅是将 `num` 对象传递给 `change` 方法并返回它:

*method_call.rb*

def nochange( x )
return x
end


在这种情况下,`object_id` 在返回 `num` 之后与发送到方法之前相同。换句话说,进入方法的对象与再次出来的对象是同一个对象。这导致了一个不可避免的结论,那就是 `change` 方法(`x += 1`)中的 `assignment` 导致了新对象的创建。

但赋值本身并不是全部的解释。如果你只是将一个变量赋值给自己,不会创建新的对象:

*assignment.rb*

num = 10
num = num # a new num object is not created


如果你现在显示 `num` 变量的 `object_id`,赋值前后数字相同,这证明了这确实是一个相同的对象。那么,如果你将对象赋值为它已经拥有的相同值呢?

num = 10
num = 10 # a new num object is not created


再次强调,赋值后 `object_id` 没有改变。这表明仅赋值本身并不一定会创建新对象。现在让我们尝试赋一个新的值:

num = 10
num += 1 # this time a new num object is created


这次,如果你在赋值前后显示 `num.object_id`,你会看到不同的数字——比如说赋值前是 21,赋值后是 23。实际的数字是由 Ruby 自动确定的,可能不同。重要的是要理解,不同的对象 ID 表示不同的对象。如果相同的变量在赋值时返回不同的 `object_id`,这意味着已经创建了新的对象。

大多数数据项被视为唯一的,所以一个字符串“hello”被认为与另一个字符串“hello,”不同,一个浮点数 10.5 被认为与另一个浮点数 10.5 不同。因此,任何字符串或浮点数的赋值都会创建一个新的对象。

但当与整数一起工作时,只有当赋值值与之前的值不同时,才会创建新对象。你可以在赋值号的右侧进行所有种类的复杂操作,但如果产生的值与原始值相同,则不会创建新对象。

num = 11
puts( "num.object_id=#{num.object_id}" )
num = (((num + 1 - 1) * 100) / 100)
puts( "num.object_id=#{num.object_id}" )


在前面的代码中,第一次赋值创建了一个具有整数值 11 的新 `num` 对象。即使下一个赋值使用了相当复杂的表达式的结果,这个值仍然是 11。由于 `num` 的值没有改变,因此没有创建新的对象,它的 `object_id` 保持不变:

num.object_id=23
num.object_id=23


# 整数是特殊的

在 Ruby 中,整数(或 Fixnum)有一个固定的身份。每个数字 10 的实例或被赋予值 10 的变量都将具有相同的 `object_id`。这不能适用于其他数据类型。例如,10.5 这样的浮点数或“hello world”这样的字符串的每个实例都将是一个具有唯一 `object_id` 的不同对象。请注意,当你将整数赋给变量时,该变量将具有整数的 `object_id`。但当你将其他类型的数据赋给变量时,即使每次赋值的数据本身相同,也会创建一个新的对象:

*object_ids.rb*

10 and x after each assignment are the same object

puts( 10.object_id )
x = 10
puts( x.object_id )
x = 10
puts( x.object_id )

10.5 and x after each assignment are 3 different objects!

puts( 10.5.object_id )
x = 10.5
puts( x.object_id )
x = 10.5
puts( x.object_id )


但为什么这一切都这么重要?

这很重要,因为有一些罕见的例外情况。正如我之前所说的,大多数时候,一个方法有一个明确的进入方式和一个明确的退出方式。一旦参数进入方法,它就进入了一个封闭的房间。任何在该方法之外的外部代码都无法了解对参数所做的任何更改,直到它以返回值的形态再次出现。这实际上是“纯”面向对象的一个深层次秘密。方法的具体实现细节应该,原则上,被隐藏起来,或者*封装*。这确保了对象之外的外部代码不能依赖于对象内部发生的事情。

# 单向进入,单向退出原则

在大多数现代面向对象的语言,如 Java 和 C#中,封装和信息隐藏并没有得到严格的强制执行。另一方面,在 Smalltalk——最著名和最有影响力的面向对象语言中——封装和信息隐藏是基本原理:如果你将变量`x`发送到方法`y`,并且`x`的值在`y`中被更改,你无法从方法外部获得`x`的更改后的值——*除非方法明确返回该值*。

封装或信息隐藏?

通常这两个术语是互换使用的。然而,要吹毛求疵的话,它们之间还是存在差异。*封装*指的是将对象的“状态”(其数据)和可能改变或查询其状态的运算(其方法)组合在一起。*信息隐藏*指的是数据被封闭起来,只能通过定义良好的进出路径访问——在面向对象术语中,这暗示了“访问器方法”来获取或返回值。在过程式语言中,信息隐藏可能采取其他形式;例如,你可能必须定义接口从代码“单元”或“模块”而不是从对象中检索数据。

在面向对象的概念中,封装和信息隐藏几乎是同义的——真正的封装必然意味着对象的内部数据是隐藏的。然而,许多现代面向对象的语言,如 Java、C#、C++和 Object Pascal,在强制执行信息隐藏的程度(如果有的话)上相当宽容。

通常,Ruby 遵循这一原则:参数进入方法,但方法内部所做的任何更改都无法从外部访问,除非 Ruby 返回更改后的值:

*hidden.rb*

def hidden( aStr, anotherStr )
anotherStr = aStr + " " + anotherStr
return anotherStr.reverse
end

str1 = "dlrow"
str2 = "olleh"
str3 = hidden(str1, str2)
puts( str1 ) #=> dlrow
puts( str2 ) #=> olleh
puts( str3 ) #=> hello world


在之前的代码中,第二个对象`str2`的字符串值被接收为`hidden`方法的`anotherStr`参数。该参数被分配了一个新的字符串值并反转。即便如此,原始变量`str1`或`str2`都没有改变。只有被分配返回值`str3`的变量包含了更改后的“hello world”字符串。

结果表明,在某些情况下,传递给 Ruby 方法的参数可以像其他语言的“按引用”参数一样使用(也就是说,在方法内部做出的更改可能会影响方法外部的变量)。这是因为一些 Ruby 方法修改了原始对象,而不是返回一个值并将其分配给新对象。

例如,有一些以感叹号结尾的方法会改变原始对象。同样,String 的追加方法`<<`将右侧的字符串连接到左侧的字符串,但在过程中不会创建新的字符串对象:因此,左侧字符串的值被修改,但字符串对象本身保留了其原始的`object_id`。

这种做法的后果是,如果你在方法中使用`<<`运算符而不是`+`运算符,你的结果将会改变:

*not_hidden.rb*

def nothidden( aStr, anotherStr )
anotherStr = aStr << " " << anotherStr
return anotherStr.reverse
end

str1 = "dlrow"
str2 = "olleh"
str3 = nothidden(str1, str2)
puts( str1 ) #=> dlrow olleh
puts( str2 ) #=> olleh
puts( str3 ) #=> hello world


在前面的代码中,`anotherStr`参数使用`<<`与`aStr`参数连接,并使用`<<`返回的结果字符串被反转。如果严格实施信息隐藏,这可能会产生与上一个程序相同的结果,即`str1`和`str2`将保持不变。然而,使用`<<`产生了深远的影响,因为它导致在`nothidden`方法内部对`aStr`参数所做的修改改变了方法外部的`str1`对象的值。

顺便说一下,如果将`nothidden`方法放入一个单独的类中,这种行为也会相同:

*nothidden2.rb*

class X
def nothidden( aStr, anotherStr )
anotherStr = aStr << " " << anotherStr
return anotherStr.reverse
end
end
ob = X.new
str1 = "dlrow"
str2 = "olleh"
str3 = ob.nothidden(str1, str2)
puts( str1 ) #=> dlrow olleh


这表明,在某些情况下,对象方法的内部实现细节可能会意外地改变调用它的代码。通常,隐藏实现细节更安全;否则,当类内部重写代码时,这些更改可能会对使用该类的代码产生副作用。

# 修改接收者并返回新对象

你可能还记得,在第四章中,我区分了修改其接收者的方法和不修改其接收者的方法。(记住,*接收者*是“拥有”方法的对象。)在大多数情况下,Ruby 方法不会修改接收者对象。然而,一些方法,如以`!`结尾的方法,确实会修改它们的接收者。

*str_reverse.rb* 示例程序应该有助于阐明这一点。这表明,当你使用`reverse`方法时,例如,不会对接收者对象(即`str1`这样的对象)做出任何更改。但是,当你使用`reverse!`方法时,对象(其字母顺序被反转)会发生变化。即便如此,也不会创建新对象:在调用`reverse!`方法之前和之后,`str1`仍然是同一个对象。

在这里,`reverse`方法像大多数 Ruby 方法一样工作:它返回一个值,为了使用这个值,你必须将它分配给一个新的对象。考虑以下情况:

*str_reverse.rb*

str1 = "hello"
str1.reverse


在这里,`str1` 调用 `reverse` 后不受影响。它仍然具有值“hello”和它的原始 `object_id`。现在看看这个:

str1 = "hello"
str1.reverse!


这次,`str1` 被改变了(变成了“olleh”)。即便如此,也没有创建新的对象:`str1` 仍然具有它开始时的相同的 `object_id`。那么,看看这个:

str1 = "hello"
str1 = str1.reverse


这次,`str1.reverse` 产生的值被分配给了 `str1`。产生的值是一个新对象,所以 `str1` 现在分配了反转的字符串(“olleh”),并且它现在有一个新的 `object_id`。

参考示例程序 *concat.rb*,了解字符串连接方法 `<<` 的示例,它就像以 `!` 结尾的方法一样,修改接收器对象而不创建新对象(再次强调,当你运行代码时实际的 `object_id` 数字可能不同):

*concat.rb*

str1 = "hello" #object_id = 23033940
str2 = "world" #object_id = 23033928
str3 = "goodbye" #object_id = 23033916
str3 = str2 << str1
puts( str1.object_id ) #=> 23033940 # unchanged
puts( str2.object_id ) #=> 23033928 # unchanged
puts( str3.object_id ) #=> 23033928 # now the same as str2!


在这个例子中,`str1` 永远没有被修改,所以它始终具有相同的 `object_id`;`str2` 通过连接操作被修改。然而,`<<` 操作符不会创建一个新的对象,所以 `str2` 也保留了其原始的 `object_id`。

但 `str3` 在结束时与开始时是不同的对象,因为它被分配了这个表达式的值:`str2 << str1`。这个值恰好是 `str2` 对象本身,所以 `str3` 的 `object_id` 现在与 `str2` 的相同(也就是说,`str2` 和 `str3` *现在引用了同一个对象*)。

总结来说,那么,以 `!` 结尾的方法,如 `reverse!`,以及一些其他方法,如 `<<` 连接方法,会改变接收器对象的值。大多数其他方法不会改变接收器对象的值。要使用调用这些方法之一产生的新值,你必须将该值赋给一个变量(或将产生的值作为参数传递给一个方法)。

### 注意

事实是,只有少数方法会修改接收器对象,而大多数方法则不会,这看起来可能无害,但请注意:这种行为为你提供了通过“引用”检索参数值的能力,而不是检索显式返回的值。这样做会破坏封装,允许你的代码依赖于方法的内部实现细节。这可能导致不可预测的副作用,在我看来,应该避免。

# 依赖参数值而非显式返回值的潜在副作用

对于一个简单(但在现实世界的编程中可能严重的)例子,说明依赖参数的修改值而不是显式返回值如何引入对实现细节的不希望依赖,请参阅 *side_effects.rb*。这里有一个名为 `stringProcess` 的方法,它接受两个字符串参数,对它们进行操作,并返回结果:

*side_effects.rb*

def stringProcess( aStr, anotherStr )
aStr.capitalize!
anotherStr.reverse!.capitalize!
aStr = aStr + " " + anotherStr.reverse!
return aStr
end


假设练习的目标是取两个小写字符串,并返回一个将这两个字符串结合起来的单个字符串,用空格分隔,并且首尾字母大写。所以,两个原始字符串可能是“hello”和“world”,返回的字符串是“Hello worlD”。这没问题:

str1 = "hello"
str2 = "world"
str3 = stringProcess( str1, str2 )
puts( "#{str3}" ) #=> Hello worlD


但现在有一个急躁的程序员不愿意处理返回值。他注意到方法内部所做的修改改变了传入参数的值。所以,他心想!(他决定),他干脆使用这些参数本身!这是他的版本:

puts( "#{str1} #{str2}" ) #=> Hello worlD


通过使用输入参数的值`str1`和`str2`,他已经得到了与使用返回值`str3`相同的结果。然后他离开并编写了一个极其复杂的文本处理系统,有成千上万的代码依赖于这两个参数的改变值。

但现在,最初编写`stringProcess`方法的程序员决定原始实现效率低下或不够优雅,因此重新编写了代码,并确信返回值没有改变(如果发送“hello”和“world”作为参数,将返回“Hello worlD”,就像上一个版本一样):

def stringProcess( aStr, anotherStr )
myStr = aStr.capitalize!
anotherStr.reverse!.capitalize!
myStr = myStr + " " + anotherStr.reverse
return myStr
end

str1 = "hello"
str2 = "world"
str3 = stringProcess( str1, str2 )

puts( "#{str3}" ) #=> Hello worlD
puts( "#{str1} #{str2}" ) #=> Hello Dlrow


哎!但新的实现导致方法体内输入参数的值被改变。所以,那个急躁的程序员依赖这些*参数*而不是返回值的文本处理系统,现在充满了“hello Dlrow”这样的文本片段,而不是他预期的“Hello worlD”(实际上,他的程序正在处理莎士比亚的作品,所以一代演员最终会大声朗诵,“To eb or ton to eb, that si the noitseuq...”)。这是可以很容易避免的意外副作用,只要遵循单向输入、单向输出的原则。

# 并行赋值

我之前提到,一个方法可以返回多个值,这些值通过逗号分隔。通常,你希望将这些返回的值分配给一组匹配的变量。

在 Ruby 中,你可以通过并行赋值在单个操作中完成这个操作。这意味着你可以有多个左侧变量或赋值运算符,以及多个右侧值。右侧的值将按顺序分配给左侧的变量,如下所示:

*parallel_assign.rb*

s1, s2, s3 = "Hickory", "Dickory", "Dock"


这种能力不仅为你提供了快速进行多次赋值的方法;它还允许你交换变量的值(你只需改变赋值运算符两侧的顺序):

i1 = 1
i2 = 2

i1, i2 = i2, i1 #=> i1 is now 2, i2 is 1


你还可以从方法返回的值中进行多次赋值:

def returnArray( a, b, c )
a = "Hello, " + a
b = "Hi, " + b
c = "Good day, " + c
return a, b, c
end
x, y, z = returnArray( "Fred", "Bert", "Mary" )


如果你指定赋值号右侧的值比左侧的变量多,任何“剩余”的变量将被赋值为`nil`:

x, y, z, extravar = returnArray( "Fred", "Bert", "Mary" ) # extravar = nil


方法返回的多个值被放入一个数组中。当你将数组放在多个变量赋值的右侧时,其各个元素将被分配给每个变量,并且如果提供的变量太多,额外的变量将被分配`nil`:

s1, s2, s3 = ["Ding", "Dong", "Bell"]


深入挖掘

在本节中,我们探讨了一些参数传递和对象相等性的内部工作原理。我还讨论了括号对于代码清晰性的价值。

通过引用还是通过值?

之前我说过,Ruby 在“通过值”传递的参数和“通过引用”传递的参数之间没有区别。即便如此,如果你在互联网上搜索,很快就会发现 Ruby 程序员经常就参数的确切传递方式发生争论。在许多过程式编程语言,如 Pascal 或 C 中,传递值或引用的参数之间存在明显的区别。

“通过值”的参数是原始变量的“副本”;你可以将它传递给一个函数并对其进行操作,而原始变量的值保持不变。

相反,“通过引用”的参数是原始变量的“指针”。当这个指针传递给一个过程时,你传递的不是一个新的副本,而是指向存储原始数据的内存块的引用。因此,在过程中所做的任何更改都会影响原始数据,并必然影响原始变量的值。

那么,Ruby 是以哪种方式传递参数的呢?实际上,解决这个问题非常简单。如果 Ruby 通过值传递,那么它会复制原始变量,因此这个副本将具有不同的`object_id`。实际上,情况并非如此。尝试运行`*arg_passing.rb*`程序来证明这一点。

`*arg_passing.rb*`

def aMethod( anArg )
puts( "#{anArg.object_id}\n\n" )
end

class MyClass
end

i = 10
f = 10.5
s = "hello world"
ob = MyClass.new

puts( "#{i}.object_id = #{i.object_id}" )
aMethod( i )
puts( "#{f}.object_id = #{f.object_id}" )
aMethod( f )
puts( "#{s}.object_id = #{s.object_id}" )
aMethod( s )
puts( "#{ob}.object_id = #{ob.object_id}" )
aMethod( ob )


该程序在原始声明时以及将它们作为`aMethod()`方法的参数传递时,打印出整数、浮点数、字符串和自定义对象的`object_id`。在每种情况下,参数的 ID 与原始变量的 ID 相同,因此参数必须是通过引用传递的。

现在,在某种情况下,参数的传递可能“幕后”被“实现”为“通过值”。然而,这样的实现细节应该对 Ruby 解释器和编译器的编写者而不是 Ruby 程序员感兴趣。简单的事实是,如果你以“纯”面向对象的方式编程——通过将参数传递给方法,但只随后使用这些方法返回的值——那么实现细节(通过值或通过引用)对你来说将无关紧要。

尽管如此,由于 Ruby 有时可以修改参数(例如,使用 `!` 方法或 `<<`,如 修改接收者和返回新对象 中所述),一些程序员养成了使用参数自身修改后的值的习惯(相当于在 C 中使用按引用参数),而不是使用返回的值。在我看来,这是一种不良做法。它使你的程序依赖于方法的实现细节,因此应该避免。

作业是副本还是引用?

我之前说过,当某个表达式 *yield* 一个值时,会创建一个新的对象。所以,例如,如果你将新值赋给一个名为 `x` 的变量,赋值后的对象将与赋值前的对象不同(即,它将具有不同的 `object_id`):

x = 10 # this x has one object_id
x +=1 # and this x has a different one


但创建新对象的不是赋值操作,而是产生的值。在上面的例子中,`+=1` 是一个产生值的表达式(`x+=1` 等价于表达式 `x=x+1`)。

简单地将一个变量的值赋给另一个变量不会创建一个新的对象。所以,假设你有一个名为 `num` 的变量和另一个名为 `num2` 的变量。如果你将 `num2` 赋值给 `num`,这两个变量将引用同一个对象。你可以使用 Object 类的 `equal?` 方法来测试这一点:

*assign_ref.rb*

num = 11.5
num2 = 11.5

# num and num 2 are not equal

puts( "#{num.equal?(num2)}" ) #=> false

num = num2
# but now they are equal
puts( "#{num.equal?(num2)}" ) #=> true


相等性测试:== 还是 equal?

默认情况下(如 Ruby 的 `Kernel` 模块中定义的),使用 `==` 进行测试,当被测试的两个对象是同一个对象时返回 `true`。因此,如果值相同但对象不同,则返回 `false`:

*equal_tests.rb*

ob1 = Object.new
ob2 = Object.new
puts( ob1==ob2 ) #=> false


事实上,`==` 经常被 String 等类覆盖,然后当值相同但对象不同时返回 `true`:

s1 = "hello"
s2 = "hello"
puts( s1==s2 ) #=> true


因此,当你想要确定两个变量是否引用同一个对象时,`equal?` 方法更可取:

puts( ob1.equal?(ob2) ) #=> false
puts( s1.equal?(s2) ) #=> false


何时两个对象是相同的?

作为一般规则,如果你用 10 个值初始化 10 个变量,每个变量将引用不同的对象。例如,如果你这样创建两个字符串:

*identical.rb*

s1 = "hello"
s2 = "hello"


然后 `s1` 和 `s2` 将引用独立的对象。对于两个浮点数也是一样:

f1 = 10.00
f2 = 10.00


但是,如前所述,整数是不同的。创建具有相同值的两个整数,它们最终会引用同一个对象:

i1 = 10
i2 = 10


这甚至适用于字面整数值。如果有疑问,请使用 `equals?` 方法来测试两个变量或值是否引用了完全相同的对象:

10.0.equal?(10.0) # compare floats - returns false
10.equal?(10) # compare integers (Fixnums) - returns true


括号避免歧义

方法可能与局部变量有相同的名称。例如,你可能有一个名为 `name` 的变量和一个名为 `name` 的方法。如果你习惯于不带括号调用方法,那么可能不清楚你是在指方法还是变量。再次强调,括号可以避免歧义:

*parentheses.rb*

greet = "Hello"
name = "Fred"

def greet
return "Good morning"
end

def name
return "Mary"
end

def sayHi( aName )
return "Hi, #{aName}"
end

puts( greet ) #=> Hello
puts greet #=> Hello
puts greet() #=> good morning
puts( sayHi( name ) ) #=> Hi, Fred
puts( sayHi( name() ) ) #=> Hi, Mary



# 第九章。异常处理

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860138.png.jpg)

即使是最精心编写的程序有时也会遇到未预见到的错误。例如,如果你编写了一个需要从磁盘读取一些数据的程序,它基于这样的假设:指定的磁盘实际上是可用的,数据是有效的。如果你的程序基于用户输入进行计算,它基于这样的假设:输入适合用于计算。

尽管你可能在问题出现之前尝试预测一些潜在的问题——例如,通过编写代码来检查在从文件中读取数据之前文件是否存在,或者在执行计算之前检查用户输入是否为数值型——但你永远无法提前预测到每一个可能的问题。

例如,用户可能在已经开始从数据盘中读取数据后移除该数据盘;或者某些晦涩的计算可能在你的代码尝试除以这个值之前得到 0。当你知道代码在运行时可能会因为一些未预见的情况而“出错”,你可以通过使用*异常处理*来尝试避免灾难。

*异常*是一个封装成对象的错误。该对象是 Exception 类(或其子类)的一个实例。你可以通过捕获 Exception 对象来处理异常,可选地使用它包含的信息(例如打印适当的错误消息)并采取任何必要的措施来从错误中恢复——可能通过关闭任何仍然打开的文件或将变量赋值为一个有意义的值,该变量可能由于错误的计算而被赋予了一些无意义的值。

# rescue:当发生错误时执行代码

异常处理的语法可以总结如下:

begin

Some code which may cause an exception

rescue

Code to recover from the exception

end


当异常未被处理时,你的程序可能会崩溃,Ruby 可能会显示一个相对不友好的错误消息:

*div_by_zero.rb*

x = 1/0
puts( x )


程序以这个错误终止:

C:/bookofruby/ch9/div_by_zero.rb:3:in /': divided by 0 (ZeroDivisionError) from C:/bookofruby/ch9/div_by_zero.rb:3:in

'


为了防止这种情况发生,你应该自己处理异常。以下是一个处理尝试除以零的异常处理器的示例:

*exception1.rb*

begin
x = 1/0
rescue Exception
x = 0
puts( $!.class )
puts( $! )
end
puts( x )


当运行时,跟随`rescue Exception`的代码执行并显示以下内容:

ZeroDivisionError
divided by 0
0


在`begin`和`end`之间的代码是我的异常处理块。我将麻烦的代码放在了`begin`之后。当发生异常时,它会在以`rescue`开始的段落中处理。我首先做的事情是将变量`x`设置为一个有意义的值。接下来是这两个难以理解的语句:

puts( $!.class )
puts( $! )


在 Ruby 中,`$!`是一个全局变量,它被分配了最后一个异常。打印`$!.class`会显示类名,这里为 ZeroDivisionError;单独打印变量`$!`的效果是显示 Exception 对象包含的错误消息,这里为“除以 0。”

我通常不太喜欢依赖于全局变量,尤其是当它们有像`$!`这样不描述性的名字时。幸运的是,有一个替代方案。你可以在异常的类名之后和变量名之前放置“关联运算符”(`=>`)来将变量名与异常关联起来:

`exception2.rb`

rescue Exception => exc


现在,你可以使用变量名(这里为`exc`)来引用异常对象:

puts( exc.class )
puts( exc )


虽然当除以零时,你可能会得到 ZeroDivisionError 异常似乎很明显,但在实际代码中,有时异常的类型并不那么可预测。假设,例如,你有一个基于用户提供的两个值进行除法的方法:

def calc( val1, val2 )
return val1 / val2
end


这可能会产生各种不同的异常。显然,如果用户输入的第二个值是 0,你将得到一个 ZeroDivisionError。

异常有一个家族树

要理解`rescue`子句如何捕获异常,只需记住异常是对象,就像所有其他对象一样,它们由一个类定义。还有一个清晰的“继承链”,从基类开始:Object(在 Ruby 1.8 中)或 BasicObject(Ruby 1.9 中)。运行`exception_tree.rb`来显示异常的祖先。这是 Ruby 1.9 显示的内容:

ZeroDivisionError
StandardError
Exception
Object
BasicObject


`exception_tree.rb`

然而,如果第二个值是一个字符串,异常将是一个 TypeError,而如果第一个值是一个字符串,它将是一个 NoMethodError(因为 String 类没有定义“除法运算符”,即`/`)。在这里,`rescue`块处理所有可能的异常:

`multi_except.rb`

def calc( val1, val2 )
begin
result = val1 / val2
rescue Exception => e
puts( e.class )
puts( e )
result = nil
end
return result
end


你可以通过故意生成不同的错误条件来测试这一点:

calc( 20, 0 )
#=> ZeroDivisionError
#=> divided by 0
calc( 20, "100" )
#=> TypeError
#=> String can't be coerced into Fixnum
calc( "100", 100 )
#=> NoMethodError
#=> undefined method `/' for "100":String


通常,对于不同的异常采取不同的行动会有所帮助。你可以通过添加多个`rescue`子句来实现这一点。每个`rescue`子句可以处理多种异常类型,异常类名用逗号分隔。在这里,我的`calc`方法在一个子句中处理 TypeError 和 NoMethodError 异常,并使用一个通用的 Exception 处理程序来处理其他类型的异常:

`multi_except2.rb`

def calc( val1, val2 )
begin
result = val1 / val2
rescue TypeError, NoMethodError => e
puts( e.class )
puts( e )
puts( "One of the values is not a number!" )
result = nil
rescue Exception => e
puts( e.class )
puts( e )
result = nil
end
return result
end


这次,当处理 TypeError 或 NoMethodError(但没有其他类型的错误)时,我的附加错误信息将显示如下:

NoMethodError
undefined method `/' for "100":String
One of the values is not a number!


当处理多种异常类型时,你应该始终将处理特定异常的`rescue`子句放在前面,然后跟随处理更通用异常的`rescue`子句。

当处理特定的异常,例如 TypeError 时,`begin..end`异常块会退出,这样执行流程就不会“滴漏”到更通用的`rescue`子句中。然而,如果你首先放置一个通用的异常处理`rescue`子句,那么它将处理所有异常,所以任何更具体的子句都不会执行。

例如,如果我在`calc`方法中反转了`rescue`子句的顺序,将通用的异常处理程序放在前面,这将匹配所有异常类型,因此特定类型错误(TypeError)和 NoMethodError 异常的子句将永远不会被执行:

*multi_except_err.rb*

This is incorrect...

rescue Exception => e
puts( e.class )
result = nil
rescue TypeError, NoMethodError => e
puts( e.class )
puts( e )
puts( "Oops! This message will never be displayed!" )
result = nil
end
calc( 20, 0 ) #=> ZeroDivisionError
calc( 20, "100" ) #=> TypeError
calc( "100", 100 ) #=> NoMethodError


# ensure: 无论是否发生错误都执行代码

在某些情况下,你可能希望在发生异常与否的情况下都执行某些特定的操作。例如,无论何时你处理某种不可预测的输入/输出——比如说,当你在磁盘上的文件和目录工作时——总有可能位置(磁盘或目录)或数据源(文件)根本不存在,或者可能提供其他类型的问题——例如,当你尝试写入时磁盘已满,或者当你尝试读取时文件包含错误类型的数据。

无论你是否遇到任何问题,你可能都需要执行一些最终的“清理”程序,比如登录特定的工作目录或关闭之前打开的文件。你可以通过在`begin..rescue`代码块后面跟着另一个以`ensure`关键字开始的代码块来实现。`ensure`块中的代码将始终执行,无论之前是否发生了异常。

让我们看看两个简单的例子。在第一个例子中,我尝试登录磁盘并显示目录列表。在这一点上,我想要确保我的工作目录(由`Dir.getwd`给出)总是恢复到其原始位置。我通过在`startdir`变量中保存原始目录,并在`ensure`块中再次将其设置为工作目录来实现这一点:

*ensure.rb*

startdir = Dir.getwd

begin
Dir.chdir( "X:\" )
puts( dir )
rescue Exception => e
puts e.class
puts e
ensure
Dir.chdir( startdir )
end


当我运行这个程序时,显示以下内容:

We start out here: C:/Huw/programming/bookofruby/ch9
Errno::ENOENT
No such file or directory - X:
We end up here: C:/Huw/programming/bookofruby/ch9


现在我们来看看如何处理从文件中读取错误数据的问题。这可能发生在数据损坏、你意外打开了错误的文件,或者——非常简单地说——如果你的程序代码中存在错误。

这里有一个文件,*test.txt*,包含六行。前五行是数字;第六行是字符串,“six。”我的代码打开这个文件并读取所有六行:

*ensure2.rb*

f = File.new( "test.txt" )
begin
for i in (1..6) do
puts("line number: #{f.lineno}")
line = f.gets.chomp
num = line.to_i
puts( "Line '#{line}' is converted to #{num}" )
puts( 100 / num )
end
rescue Exception => e
puts( e.class )
puts( e )
ensure
f.close
puts( "File closed" )
end


行被读取为字符串(使用`gets`),代码尝试将它们转换为整数(使用`to_i`)。当转换失败时不会产生错误;相反,Ruby 返回值 0。问题出现在下一行代码中,它尝试除以转换后的数字。

在一开始打开数据文件后,我想确保无论是否发生错误,文件都会被关闭。例如,如果我只通过编辑 `for` 循环中的范围来读取前五行,那么就不会有异常。但我仍然希望关闭文件。但是将文件关闭代码(`f.close`)放在 `rescue` 子句中是没有用的,因为在这种情况下,它不会执行。然而,通过将其放在 `ensure` 子句中,我可以确保无论是否发生异常,文件都会被关闭。

# else: 当没有错误发生时执行代码

如果 `rescue` 部分在发生错误时执行,而 `ensure` 部分无论是否发生错误都会执行,那么如何具体执行仅在错误 *不* 发生时的一些代码?

要做到这一点,可以在 `rescue` 部分之后和 `ensure` 部分之前(如果有)添加一个可选的 `else` 子句,如下所示:

begin
# code which may cause an exception
rescue [Exception Type]
else # optional section executes if no exception occurs
ensure # optional exception always executes
end


这是一个例子:

*else.rb*

def doCalc( aNum )
begin
result = 100 / aNum.to_i
rescue Exception => e # executes when there is an error
result = 0
msg = "Error: " + e.to_s
else # executes when there is no error
msg = "Result = #{result}"
ensure # always executes
msg = "You entered '#{aNum}'. " + msg
end
return msg
end


尝试运行前面的程序并输入一个不会引起错误的数字,例如 10,这样 `msg` 将在 `else` 子句中赋值;然后尝试输入 0,这将引起错误,因此 `msg` 将在 `rescue` 子句中赋值。无论是否有错误,`ensure` 部分都将执行以创建一个以“您输入了”开头,后跟任何其他信息的 `msg` 字符串。例如:

You entered '5'. Result = 20
You entered '0'. Error: divided by 0


# 错误号

如果你之前运行了 *ensure.rb* 程序并且密切关注,当你尝试登录一个不存在的驱动器(例如,在我的系统中可能是 *X:\* 驱动器)时,你可能已经注意到了一些异常情况。通常,当发生异常时,异常类是一个特定命名的类型的实例,例如 ZeroDivisionError 或 NoMethodError。然而,在这种情况下,异常的类显示为 `Errno::ENOENT`。

结果表明,在 Ruby 中存在相当多的 `Errno` 错误。尝试 *disk_err.rb*。这个文件定义了一个方法,`chDisk`,它尝试通过字符 `aChar` 识别的磁盘登录。所以如果你将“A”作为参数传递给 `chDisk`,它将尝试登录到 *A:\* 驱动器。我已经调用了 `chDisk` 方法三次,每次传递不同的字符串:

*disk_err.rb*

def chDisk( aChar )
startdir = Dir.getwd
begin
Dir.chdir( "#{aChar}:\" )
puts( dir )
rescue Exception => e
#showFamily( e.class ) # to see ancestors, uncomment
puts e.class # ...and comment out this
puts e
ensure
Dir.chdir( startdir )
end
end

chDisk( "F" )
chDisk( "X" )
chDisk( "ABC" )


当然,你可能需要编辑你电脑上的路径以适应不同的环境。在我的电脑上,*F:\* 是我的 DVD 驱动器。目前它是空的,当我的程序尝试登录到它时,Ruby 返回这种类型的异常:`Errno::EACCES`。

我电脑上没有 *X:\* 驱动器,当我尝试登录到那个驱动器时,Ruby 返回这种类型的异常:`Errno::ENOENT`。

在前面的例子中,我传递了字符串参数“ABC”,它作为磁盘标识符是无效的,Ruby 返回这种类型的异常:`Errno::EINVAL`。

这种类型的错误是 SystemCallError 类的子类。你可以通过取消注释 *disk_err.rb* 源代码中指示的行来轻松验证这一点。这会调用相同的 `showFamily` 方法,你之前在 *exception_tree.rb* 程序中使用过。

这些错误类实际上封装了底层操作系统返回的整数错误值。常量的名称和值可能根据操作系统和 Ruby 的版本而有所不同。在这里,`Errno` 是包含常量的模块的名称,例如 `EACCES` 和 `ENOENT`,它们与整数错误值相匹配。

要查看 `Errno` 常量的完整列表,请运行以下命令:

*errno.rb*

puts( Errno.constants )


要查看任何给定常量的对应数值,请将 `::Errno` 追加到常量名称之后,例如:

Errno::EINVAL::Errno


你可以使用以下代码显示所有 `Errno` 常量的列表及其数值(在这里,`eval` 方法评估传递给它的表达式——你将在 第二十章 中了解它是如何工作的):

for err in Errno.constants do
errnum = eval( "Errno::#{err}::Errno" )
puts( "#{err}, #{errnum}" )
end


# retry: 在错误后再次尝试执行代码

如果你认为错误条件可能是瞬时的或者可能被纠正(可能是用户?),你可以在 `begin..end` 块中重新运行所有代码,使用关键字 `retry`,如下例所示,如果发生像 ZeroDivisionError 这样的错误,会提示用户重新输入值:

*retry.rb*

def doCalc
begin
print( "Enter a number: " )
aNum = gets().chomp()
result = 100 / aNum.to_i
rescue Exception => e
result = 0
puts( "Error: " + e.to_s + "\nPlease try again." )
retry # retry on exception
else
msg = "Result = #{result}"
ensure
msg = "You entered '#{aNum}'. " + msg
end
return msg
end


### 注意

当你想将异常对象(如 `e`)的消息附加到字符串(如 `"Error: "`)时,Ruby 1.9 强制你显式地将 `e` 转换为字符串(`"Error: " + e.to_s`),而 Ruby 1.8 会为你完成转换(`"Error: " + e`)。

当然,错误可能并不像你想象的那么短暂,所以如果你使用 `retry`,你可能想提供一个明确定义的退出条件,以确保代码在固定次数尝试后停止执行。

你可以在 `begin` 子句中增加一个局部变量。(如果你这样做,确保它在任何可能生成异常的代码之前增加,因为一旦发生异常,`rescue` 子句之前的代码将跳过!)然后在 `rescue` 部分测试该变量的值,如下所示:

rescue Exception => e
if aValue < someValue then
retry
end


这里有一个完整的示例,我测试了一个名为 `tries` 的变量的值,以确保在异常处理块退出之前,代码在没有错误的情况下不超过三次尝试运行:

*retry2.rb*

def doCalc
tries = 0
begin
print( "Enter a number: " )
tries += 1
aNum = gets().chomp()
result = 100 / aNum.to_i
rescue Exception => e
msg = "Error: " + e.to_s
puts( msg )
puts( "tries = #{tries}" )
result = 0
if tries < 3 then # set a fixed number of retries
retry
end
else
msg = "Result = #{result}"
ensure
msg = "You entered '#{aNum}'. " + msg
end
return msg
end


如果用户连续三次输入 0,这将产生以下输出:

Enter a number: 0
Error: divided by 0
tries = 1
Enter a number: 0
Error: divided by 0
tries = 2
Enter a number: 0
Error: divided by 0
tries = 3
You entered '0'. Error: divided by 0


# raise: 重新激活已处理错误

有时候你可能想在异常被捕获后仍然保持异常“活跃”,即使它已经被异常处理块捕获。例如,你可以通过将其传递给其他方法来延迟处理异常,比如传递给名为 `handleError` 的方法。你可以使用 `raise` 方法来做这件事。然而,你需要意识到,一旦抛出,异常需要被重新处理;否则,它可能会导致你的程序崩溃。以下是一个抛出 `ZeroDivisionError` 异常并将异常传递给名为 `handleError` 的方法的简单示例:

*raise.rb*

begin
divbyzero
rescue Exception => e
puts( "A problem just occurred. Please wait..." )
x = 0
begin
raise
rescue
handleError( e )
end
end


在这里,`divbyzero` 是一个方法的名字,其中包含了除以零的操作,而 `handleError` 是一个打印异常更详细信息的方法:

def handleError( e )
puts( "Error of type: #{e.class}" )
puts( e )
puts( "Here is a backtrace: " )
puts( e.backtrace )
end


注意,这使用了 `backtrace` 方法,它显示了一个字符串数组,显示了错误发生的文件名和行号,以及在这种情况下调用产生错误 `divbyzero` 方法的行。这是该程序输出的一个示例:

A problem just occurred. Please wait...
Error of type: ZeroDivisionError
divided by 0
Here is a backtrace:
C:/Huw/programming/bookofruby/ch9/raise.rb:11:in /' C:/Huw/programming/bookofruby/ch9/raise.rb:11:in divbyzero'
C:/Huw/programming/bookofruby/ch9/raise.rb:15:in `

'


你也可以专门抛出异常来强制错误条件,即使程序代码本身没有引发异常。单独调用 `raise` 会引发一个 `RuntimeError` 类型的异常(或者全局变量 `$!` 中存储的任何异常):

raise # raises RuntimeError


默认情况下,这不会与任何描述性消息相关联。你可以添加一个消息作为参数,如下所示:

raise "An unknown exception just occurred!"


你可以抛出一个特定的错误类型:

raise ZeroDivisionError


你还可以创建一个特定异常类型的对象,并用自定义消息初始化它:

raise ZeroDivisionError.new( "I'm afraid you divided by Zero" )


这是一个简单的例子:

*raise2.rb*

begin
raise ZeroDivisionError.new( "I'm afraid you divided by Zero" )
rescue Exception => e
puts( e.class )
puts( "message: " + e.to_s )
end


这将输出以下内容:

ZeroDivisionError
message: I'm afraid you divided by Zero


如果标准异常类型不符合你的需求,你当然可以通过继承现有异常来创建新的异常。为你的类提供一个 `to_str` 方法,以便为它们提供一个默认消息。

*raise3.rb*

class NoNameError < Exception
def to_str
"No Name given!"
end
end


这是一个关于如何抛出自定义异常的示例:

def sayHello( aName )
begin
if (aName == "") or (aName == nil) then
raise NoNameError
end
rescue Exception => e
puts( e.class )
puts( "error message: " + e.to_s )
puts( e.backtrace )
else
puts( "Hello #{aName}" )
end
end


如果你现在输入 `sayHello( nil )`,这将产生以下输出:

NoNameError
error message: NoNameError
C:/Huw/programming/bookofruby/ch9/raise3.rb:12:in sayHello' C:/Huw/programming/bookofruby/ch9/raise3.rb:23:in

'


深入挖掘

在捕获异常时,在某些情况下,可以省略 `begin` 关键字。在这里,你将了解这种语法。我还会澄清一些关于 `catch` 和 `throw` 的潜在混淆。

省略 begin 和 end

在方法、类或模块内部捕获异常时,你可以选择性地省略 `begin` 和 `end`。例如,以下所有这些都是合法的:

*omit_begin_end.rb*

def calc
result = 1/0
rescue Exception => e
puts( e.class )
puts( e )
result = nil
return result
end

class X
@@x = 1/0
rescue Exception => e
puts( e.class )
puts( e )
end

module Y
@@x = 1/0
rescue Exception => e
puts( e.class )
puts( e )
end


在所有之前的案例中,如果以通常的方式在异常处理代码的开始和结束处放置 `begin` 和 `end` 关键字,异常处理也将正常工作。

catch..throw

在某些语言中,异常是通过使用关键字`catch`来捕获的,并且可以使用关键字`throw`来引发。尽管 Ruby 提供了`catch`和`throw`方法,但这些方法与其异常处理没有直接关系。相反,`catch`和`throw`用于在满足某些条件时跳出定义的代码块。当然,您可以使用`catch`和`throw`在发生异常时跳出代码块(尽管这可能不是处理错误的最优雅方式)。例如,如果发生`ZeroDivisionError`,此代码将退出由大括号分隔的代码块:

*catch_except.rb*

catch( :finished) {
print( 'Enter a number: ' )
num = gets().chomp.to_i
begin
result = 100 / num
rescue Exception => e
throw :finished # jump to end of block
end
puts( "The result of that calculation is #{result}" )
} # end of :finished catch block


详见第六章了解`catch`和`throw`的更多内容。


# 第十章:代码块、Proc 和 Lambdas

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860138.png.jpg)

当程序员谈论代码块时,他们通常指的是一些任意的“代码块”。然而,在 Ruby 中,代码块是特殊的。它是一个类似于方法的代码单元,但与方法不同,它没有名字。

代码块在 Ruby 中非常重要,但它们可能难以理解。此外,Ruby 1.8 和 Ruby 1.9 中代码块的行为存在一些重要差异。如果你没有意识到这些差异,你的程序在运行不同版本的 Ruby 时可能会以意想不到的方式运行。本章详细探讨了代码块。它不仅解释了它们是如何工作的以及为什么它们是特殊的,而且还提供了确保它们在无论使用哪个版本的 Ruby 时都能保持一致性的指导。

# 什么是代码块?

考虑以下代码:

*1blocks.rb*

3.times do |i|
puts( i )
end


很明显,这段代码的目的是执行三次。可能不那么明显的是`i`在每次循环迭代中的值。实际上,在这种情况下`i`的值将是 0、1 和 2。以下是之前代码的另一种形式。这次,代码块是通过花括号而不是`do`和`end`界定的。

3.times { |i|
puts( i )
}


根据 Ruby 文档,`times`是 Integer(让我们称它为`int`)的一个方法,它会对一个代码块进行“`int`次迭代,传递从 0 到`int`−1 的值”。所以在这里,代码块内的代码运行了三次。第一次运行时,变量`i`被赋予值 0;每次后续运行,`i`都会增加 1,直到达到最终值 2(即`int`−1)。

之前显示的两个代码示例在功能上是相同的。代码块可以被花括号或`do`和`end`关键字包围,程序员可以根据个人喜好使用任一语法。

### 注意

一些 Ruby 程序员喜欢在代码块的全部代码都放在一行时使用花括号来界定代码块,而当代码块跨越多行时使用`do..end`。我个人的偏见是保持一致性,无论代码布局如何,所以我通常在界定代码块时使用花括号。通常,你选择的界定符对代码的行为没有影响——但请参阅优先级规则中的优先级规则。

如果你熟悉类似于 C#或 Java 的 C 语言,你可能会认为 Ruby 的花括号可以像那些语言一样使用,仅仅是为了将任意的“代码块”组合在一起——例如,当条件评估为真时要执行的代码块。但这并不是情况。在 Ruby 中,代码块是一个只能在非常特定情况下使用的特殊结构。

# 换行符很重要

你必须将代码块的开头界定符放在与其关联的方法所在的同一行上。

例如,以下是可以的:

3.times do |i|
puts( i )
end

3.times { |i|
puts( i )
}


但这些包含语法错误:

3.times
do |i|
puts( i )
end

3.times
{ |i|
puts( i )
}


# 无命名函数

Ruby 代码块可以被视为一种无命名函数或方法,它最频繁的使用是提供一种遍历列表或值范围中项的手段。如果你从未遇到过无命名函数,这可能会听起来像是胡言乱语。幸运的是,到本章结束时,事情可能会变得稍微清晰一些。让我们回顾一下前面给出的简单例子。我说代码块就像一个无命名函数。以下是一个例子:

{ |i|
puts( i )
}


如果将其写成正常的 Ruby 方法,它看起来可能像这样:

def aMethod( i )
puts( i )
end


要调用该方法三次并传递从 0 到 2 的值,你可以这样写:

for i in 0..2
aMethod( i )
end


当你创建一个无命名方法(即代码块)时,在竖线之间声明的变量,如`|i|`,可以像命名方法的参数一样处理。我将把这些变量称为*代码块参数*。

再看看我之前的例子:

3.times { |i|
puts( i )
}


整数的`times`方法将值从 0 传递到指定的整数值减 1。

所以,这:

3.times{ |i| }


非常像这样:

for i in 0..2
aMethod( i )
end


主要区别在于第二个例子必须调用一个命名方法来处理`i`的值,而第一个例子使用无命名方法(花括号之间的代码)来处理`i`。

# 看起来熟悉吗?

现在你已经知道了代码块是什么,你可能注意到你以前见过它们。很多次。例如,你之前使用`do..end`代码块来遍历像这样的一系列值:

(1..3).each do |i|
puts(i)
end


你还使用`do..end`代码块来遍历数组(参见 for Loops 中的*for_each2.rb*):

arr = ['one','two','three','four']
arr.each do |s|
puts(s)
end


你通过将代码块传递给`loop`方法(参见 loop 中的*3loops.rb*)来重复执行一个代码块:

i=0
loop {
puts(arr[i])
i+=1
if (i == arr.length) then
break
end
}


之前的`loop`例子有两个显著特点:它没有要遍历的项目列表(如数组或值范围),而且相当丑陋。这两个特点并非完全无关!`loop`方法属于 Kernel 类,它“自动”对程序可用。因为它没有“结束值”,所以它会无限期地执行代码块,除非你使用`break`关键字显式地从中退出。通常有更优雅的方式来执行这种迭代——通过遍历有限范围的值序列。

# 代码块和数组

代码块通常用于遍历数组。因此,Array 类提供了一些方法,可以将代码块传递给这些方法。

一个有用的方法是`collect`;它将数组的每个元素传递给一个代码块,并创建一个新的数组来包含代码块返回的每个值。例如,这里,一个代码块被传递给数组中的每个整数(每个整数被分配给变量`x`);代码块将其值加倍并返回。`collect`方法创建一个新的数组,按顺序包含返回的每个整数:

*2blocks.rb*

b3 = [1,2,3].collect{|x| x*2}


之前的例子将这个数组赋值给`b3`:

[2,4,6]


在下一个示例中,块返回一个版本的原字符串,其中每个首字母都被大写:

b4 = ["hello","good day","how do you do"].collect{|x| x.capitalize }


因此,`b4` 现在如下所示:

["Hello", "Good day", "How do you do"]


数组类的 `each` 方法可能看起来与 `collect` 方法相当相似;它同样逐个将数组元素传递给块进行处理。然而,与 `collect` 方法不同,`each` 方法不会创建一个新的数组来包含返回的值:

b5 = ["hello","good day","how do you do"].each{|x| x.capitalize }


这次,`b5` 没有改变:

["hello", "good day", "how do you do"]


然而,请记住,某些方法——特别是以感叹号 (`!`) 结尾的方法——实际上会改变原始对象而不是产生新的值。如果你想使用 `each` 方法来大写字符串中的原始数组,你可以使用 `capitalize!` 方法:

b6 = ["hello","good day", "how do you do"].each{|x| x.capitalize! }


因此,`b6` 现在如下所示:

["Hello", "Good day", "How do you do"]


经过一些思考,你也可以使用块来遍历字符串中的字符。首先,你需要从字符串中分割出每个字符。这可以通过使用字符串类的 `split` 方法来完成,如下所示:

"hello world".split(//)


`split` 方法根据分隔符将字符串分割成子字符串,并返回这些子字符串的数组。在这里,`//` 是一个正则表达式,它定义了一个零长度的字符串;这会产生返回单个字符的效果,因此你最终会创建一个包含字符串中所有字符的数组。你现在可以遍历这个字符数组,返回每个字符的大写版本:

a = "hello world".split(//).each{ |x| newstr << x.capitalize }


在每次迭代中,一个首字母大写的字符被添加到 `newstr` 中,然后显示以下内容:

H
HE
HEL
HELL
HELLO
HELLO
HELLO W
HELLO WO
HELLO WOR
HELLO WORL
HELLO WORLD


由于你在这里使用了 `capitalize` 方法(没有终止的 `!` 字符),数组 `a` 中的字符保持不变,全部为小写,因为 `capitalize` 方法不会改变接收对象(在这里,接收对象是传递到块中的字符)。

然而,请注意,如果你使用 `capitalize!` 方法来修改原始字符,这段代码将无法工作。这是因为当没有变化时,`capitalize!` 返回 `nil`,所以当遇到空格字符时,将返回 `nil`,并且尝试将 `nil` 值追加到字符串 `newstr` 中将失败。

你还可以使用 `each_byte` 方法来大写字符串。这个方法遍历字符串中的字符,将每个字节传递给块。这些字节以 ASCII 码的形式出现。所以,“hello world”将以这些数值的形式传递:`104 101 108 108 111 32 119 111 114 108 100`。

显然,你不能对整数进行大写,因此你需要将每个 ASCII 值转换为字符。字符串的 `chr` 方法可以做到这一点:

a = "hello world".each_byte{|x| newstr << (x.chr).capitalize }


# 进程和 Lambda

到目前为止的示例中,块与方法一起使用。这自从无名的块在 Ruby 中不能独立存在以来一直是必需的。例如,你不能创建一个独立的块,如下所示:

{|x| x = x*10; puts(x)} # This is not allowed!


这是“Ruby 中的一切都是对象”这一规则的一个例外。块显然不是对象。每个对象都是由一个类创建的,你可以通过调用其 `class` 方法来找到对象所属的类。

例如,用哈希来做这件事,将显示类名“Hash”:

puts({1=>2}.class)


然而,尝试使用块,你将只会得到一个错误信息:

puts({|i| puts(i)}.class) #<= error!


# 块或哈希?

Ruby 使用花括号来界定块和哈希。那么,你(以及 Ruby)如何区分它们呢?答案基本上是,当它看起来像哈希时,它就是一个哈希,否则它就是一个块。哈希看起来像哈希,当花括号中包含键值对时:

puts( {1=>2}.class ) #<= Hash


或者当它们为空时:

*block_or_hash.rb*

puts( {}.class ) #<= Hash


然而,如果你省略了括号,这里就有歧义。这是一个空哈希,还是一个与 `puts` 方法关联的块?

puts{}.class


坦白说,我必须承认我不知道这个问题的答案,我也无法让 Ruby 告诉我。Ruby 接受这作为有效的语法,但实际上在代码执行时并不会显示任何内容。那么,这个怎么样?

print{}.class


再次强调,在 Ruby 1.9 中,这什么也不打印,但在 Ruby 1.8 中,它会显示 `nil`(请注意,这不是 `nil` 的实际 *类*,`nil` 的实际类是 NilClass,而是 `nil` 本身)。如果你觉得这一切都很混乱(就像我一样!),只需记住,这可以通过恰当地使用括号来澄清:

print( {}.class ) #<= Hash


# 从块创建对象

虽然块默认可能不是对象,但它们可以被“转换”为对象。有三种从块创建对象并将其赋值给变量的方法——下面是如何做的:

*proc_create.rb*

a = Proc.new{|x| x = x10; puts(x) } #=> Proc
b = lambda{|x| x = x
10; puts(x) } #=> Proc
c = proc{|x| x.capitalize! } #=> Proc


在这三种情况下,你最终都会创建 Proc 类的一个实例——这是 Ruby 对块的“对象包装器”。

让我们看看创建和使用 Proc 对象的一个简单例子。首先,你可以通过调用 `Proc.new` 并传递一个块作为参数来创建一个对象:

*3blocks.rb*

a = Proc.new{|x| x = x*10; puts(x)}


其次,你可以使用 Proc 类的 `call` 方法执行 `a` 所指的块中的代码,通过传递一个或多个参数(匹配块参数)到块中;在前面的代码中,你可以传递一个整数,例如 100,这将分配给块变量 `x`:

a.call(100) #=> 1000


最后,你还可以通过调用 Kernel 类提供的 `lambda` 或 `proc` 方法来创建 Proc 对象。名称 `lambda` 来自于 Scheme(Lisp)语言,是描述匿名方法或 *闭包* 的术语。

b = lambda{|x| x = x*10; puts(x) }
b.call(100) #=> 1000

c = proc{|x| x.capitalize! }
c1 = c.call( "hello" )
puts( c1 ) #=> Hello


这里有一个稍微复杂一点的例子,它遍历一个字符串数组,逐个将每个字符串转换为大写。然后,将转换为大写的字符串数组赋值给 `d1` 变量:

d = lambda{|x| x.capitalize! }
d1 = ["hello","good day","how do you do"].each{ |s| d.call(s)}
puts(d1.inspect) #=> ["Hello", "Good day", "How do you do"]


使用 `Proc.new` 创建 Proc 对象和使用 `lambda` 方法创建 Proc 对象之间存在一个重要的区别——`Proc.new` 不会检查传递给块的参数数量是否与块参数的数量匹配。`lambda` 会检查。在 Ruby 1.8 和 1.9 中,`proc` 方法的行为不同。在 Ruby 1.8 中,`proc` 等同于 `lambda`——它会检查参数数量。在 Ruby 1.9 中,`proc` 等同于 `Proc.new`——它不会检查参数数量:

*proc_lamba.rb*

a = Proc.new{|x,y,z| x = y*z; puts(x) }
a.call(2,5,10,100) # This is not an error

b = lambda{|x,y,z| x = y*z; puts(x) }
b.call(2,5,10,100) # This is an error

puts('---Block #2---' )
c = proc{|x,y,z| x = y*z; puts(x) }
c.call(2,5,10,100) # This is an error in Ruby 1.8
# Not an error in Ruby 1.9


# 什么是闭包?

一个 *闭包* 是一个能够存储(即,“封装”)在创建块的作用域内局部变量值的函数(想想看,这就是块的原生作用域)。Ruby 的块是闭包。为了理解这一点,请看这个例子:

*block_closure.rb*

x = "hello world"

ablock = Proc.new { puts( x ) }

def aMethod( aBlockArg )
x = "goodbye"
aBlockArg.call
end

puts( x )
ablock.call
aMethod( ablock )
ablock.call
puts( x )


这里,局部变量 `x` 在 `ablock` 的作用域内的值是 “hello world”。然而,在 `aMethod` 中,一个名为 `x` 的局部变量具有值 “goodbye”。尽管如此,当 `ablock` 被传递给 `aMethod` 并在 `aMethod` 的作用域内调用时,它打印出 “hello world”(即块原生作用域内的 `x` 的值),而不是 “goodbye”,这是 `aMethod` 作用域内 `x` 的值。因此,之前的代码始终只打印出 “hello world”。

### 注意

有关闭包的更多信息,请参阅 深入挖掘。

# yield

让我们看看更多块的使用。*4blocks.rb* 程序引入了新的内容,即,当块被传递给方法时执行一个无名称块的方法。这是通过使用关键字 `yield` 来实现的。在第一个例子中,我定义了这个简单的方法:

*4blocks.rb*

def aMethod
yield
end


它实际上并没有自己的代码。相反,它期望接收一个块,`yield` 关键字导致块执行。这就是我向它传递块的方式:

aMethod{ puts( "Good morning" ) }


注意这次块不是作为命名参数传递的。尝试将块放在括号之间传递,像这样,将会是一个错误:

aMethod( { puts( "Good morning" ) } ) # This won't work!


相反,你只需将块放在你传递给它的方法旁边,就像你在本章的第一个例子中所做的那样。该方法接收块,而无需为它声明命名参数,并且使用 `yield` 调用块。

这里是一个稍微有用一点的例子:

def caps( anarg )
yield( anarg )
end

caps( "a lowercase string" ){ |x| x.capitalize! ; puts( x ) }


这里,`caps` 方法接收一个参数 `anarg`,并将此参数传递给一个无名称块,然后通过 `yield` 执行该块。当我调用 `caps` 方法时,我使用正常的参数传递语法传递一个字符串参数(`"a lowercase string"`)。无名称块在参数列表的 *之后* 传递。

当 `caps` 方法调用 `yield( anarg )` 时,字符串参数被传递到块中;它被分配给块变量 `x`。然后将其转换为大写,并通过 `puts( s )` 显示出来,这表明首字母已被大写:“一个小写字符串。”

# 块中的块

你已经看到了如何使用块来遍历数组。在下一个示例(也在 *4blocks.rb* 中),我使用一个块来遍历一个字符串数组,依次将每个字符串赋值给块变量 `s`。然后,另一个块被传递给 `caps` 方法以将字符串转换为大写:

["hello","good day","how do you do"].each{
|s|
caps( s ){ |x| x.capitalize!
puts( x )
}
}


这会产生以下输出:

Hello
Good day
How do you do


# 传递命名 Proc 参数

到目前为止,你已经以匿名(在这种情况下,块将使用 `yield` 关键字执行)或命名参数(在这种情况下,它将使用 `call` 方法执行)的形式将块传递给过程。还有另一种传递块的方法。当一个方法参数列表中的最后一个参数前面有一个 ampersand (`&`) 时,它被视为一个 Proc 对象。这让你可以选择使用与传递给迭代器的块相同的语法将匿名块传递给过程,同时过程本身可以将块作为命名参数接收。加载 *5blocks.rb* 来查看一些示例。

首先,这是一个提醒,你已经看到了两种传递块的方法。这种方法有三个参数,`a`、`b` 和 `c`:

*5blocks.rb*

def abc( a, b, c )
a.call
b.call
c.call
yield
end


你可以用三个命名参数(在这里恰好是块,但原则上可以是任何东西)加上一个未命名的块来调用此方法:

a = lambda{ puts "one" }
b = lambda{ puts "two" }
c = proc{ puts "three" }
abc(a, b, c ){ puts "four" }


`abc` 方法使用 `call` 方法执行命名块参数,并使用 `yield` 关键字执行未命名的块。结果在这里的 `#=>` 注释中显示:

a.call #=> one
b.call #=> two
c.call #=> three
yield #=> four


下一个方法 `abc2` 只接受一个参数,`&d`。这里的 ampersand 很重要,因为它表明 `&d` 参数是一个块。`abc2` 方法能够使用参数的名称(不带 ampersand)来执行块,而不是使用 `yield` 关键字:

def abc2( &d )
d.call
end


因此,带 ampersand 的块参数的调用方式与不带 ampersand 的块参数相同。然而,在将匹配该参数的对象传递给方法的方式上存在差异。为了匹配 ampersand 参数,通过将其附加到方法名称来传递一个未命名的块:

abc2{ puts "four" }


你可以将 ampersand 参数视为类型检查的块参数。与没有 ampersand 的正常参数不同,该参数不能匹配任何类型;它只能匹配一个块。你不能向 `abc2` 传递其他类型的对象:

abc2( 10 ) # This won't work!


`abc3` 方法本质上与 `abc` 方法相同,只是它指定了一个第四个形式块类型参数(`&d`):

def abc3( a, b, c, &d)


参数 `a`、`b` 和 `c` 被调用,而参数 `&d` 可以根据你的喜好调用或传递:

def abc3( a, b, c, &d)
a.call
b.call
c.call
d.call # first call block &d
yield # then yield block &d
end


这意味着调用代码必须向此方法传递三个形式参数加上一个块,该块可能没有名字:

abc3(a, b, c){ puts "five" }


上一个方法调用会产生以下输出(请注意,最后一个块参数执行了两次,因为它既被调用又被传递):

one
two
three
five
five


你也可以使用前面的 ampersand 来将一个命名块传递给一个没有匹配命名参数的方法,如下所示:

myproc = proc{ puts("my proc") }
abc3(a, b, c, &myproc )


在前面的代码中,即使方法没有在其参数列表中声明匹配的变量,也可以将类似于 `&myproc` 的块变量传递给方法。这给了你选择传递未命名的块或 Proc 对象的机会:

xyz{ |a,b,c| puts(a+b+c) }
xyz( &myproc )


然而,请注意!在先前的例子中,我使用了与之前分配给 Proc 对象的三个局部变量同名(`a`、`b`、`c`)的块参数:`|a,b,c|`:

a = lambda{ puts "one" }
b = lambda{ puts "two" }
c = proc{ puts "three" }
xyz{ |a,b,c| puts(a+b+c) }


原则上,块参数只应在块内部可见。然而,实际上在 Ruby 1.8 和 Ruby 1.9 中对块参数的赋值有截然不同的影响。让我们首先看看 Ruby 1.8。在这里,对块参数的赋值可以初始化块本生作用域(即程序的主作用域)中具有相同名称的任何局部变量的值(参见 什么是闭包? 在 什么是闭包? 中)。

即使 `xyz` 方法中的变量命名为 `x`、`y` 和 `z`,但实际上在该方法中的整数赋值是针对变量 `a`、`b` 和 `c` 进行的,当这个块:

{ |a,b,c| puts(a+b+c) }


被传递了 `x`、`y` 和 `z` 的值:

def xyz
x = 1
y = 2
z = 3
yield( x, y, z ) # 1,2,3 assigned to block parameters a,b,c
end


因此,块本生作用域内的 Proc 变量 `a`、`b` 和 `c` 会在块中的代码运行后初始化为块变量 `x`、`y` 和 `z` 的整数值。因此,`a`、`b` 和 `c`,最初是 Proc 对象,最终成为整数。

相反,在 Ruby 1.9 中,块内部的变量与块外声明的变量隔离开来。因此,`xyz` 方法中的 `x`、`y` 和 `z` 变量的值不会被分配给块的 `a`、`b` 和 `c` 参数。这意味着一旦块执行完毕,块外声明的 `a`、`b` 和 `c` 变量的值不受影响:它们最初是 Proc 对象,最终仍然是 Proc 对象。

现在假设你执行以下代码,记住 `a`、`b` 和 `c` 在开始时是 Proc 对象:

xyz{ |a,b,c| puts(a+b+c) }
puts( a, b, c )


在 Ruby 1.8 中,前面展示的 `puts` 语句显示了 `a`、`b` 和 `c` 的最终值,表明它们已经被初始化为在 `xyz` 方法中传递到块中的整数值(当它被 `yield( x, y, z )` 调用时)。因此,它们现在是整数:

1
2
3


但在 Ruby 1.9 中,`a`、`b` 和 `c` 并不是由块参数初始化的,它们保持最初的状态,作为 Proc 对象:

<Proc:0x2b65828@C:/bookofruby/ch10/5blocks.rb:36 (lambda)>

<Proc:0x2b65810@C:/bookofruby/ch10/5blocks.rb:37 (lambda)>

Proc:0x2b657f8@C:/bookofruby/ch10/5blocks.rb:38


这种行为可能难以理解,但花时间理解它是值得的。在 Ruby 中,块的使用很常见,了解块的执行可能(或可能不)如何影响块外声明的变量值是很重要的。为了澄清这一点,尝试在 *6blocks.rb* 中的简单程序:

*6blocks.rb*

a = "hello world"

def foo
yield 100
end

puts( a )
foo{ |a| puts( a ) }

puts( a )


在这里,`a` 是主程序范围内的一个字符串。在块中声明了另一个具有相同名称的变量 `a`,该块被传递给 `foo` 并产生。当它被产生时,一个整数值 100 被传递到块中,导致块的参数 `a` 被初始化为 100。问题是,块参数 `a` 的初始化是否也会初始化主作用域中的字符串变量 `a`?答案是,在 Ruby 1.8 中是 *是的*,但在 Ruby 1.9 中是 *不是*。

Ruby 1.8 显示如下:

hello world
100
100


Ruby 1.9 显示如下:

hello world
100
hello world


如果你想要确保无论使用哪个版本的 Ruby,块参数都不会改变块外声明的变量的值,只需确保块参数的名称不与别处使用的名称重复。在当前程序中,你可以通过简单地更改块参数的名称来确保它是块独有的:

foo{ |b| puts( b ) } # the name 'b' is not used elsewhere


这次,当程序运行时,Ruby 1.8 和 Ruby 1.9 都产生了相同的结果:

hello world
100
hello world


这是在 Ruby 中很容易陷入的一个陷阱的例子。一般来说,当变量共享相同的范围(例如,在这里主程序范围内的一个块)时,最好使它们的名称唯一,以避免任何未预见的副作用。有关作用域的更多信息,请参阅 深入挖掘。

# 优先级规则

花括号内的块比 `do` 和 `end` 内的块有更强的优先级。让我们看看这在实践中意味着什么。考虑以下两个例子:

foo bar do |s| puts( s ) end
foo bar{ |s| puts(s) }


在这里,`foo` 和 `bar` 都是方法,花括号和 `do` 与 `end` 之间的代码是块。那么,这些块中的每一个会被传递给哪个方法?结果是,`do..end` 块会被传递给最左边的 `foo` 方法,而花括号中的块会被传递给最右边的 `bar` 方法。这是因为据说花括号比 `do` 和 `end` 有更高的优先级。

考虑这个程序:

*precedence.rb*

def foo( b )
puts("---in foo---")
a = 'foo'
if block_given?
puts( "(Block passed to foo)" )
yield( a )
else
puts( "(no block passed to foo)" )
end
puts( "in foo, arg b = #{b}" )
return "returned by " << a
end

def bar
puts("---in bar---")
a = 'bar'
if block_given?
puts( "(Block passed to bar)" )
yield( a )
else
puts( "(no block passed to bar)" )
end
return "returned by " << a
end

foo bar do |s| puts( s ) end # 1) do..end block
foo bar{ |s| puts(s) } # 2) {..} block


在这里,`do..end` 块的优先级较低,`foo` 方法被赋予优先权。这意味着 `bar` 和 `do..end` 块都被传递给 `foo`。因此,这两个表达式是等价的:

foo bar do |s| puts( s ) end
foo( bar ) do |s| puts( s ) end


另一方面,花括号块有更强的优先级,因此它会立即尝试执行,并传递给第一个可能的方法接收器(`bar`)。结果(即 `bar` 返回的值)然后作为参数传递给 `foo`,但这次 `foo` 并没有接收到块本身。因此,以下两个表达式是等价的:

foo bar{ |s| puts(s) }
foo( bar{ |s| puts(s) } )


如果你对此感到困惑,请放心,你不是唯一一个!这些潜在的歧义源于 Ruby 中参数列表周围的括号是可选的。正如你从我在前面给出的替代版本中看到的那样,当你使用括号时,歧义就会消失。

### 注意

一个方法可以使用 `block_given?` 方法来测试它是否接收到了一个块。你可以在 *precedence.rb* 程序中找到这个的例子。

# 块作为迭代器

如前所述,Ruby 中块的主要用途之一是提供迭代器,可以将范围或项目列表传递给这些迭代器。许多标准类,如 Integer 和 Array,都有可以提供块可以迭代的项的方法。例如:

3.times{ |i| puts( i ) }
[1,2,3].each{|i| puts(i) }


你当然可以创建自己的迭代器方法来向块提供一系列值。在 *iterate1.rb* 程序中,我定义了一个简单的 `timesRepeat` 方法,它执行一个指定次数的块。这与 Integer 类的 `times` 方法类似,但它从索引 1 开始而不是从索引 0 开始(这里变量 `i` 被显示出来以演示这一点):

*iterate1.rb*

def timesRepeat( aNum )
for i in 1..aNum do
yield i
end
end


下面是如何调用这个方法的例子:

timesRepeat( 3 ){ |i| puts("[#{i}] hello world") }


这会显示以下内容:

[1] hello world
[2] hello world
[3] hello world


我还创建了一个 `timesRepeat2` 方法来迭代数组:

def timesRepeat2( aNum, anArray )
anArray.each{ |anitem|
yield( anitem )
}
end


这可以通过以下方式调用:

timesRepeat2( 3, ["hello","good day","how do you do"] ){ |x| puts(x) }


这会显示以下内容:

hello
good day
how do you do


当然,如果对象本身包含自己的迭代器方法会更好(更符合面向对象的精神)。我在下一个例子中实现了这个功能。在这里,我创建了 MyArray,它是 Array 的一个子类:

class MyArray < Array


当创建一个新的 MyArray 对象时,它使用数组进行初始化:

def initialize( anArray )
super( anArray )
end


它依赖于自己的 `each` 方法(一个对象将自己称为 `self`),这是由其祖先 Array 提供的,用于遍历数组中的项,并且它使用 Integer 的 `times` 方法来完成这个操作一定次数。这是完整的类定义:

*iterate2.rb*

class MyArray < Array
def initialize( anArray )
super( anArray )
end

def timesRepeat( aNum )
    aNum.times{           # start block 1...
         | num |
         self.each{       # start block 2...
              | anitem |
              yield( "[#{num}] :: '#{anitem}'" )
         }                # ...end block 2
    }                     # ...end block 1
end

end


注意,因为我使用了两个迭代器(`aNum.times` 和 `self.each`),所以 `timesRepeat` 方法包含两个嵌套的块。这是一个你可以如何使用它的例子:

numarr = MyArray.new( [1,2,3] )
numarr.timesRepeat( 2 ){ |x| puts(x) }


这将输出以下内容:

[0] :: '1'
[0] :: '2'
[0] :: '3'
[1] :: '1'
[1] :: '2'
[1] :: '3'


在 *iterate3.rb* 中,我给自己设定了定义一个包含任意数量子数组的迭代器的任务,其中每个子数组具有相同数量的项。换句话说,它将像一个具有固定行数和固定列数的表格或矩阵。例如,这是一个具有三个“行”(子数组)和四个“列”(项)的多维数组:

*iterate3.rb*

multiarr =
[ ['one','two','three','four'],
[1, 2, 3, 4 ],
[:a, :b, :c, :d ]
]


我尝试了三种不同的版本。第一个版本受到限制,只能与预定义的数字(这里索引 [0] 和 [1] 上的 2)的“行”一起工作,因此它不会显示第三行的符号:

multiarr[0].length.times{|i|
puts(multiarr[0][i], multiarr[1][i])
}


第二个版本通过遍历 `multiarr` 的每个元素(或“行”),然后通过获取行长度并使用 Integer 的 `times` 方法来迭代该行中的每个项来解决这个问题。因此,它显示了所有三行的数据:

multiarr.each{ |arr|
multiarr[0].length.times{|i|
puts(arr[i])
}
}


第三个版本反转了这些操作:外层块沿着行 0 的长度迭代,内层块获取每行的索引 `i` 处的项。再次显示所有三行的数据:

multiarr[0].length.times{|i|
multiarr.each{ |arr|
puts(arr[i])
}
}


然而,尽管版本 2 和 3 以类似的方式工作,你会发现它们遍历项目的方式不同。版本 2 逐行遍历每一行。版本 3 遍历每一列的项目。运行程序以验证这一点。你可以尝试创建自己的 Array 子类,并添加像这样的迭代器方法——一个方法按顺序遍历行,另一个方法遍历列。

深入挖掘

在这里,我们来看看 Ruby 1.8 和 1.9 中块作用域的重要差异,并了解从方法中返回块。

从方法中返回块

之前,我解释了 Ruby 中的块可以作为闭包。闭包可以说封装了它声明的“环境”。或者,换句话说,它携带了局部变量的值从其原始作用域到不同的作用域。我之前给出的例子展示了名为 `ablock` 的块是如何捕获局部变量 `x` 的值的:

*block_closure.rb*

x = "hello world"
ablock = Proc.new { puts( x ) }


然后,它能够“携带”这个变量到不同的作用域。例如,`ablock` 被传递给 `aMethod`。当 `ablock` 在那个方法中被调用时,它运行 `puts( x )` 这段代码。这显示了“hello world”,而不是“goodbye”:

def aMethod( aBlockArg )
x = "goodbye"
aBlockArg.call #=> "hello world"
end


在这个特定的例子中,这种行为可能看起来像是一个不引人注目的好奇。实际上,块/闭包可以被更有创意地使用。

例如,你不必创建一个块并将其发送到方法,你可以在方法内部创建一个块,并将其返回给调用代码。如果创建块的方 法恰好接受一个参数,那么块可以用那个参数初始化。

这为你提供了一个简单的方法来从相同的“块模板”创建多个块,每个实例都使用不同的数据初始化。例如,我创建了两个块,并将它们分配给变量 `salesTax` 和 `vat`,每个都基于不同的值(0.10)和(0.175)计算结果:

*block_closure2.rb*

def calcTax( taxRate )
return lambda{
|subtotal|
subtotal * taxRate
}
end

salesTax = calcTax( 0.10 )
vat = calcTax( 0.175 )

print( "Tax due on book = ")
print( salesTax.call( 10 ) ) #=> 1.0

print( "\nVat due on DVD = ")
print( vat.call( 10 ) ) #=> 1.75


块和实例变量

块的一个不太明显的特点是它们使用变量的方式。如果块确实可以被视为一个无名的函数或方法,那么从逻辑上讲,它应该能够包含自己的局部变量,并且能够访问块所属对象的实例变量。

让我们先看看实例变量。加载 *closures1.rb* 程序。这个程序提供了另一个块作为闭包的例子——通过捕获它创建的作用域中的局部变量的值。在这里,我使用 `lambda` 方法创建了一个块:

*closures1.rb*

aClos = lambda{
@hello << " yikes!"
}


这个块将字符串 “yikes!” 追加到实例变量 `@hello` 上。注意,在这个阶段,`@hello` 还没有被分配任何值。然而,我创建了一个单独的方法 `aFunc`,它确实为名为 `@hello` 的变量分配了一个值:

def aFunc( aClosure )
@hello = "hello world"
aClosure.call
end


当我将我的块(`aClosure`参数)传递给`aFunc`方法时,该方法使`@hello`成为现实。我现在可以使用`call`方法执行块内的代码。果然,`@hello`变量包含了“hello world”字符串。同样,也可以在方法外部调用块来使用这个变量。实际上,现在,通过反复调用块,我会在`@hello`上反复追加字符串“yikes!”:

aFunc(aClos) #<= @hello = "hello world yikes!"
aClos.call #<= @hello = "hello world yikes! yikes!"
aClos.call #<= @hello = "hello world yikes! yikes! yikes!"
aClos.call # ...and so on


如果你仔细想想,这并不令人惊讶。毕竟,`@hello`是一个实例变量,所以它存在于对象的作用域内。当你运行 Ruby 程序时,会自动创建一个名为`main`的对象。因此,你应该期望在该对象(程序)内部创建的任何实例变量都可以在它内部使用。

现在出现了一个问题:如果你将块发送到另一个对象的某个方法会发生什么?如果那个对象有自己的实例变量`@hello`,那么块将使用哪个变量——是创建块的作用域中的`@hello`还是调用块的对象作用域中的`@hello`?让我们试试。你将使用与之前相同的块,但这次它会显示有关块所属对象的一些信息以及`@hello`的值:

aClos = lambda{
@hello << " yikes!"
puts("in #{self} object of class #{self.class}, @hello = #{@hello}")
}


现在,从一个新的类(X)创建一个新的对象,并给它一个接收块`b`并调用块的方法:

class X
def y( b )
@hello = "I say, I say, I say!!!"
puts( " [In X.y]" )
puts("in #{self} object of class #{self.class}, @hello = #{@hello}")
puts( " [In X.y] when block is called..." )
b.call
end
end

x = X.new


为了测试它,只需将块`aClos`传递给`x`的`y`方法:

x.y( aClos )


这就是显示的内容:

[In X.y]
in #<X:0x32a6e64> object of class X, @hello = I say, I say, I say!!!
[In X.y] when block is called...
in main object of class Object, @hello = hello world yikes! yikes! yikes!
yikes! yikes! yikes!


因此,很明显,块是在创建它的对象的作用域内执行的(即`main`),即使调用块的对象的作用域中有一个同名的实例变量和不同的值,它也保留了该对象的实例变量。

块和局部变量

现在让我们看看块/闭包如何处理局部变量。在*closures2.rb*程序中,我声明了一个变量`x`,它是程序上下文中的局部变量:

*closures2.rb*

x = 3000


第一个块/闭包被命名为`c1`。每次调用这个块时,它会获取块外部定义的`x`的值(3,000)并返回`x + 100`:

c1 = proc{
x + 100
}


顺便说一下,尽管这返回了一个值(在常规 Ruby 方法中,默认值是最后一个要评估的表达式的结果),但在 Ruby 1.9 中,你不能像这样显式地使用`return`语句:

return x + 1


如果你这样做,Ruby 1.9 会抛出一个 LocalJumpError 异常。另一方面,Ruby 1.8 不会抛出异常。

这个块没有块参数(也就是说,在竖线之间没有“块局部”变量),所以当它用一个变量`someval`调用时,该变量被丢弃,未使用。换句话说,`c1.call(someval)`的效果与`c1.call()`相同。

因此,当你调用块`c1`时,它返回`x+100`(即 3,100);这个值随后被分配给`someval`。当你第二次调用`c1`时,同样的事情再次发生,所以`someval`再次被分配 3,100:

someval=1000
someval=c1.call(someval); puts(someval) #<= someval is now 3100
someval=c1.call(someval); puts(someval) #<= someval is now 3100


### 注意

而不是像前面展示的那样重复调用`c1`,你可以将调用放在一个块中,并将其传递给 Integer 的`times`方法,如下所示:

2.times{ someval=c1.call(someval); puts(someval) }


然而,因为仅确定一个块(例如这里的*`c1`*块)的功能就足够困难了,我故意在这个程序中避免使用任何不必要的额外块!

第二个块被命名为`c2`。这声明了“块参数”`z`。这也返回一个值:

c2 = proc{
|z|
z + 100
}


但是,这次返回的值可以被重用,因为块参数就像方法的一个传入参数——所以当`someval`被分配了`c2`的返回值之后,这个改变后的值随后被作为参数传递:

someval=1000
someval=c2.call(someval); puts(someval) #<= someval is now 1100
someval=c2.call(someval); puts(someval) #<= someval is now 1200


第三个块`c3`乍一看与第二个块`c2`几乎相同。实际上,唯一的区别是它的块参数被命名为`x`而不是`z`:

c3 = proc{
|x|
x + 100
}


块参数的名称对返回值没有影响。和之前一样,`someval`首先被分配了 1,100 的值(即,它的原始值 1,000 加上块内增加的 100)。然后,当块被第二次调用时,`someval`被分配了 1,200 的值(它的前一个值 1,100 加上块内增加的 100)。

但是现在看看局部变量`x`的值发生了什么。这个值在单元顶部被分配为 3,000。记住,在 Ruby 1.8 中,对块参数的赋值可以改变其周围上下文中同名变量的值。因此,在 Ruby 1.8 中,当块参数`x`改变时,局部变量`x`也会改变。现在它的值是 1,100——即,当调用`c3`块时块参数`x`最后的值:

x = 3000
someval=1000
someval=c3.call(someval); puts(someval) #=> 1100
someval=c3.call(someval); puts(someval) #=> 1200
puts( x ) # Ruby 1.8, x = 1100. Ruby 1.9, x = 3000


顺便提一下,尽管在 Ruby 1.8 中,块局部变量和块参数可以影响块外同名局部变量,但块变量本身在块外没有“存在”。你可以使用`defined?`关键字来验证这一点,尝试显示变量的类型,如果它确实被定义了:

print("x=[#{defined?(x)}],z=[#{defined?(z)}]")


这表明只有`x`,而不是块变量`z`,在主作用域中被定义:

x=[local-variable], z=[]


Ruby 的创造者 Matz 曾将块内的局部变量作用域描述为“令人遗憾的”。尽管 Ruby 1.9 解决了某些问题,但值得注意的是,块作用域的一个其他奇特特性仍然存在:即,块内的局部变量对包含该块的函数是不可见的。这可能在未来的版本中改变。以下是一个例子:

*local_var_scope.rb*

def foo
a = 100
[1,2,3].each do |b|
c = b
a = b
print("a=#{a}, b=#{b}, c=#{c}\n")
end
print("Outside block: a=#{a}\n") # Can't print #{b} and #{c} here!!!
end


在这里,块参数`b`和块局部变量`c`仅在块内部可见。块可以访问这两个变量以及变量`a`(`foo`方法的局部变量)。然而,在块外部,`b`和`c`是不可访问的,只有`a`是可见的。

仅仅是为了增加混淆,在先前的例子中,块局部变量`c`和块参数`b`在块外部都是不可访问的,但是当你使用`for`循环迭代一个块时,它们就变得可访问了,如下面的例子所示:

def foo2
a = 100
for b in [1,2,3] do
c = b
a = b
print("a=#{a}, b=#{b}, c=#{c}\n")
end
print("Outside block: a=#{a}, b=#{b}, c=#{b}\n")
end



# 第十一章。符号

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860138.png.jpg)

许多新接触 Ruby 的人对符号感到困惑。符号是一个以冒号(`:`)开头的标识符,所以`:this`是一个符号,`:that`也是。实际上,符号并不复杂——在某些情况下,它们可能非常有用,您很快就会看到。

让我们首先明确符号不是什么:它不是一个字符串,它不是一个常量,它也不是一个变量。符号简单地说,是一个没有内在意义的标识符,除了它的名字。而您可能像这样给变量赋值 . . .

name = "Fred"


您**不会**给符号赋值:

:name = "Fred" # Error!


符号的值就是它本身。因此,名为`:name`的符号的值是`:name`。

### 注意

对于符号的更技术性描述,请参阅深入挖掘中的深入挖掘。

当然,您之前已经使用过符号。例如,在第二章中,您通过将符号传递给`attr_reader`和`attr_writer`方法来创建属性读取器和写入器,如下所示:

attr_reader( :description )
attr_writer( :description )


您可能还记得,之前的代码导致 Ruby 创建一个`@description`实例变量以及一对名为`description`的获取器(读取器)和设置器(写入器)方法。Ruby 将符号的值视为字面量。`attr_reader`和`attr_writer`方法从该名称创建具有匹配名称的变量和方法。

# 符号和字符串

人们普遍认为符号是一种字符串类型。毕竟,符号`:hello`和字符串`"hello"`不是非常相似吗?实际上,符号与字符串截然不同。首先,每个字符串都是不同的——因此,从 Ruby 的角度来看,`"hello"`、`"hello"`和`"hello"`是三个具有三个不同`object_id`的独立对象。

*symbol_ids.rb*

These 3 strings have 3 different object_ids

puts( "hello".object_id ) #=> 16589436
puts( "hello".object_id ) #=> 16589388
puts( "hello".object_id ) #=> 16589340


但符号是唯一的,所以`:hello`、`:hello`和`:hello`都指向具有相同`object_id`的相同对象。

These 3 symbols have the same object_id

puts( :hello.object_id ) #=> 208712
puts( :hello.object_id ) #=> 208712
puts( :hello.object_id ) #=> 208712


在这方面,符号与整数的相似之处多于与字符串的相似之处。您可能记得,给定整数值的每个出现都指向同一个对象,所以`10`、`10`和`10`可以被认为是同一个对象,并且它们具有相同的`object_id`。记住,分配给对象的实际 ID 每次运行程序时都会改变。数字本身并不重要。重要的是要注意,每个独立的对象始终有一个唯一的 ID,因此当 ID 重复时,它表示对同一对象的重复引用。

*ints_and_symbols.rb*

These three symbols have the same object_id

puts( :ten.object_id ) #=> 20712
puts( :ten.object_id ) #=> 20712
puts( :ten.object_id ) #=> 20712

These three integers have the same object_id

puts( 10.object_id ) #=> 21
puts( 10.object_id ) #=> 21
puts( 10.object_id ) #=> 21


您还可以使用`equal?`方法进行相等性测试:

*symbols_strings.rb*

puts( :helloworld.equal?( :helloworld ) ) #=> true
puts( "helloworld".equal?( "helloworld" ) ) #=> false
puts( 1.equal?( 1 ) ) #=> true


作为独特的符号,提供了一个明确的标识符。您可以将符号作为参数传递给方法,例如:

amethod( :deletefiles )


一个方法可能包含用于测试传入参数值的代码:

*symbols_1.rb*

def amethod( doThis )
if (doThis == :deletefiles) then
puts( 'Now deleting files...')
elsif (doThis == :formatdisk) then
puts( 'Now formatting disk...')
else
puts( "Sorry, command not understood." )
end
end


符号也可以用于 `case` 语句中,它们提供了字符串的可读性和整数的唯一性:

case doThis
when :deletefiles then puts( 'Now deleting files...')
when :formatdisk then puts( 'Now formatting disk...')
else puts( "Sorry, command not understood." )
end


声明符号的作用域不会影响其唯一性。考虑以下情况:

*symbol_ref.rb*

module One
class Fred
end
$f1 = :Fred
end

module Two
Fred = 1
$f2 = :Fred
end

def Fred()
end

$f3 = :Fred


在这里,变量 `$f1`、`$f2` 和 `$f3` 在三个不同的作用域中分别被赋值为符号 `:Fred`:模块 `One`、模块 `Two` 和“主”作用域。以 `$` 开头的变量是全局的,因此一旦创建,就可以在任何地方引用。我将在第十二章(Chapter 12)中详细介绍模块。现在,只需将它们视为定义不同作用域的“命名空间”即可。然而,每个变量都引用相同的符号 `:Fred`,并且具有相同的 `object_id`。

All three display the same id!

puts( $f1.object_id ) #=> 208868
puts( $f2.object_id ) #=> 208868
puts( $f3.object_id ) #=> 208868


即使如此,符号的“含义”会根据其作用域而改变。在模块 `One` 中,`:Fred` 指的是类 `Fred`;在模块 `Two` 中,它指的是常量 `Fred = 1`;在主作用域中,它指的是方法 `Fred`。

之前程序的改写版本展示了这一点:

*symbol_ref2.rb*

module One
class Fred
end
$f1 = :Fred
def self.evalFred( aSymbol )
puts( eval( aSymbol.id2name ) )
end
end

module Two
Fred = 1
$f2 = :Fred
def self.evalFred( aSymbol )
puts( eval( aSymbol.id2name ) )
end
end

def Fred()
puts( "hello from the Fred method" )
end

$f3 = :Fred


首先,我使用两个冒号(`::`)在名为 `One` 的模块内部访问 `evalFred` 方法,这是 Ruby 的“作用域解析运算符”。然后,我将 `$f1` 传递给该方法:

One::evalFred( $f1 )


在这个上下文中,`Fred` 是在模块 `One` 内部定义的类的名称,因此当 `:Fred` 符号被评估时,会显示模块和类名:

One::Fred


接着,我将 `$f2` 传递给模块 `Two` 的 `evalFred` 方法:

Two::evalFred( $f2 )


在这个上下文中,`Fred` 是一个被赋值为整数 1 的常量,所以显示的是 `1`。最后,我调用一个名为 `method` 的特殊方法。这是一个 Object 的方法。它试图找到与作为参数传递给它的符号具有相同名称的方法,如果找到,则返回该方法作为可以调用的对象:

method($f3).call


`Fred` 方法存在于主作用域中,当被调用时,其输出是以下字符串:

"hello from the Fred method"


自然地,由于变量 `$f1`、`$f2` 和 `$f3` 引用了相同的符号,所以在任何特定时刻使用哪个变量都无关紧要。任何被符号赋值的变量,或者,实际上,符号本身,都会产生相同的结果。以下内容是等价的:

One::evalFred( $f1 ) #=> One::Fred
Two::evalFred( \(f2 ) #=> 1 method(\)f3).call #=> hello from the Fred method

One::evalFred( $f3 ) #=> One::Fred
Two::evalFred( \(f1 ) #=> 1 method(\)f2).call #=> hello from the Fred method

One::evalFred( :Fred ) #=> One::Fred
Two::evalFred( :Fred ) #=> 1
method(:Fred).call #=> hello from the Fred method


# 符号和变量

要了解符号和如变量名这样的标识符之间的关系,请查看 *symbols_2.rb* 程序。它首先将值 1 赋给局部变量 `x`。然后,它将符号 `:x` 赋给局部变量 `xsymbol`:

*symbols_2.rb*

x = 1
xsymbol = :x


在这一点上,变量 `x` 和符号 `:x` 之间没有明显的联系。我声明了一个方法,它简单地接受一些传入的参数,并使用 `p` 方法检查和显示它。我可以使用变量和符号调用此方法:

def amethod( somearg )
p( somearg )
end

Test 1

amethod( x )
amethod( :x )


这是方法打印出的数据:

1
:x


换句话说,`x` 变量的值是 1,因为这是分配给它的值,`:x` 的值是 `:x`。但出现的一个有趣问题是:如果 `:x` 的值是 `:x`,这也是变量 `x` 的符号名称,那么是否可以使用符号 `:x` 来找到变量 `x` 的值?困惑了吗?我希望下一行代码会使这一点更清晰:

Test 2

amethod( eval(:x.id2name))


这里,`id2name` 是 Symbol 类的一个方法。它返回与符号对应的名称或字符串(`to_s` 方法会执行相同的功能);最终结果是,当给定符号 `:x` 作为参数时,`id2name` 返回字符串“x。”Ruby 的 `eval` 方法(在 Kernel 类中定义)能够评估字符串中的表达式。在本例中,这意味着它找到字符串`x`并尝试将其作为可执行代码进行评估。它发现 `x` 是一个变量的名称,`x` 的值是 1。因此,值 1 被传递给 `amethod`。你可以通过运行 *symbols2.rb* 来验证这一点。

### 注意

将数据作为代码进行评估的详细解释请参阅第二十章。

事情可能会变得更加复杂。记住,变量 `xsymbol` 已经被分配了符号 `:x`。

x = 1
xsymbol = :x


这意味着如果你 eval `:xsymbol`,你可以获得分配给它的名称——即符号 `:x`。获得 `:x` 后,你可以继续评估这个符号,得到 `x` 的值,即 1:

Test 3

amethod( xsymbol ) #=> :x
amethod( :xsymbol ) #=> :xsymbol
amethod( eval(:xsymbol.id2name)) #=> :x
amethod( eval( ( eval(:xsymbol.id2name)).id2name ) ) #=> 1


正如你所看到的,当用于创建属性访问器时,符号可以引用方法名。你可以通过将方法名作为符号传递给 `method` 方法,然后使用 `call` 方法调用指定的方法来利用这一点:

Test 4

method(:amethod).call("")


`call` 方法允许你传递参数,所以,只是为了好玩,你可以通过评估一个符号来传递一个参数:

method(:amethod).call(eval(:x.id2name))


如果这看起来很复杂,请查看 *symbols_3.rb* 中的更简单示例。它从以下赋值开始:

*symbols_3.rb*

def mymethod( somearg )
print( "I say: " << somearg )
end

this_is_a_method_name = method(:mymethod)


这里 `method(:mymethod)` 寻找由作为参数传递的符号指定的方法名(`:mymethod`),如果找到了,它将返回具有相应名称的方法对象。在我的代码中,我有一个名为 `mymethod` 的方法,现在它被分配给了变量 `this_is_a_method_name`。

当你运行这个程序时,你会看到输出第一行打印了变量的值:

puts( this_is_a_method_name ) #=> #<Method: Object#mymethod>


这表明变量 `this_is_a_method_name` 已经被分配了方法 `mymethod`,该方法绑定到 Object 类(所有作为“独立”函数输入的方法都是绑定到 Object 类的)。为了验证变量确实是 Method 类的实例,下一行代码打印出它的类:

puts( "#{this_is_a_method_name.class}" ) #=> Method


好吧,如果这真的是一个方法,那么你应该能够调用它,不是吗?要做到这一点,你需要使用 `call` 方法。这正是代码最后一行所做的事情:

this_is_a_method_name.call( "hello world" ) #=>I say: hello world


# 为什么使用符号?

Ruby 类库中的一些方法指定符号作为参数。自然地,如果你需要调用这些方法,你必须向它们传递符号。然而,除了这些情况之外,在你的编程中并没有绝对必要使用符号。对于许多 Ruby 程序员来说,“传统”的数据类型,如字符串和整数,已经足够完美。然而,许多 Ruby 程序员确实喜欢使用符号作为散列的键。例如,当你查看第十九章(ch19.html "第十九章。Ruby on Rails")中的 Rails 框架时,你会看到类似以下示例的内容:

{ :text => "Hello world" }


然而,符号在“动态”编程中确实有一个特殊的位置。例如,Ruby 程序能够在某个类的范围内通过调用 `define_method` 并传递一个表示要定义的方法的符号和一个表示方法代码的代码块来在运行时创建一个新的方法:

*add_method.rb*

class Array
define_method( :aNewMethod, lambda{
|*args| puts( args.inspect)
} )
end


在执行之前的代码后,Array 类将获得一个名为 `aNewMethod` 的方法。你可以通过调用 `method_defined?` 并传递一个表示方法名称的符号来验证这一点:

Array.method_defined?( :aNewMethod ) #=> true


当然,你也可以调用该方法本身:

[].aNewMethod( 1,2,3 #=> [1,2,3]


你可以通过在类内部调用 `remove_method` 并传递一个提供要删除的方法名称的符号来以类似的方式在运行时删除现有方法:

class Array
remove_method( :aNewMethod )
end


动态规划在需要修改正在执行中的 Ruby 程序行为的应用中非常有价值。例如,在 Rails 框架中广泛使用动态规划,并在本书的最后一章中进行了深入讨论。

深入挖掘

符号是 Ruby 的基础。在这里,你将了解为什么是这样,以及如何显示所有可用的符号。

什么是符号?

之前我说过,符号是一个其值就是自身的标识符。从 Ruby 程序员的视角来看,这大致描述了符号的行为方式。但这并没有告诉你从 Ruby 解释器的视角来看符号是什么。实际上,符号是符号表的指针。符号表是 Ruby 的已知标识符的内部列表——例如变量和方法名称。

如果你想要深入了解 Ruby,你可以像这样显示 Ruby 所知道的全部符号:

*allsymbols.rb*

p( Symbol.all_symbols )


这将显示包括 `:to_s` 和 `:reverse` 这样的方法名称、`:$/` 和 `:$DEBUG` 这样的全局变量、`:Array` 和 `:Symbol` 这样的类名称在内的成千上万的符号。你可以使用数组索引来限制显示的符号数量,如下所示:

p( Symbol.all_symbols[0,10] )


在 Ruby 1.8 中,你不能对符号进行排序,因为符号不被认为是固有的顺序。在 Ruby 1.9 中,排序是可能的,符号字符被像字符串一样排序:

In Ruby 1.9

p [:a,:c,:b].sort #=> [:a,:b,:c]

In Ruby 1.8

p [:a,:c,:b].sort #=> 'sort': undefined method '<=>' for 🅰️Symbol


在避免与 Ruby 版本相关的兼容性问题的情况下,显示符号排序列表的最简单方法是将符号转换为字符串,并对这些字符串进行排序。在下面的代码中,我将 Ruby 所知的所有符号传递到一个块中,将每个符号转换为字符串,并将这些字符串收集到一个新的数组中,该数组被分配给`str_array`变量。现在我可以对这个数组进行排序并显示结果:

str_arr = Symbol.all_symbols.collect{ |s| s.to_s }
puts( str_arr.sort )



# 第十二章:模块和混入

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860138.png.jpg)

在 Ruby 中,每个类只有一个直接的“父类”,尽管每个父类可能有多个“子类”。通过将类层次结构限制为单行继承,Ruby 避免了那些允许多行继承的编程语言(如 C++)可能出现的某些问题。当类有多个父类和子类,并且它们的父类和子类也有其他父类和子类时,你可能会得到一个难以穿透的网络(一个结网?),而不是你可能期望的整洁、有序的层次结构。

尽管如此,有时对于不是紧密相关的类实现一些共享功能是有用的。例如,剑可能是一种武器,但也是一种宝藏;个人电脑可能是一种计算机,但也是一种投资;等等。但是,由于定义武器和宝藏或计算机和投资的类来自不同的祖先类,它们的类层次结构没有明显的共享数据和方法的途径。Ruby 解决这个问题的方案是通过模块提供的。

# 模块就像一个类……

模块的定义看起来非常类似于类的定义。实际上,模块和类是紧密相关的;模块类是类类的直接祖先。就像类一样,模块可以包含常量、方法和类。以下是一个简单的模块:

*simple_module.rb*

module MyModule
REWARD = 100

def prize
    return "You've won #{REWARD} credits"
end

end


如您所见,这包含一个常量`REWARD`和一个*实例方法*,`prize`。

# 模块方法

除了实例方法之外,模块还可能有模块方法。就像类方法以类的名称为前缀一样,模块方法以模块的名称为前缀:

def MyModule.lose
return "Sorry, you didn't win"
end


你可以像调用类的类方法一样调用模块的模块方法,使用点符号,如下所示:

MyModule.lose #=> "Sorry, you didn't win"


但如何调用实例方法?以下两种尝试都没有成功:

puts( prize ) # Error: undefined local variable or method
puts( MyModule.prize ) # Error: undefined method 'prize'


尽管它们有相似之处,但类有两个主要特征是模块不具备的:*实例*和*继承*。类可以有实例(从类创建的对象),超类(父类),和子类(子类);模块则没有这些。从模块的实例(一个“模块对象”)中调用实例方法是不可能的,简单的原因是无法创建模块的实例。这就是当你尝试调用前面代码中的`prize`方法时出现错误的原因。

### 注意

模块类确实有一个超类,即 Object。然而,你创建的任何命名模块都没有超类。有关模块和类之间关系的更详细说明,请参阅深入挖掘中的深入挖掘。

这引出了下一个问题:如果你不能从模块创建对象,模块有什么用?这可以用两个词来回答:*命名空间*和*混入*。Ruby 的混入提供了一种处理多重继承问题的方法。你很快就会了解混入是如何工作的。首先,让我们看看命名空间。

# 模块作为命名空间

你可以将模块视为围绕一组方法、常量和类的一种命名“包装”。模块内部的代码片段共享相同的“命名空间”,因此它们彼此可见,但对模块外部的代码不可见。

Ruby 类库定义了多个模块,例如`Math`和`Kernel`。`Math`模块包含数学方法,如`sqrt`来返回平方根,以及常量如`PI`。`Kernel`模块包含了许多你从一开始就一直在使用的方 法,如`print`、`puts`和`gets`。

假设你已经编写了这个模块:

*modules1.rb*

module MyModule
GOODMOOD = "happy"
BADMOOD = "grumpy"

def greet
    return "I'm #{GOODMOOD}. How are you?"
end

def MyModule.greet
    return "I'm #{BADMOOD}. How are you?"
end

end


你已经看到了如何使用模块方法,例如`MyModule.greet`,你可以像访问类常量一样访问模块常量,使用作用域解析运算符`::`,如下所示:

puts(MyModule::GOODMOOD) #=> happy


但你如何访问实例方法`greet`?这就是混入介入的地方。

# 包含的模块,或称为“混入”

一个对象可以通过使用`include`方法包含一个模块来访问该模块的实例方法。如果你要在你的程序中包含`MyModule`,那么该模块中的所有内容都会突然在当前作用域中存在。因此,`MyModule`的`greet`方法现在可以访问:

*modules2.rb*

include MyModule


注意,只有实例方法被包含。在先前的例子中,`greet`(实例)方法已被包含,但`MyModule.greet`(模块)方法没有被包含。由于它被包含,`greet`实例方法可以像当前作用域中的普通实例方法一样使用,而模块方法,也命名为`greet`,则是使用点符号来访问的:

puts( greet ) #=> I'm happy. How are you?
puts( MyModule.greet ) #=> I'm grumpy. How are you?


包含模块的过程也称为*混入*,这也解释了为什么包含的模块通常被称为混入。当你将模块混入类定义中时,从该类创建的任何对象都将能够使用混入模块的实例方法,就像它们在类本身中定义一样。这里`MyClass`类混入了`MyModule`模块:

*modules3.rb*

class MyClass
include MyModule

def sayHi
    puts( greet )
end

end


不仅这个类的方 法可以访问`MyModule`中的`greet`方法,从该类创建的任何对象也可以:

ob = MyClass.new
ob.sayHi #=> I'm happy. How are you?
puts(ob.greet) #=> I'm happy. How are you?


你可以将模块视为离散的代码单元,这可能有助于简化可重用代码库的创建。另一方面,你可能更感兴趣的是将模块用作多重继承的替代方案。

回到我在本章开头提到的例子,假设你有一个 Sword 类,它不仅是一件武器,也是一件宝物。也许 Sword 是 Weapon 类的后代(因此继承了 Weapon 的`deadliness`属性),但它还需要具有宝物的属性(如`value`和`owner`)。此外,由于这是一把精灵剑,它还需要具有魔法物品的属性。如果你在`Treasure`和`MagicThing`模块内部而不是类内部定义这些属性,Sword 类就能够包含这些模块,以便“混入”它们的方法或属性:

*modules4.rb*

module MagicThing
attr_accessor :power
end

module Treasure
attr_accessor :value
attr_accessor :owner
end

class Weapon
attr_accessor :deadliness
end

class Sword < Weapon # descend from Weapon
include Treasure # mix in Treasure
include MagicThing # mix in MagicThing
attr_accessor :name
end


现在 Sword 对象可以访问 Sword 类的方法和属性,以及其祖先类 Weapon 的方法和属性,还有混入的模块`Treasure`和`MagicThing`的方法和属性:

s = Sword.new
s.name = "Excalibur"
s.deadliness = "fatal"
s.value = 1000
s.owner = "Gribbit The Dragon"
s.power = "Glows when Orcs appear"
puts(s.name) #=> Excalibur
puts(s.deadliness) #=> fatal
puts(s.value) #=> 1000
puts(s.owner) #=> Gribbit The Dragon
puts(s.power) #=> Glows when Orcs appear


顺便提一下,任何属于模块的局部变量都无法从模块外部访问。即使模块内部的方法试图访问局部变量,并且该方法被模块外部的代码调用,例如通过混入模块,也是如此:

*mod_vars.rb*

x = 1 # local to this program

module Foo
x = 50 # local to module Foo

              # this can be mixed in but the variable x won't be visible
def no_bar
    return x
end

def bar
     @x = 1000
     return  @x
end
puts( "In Foo: x = #{x}" )   # this can access the module-local x

end

include Foo # mix in the Foo module


当你运行这个程序时,`puts`方法在模块初始化时执行,并显示模块局部变量`x`的值:

In Foo: x = 50


如果你想在程序的主作用域中显示变量`x`,则使用的是程序主作用域中局部变量`x`的值,*而不是*模块内部局部变量`x`的值:

puts(x) #=> 1


但尝试执行`no_bar`方法将会失败:

puts( no_bar ) # Error: undefined local variable or method 'x'


在这里,`no_bar`方法无法访问名为`x`的任何局部变量,尽管`x`在模块的作用域(`x = 50`)和当前或“主”作用域(`x = 1`)中都被声明了。但对于实例变量则没有这样的问题。`bar`方法能够返回实例变量`@x`的值:

puts(bar) #=> 1000


模块可以有自己的实例变量,这些变量仅属于模块“对象”。这些实例变量将适用于模块方法:

*inst_class_vars.rb*

module X
@instvar = "X's @instvar"

def self.aaa
    puts(@instvar)
end

end

X.aaa #=> X's @instvar


但在实例对象中引用的实例变量“属于”将模块混入的作用域:

module X
@instvar = "X's @instvar"
@anotherinstvar = "X's 2nd @instvar"

    def amethod
         @instvar = 10       # creates @instvar in current scope
         puts(@instvar)
    end

end

include X
p( @instvar ) #=> nil
amethod #=> 10
puts( @instvar ) #=> 10
@instvar = "hello world"
puts( @instvar ) #=> "hello world"


类变量也被混入,并且像实例变量一样,它们的值可以在当前作用域内重新赋值:

module X
@@classvar = "X's @@classvar"
end

include X

puts( @@classvar ) #=> X's @classvar
@@classvar = "bye bye"
puts( @@classvar ) #=> "bye bye"


你可以使用`instance_variables`方法获取实例变量名称的数组:

p( X.instance_variables ) #=> [:@instvar, @anotherinstvar]
p( self.instance_variables ) #=> [:@instvar]


在这里,`X.instance_variables`返回属于 X 类的实例变量列表,而`self.instance_variables`返回当前、主对象的实例变量。`@instvar`变量在每个情况下都是不同的。

### 注意

在 Ruby 1.9 中,`instance_variables`方法返回一个符号数组。在 Ruby 1.8 中,它返回一个字符串数组。

# 命名冲突

模块方法(那些特别以模块名开头的方法)可以帮助保护你的代码免受意外的名称冲突。然而,模块中的实例方法并没有提供此类保护。假设你有两个模块——一个叫 `Happy`,另一个叫 `Sad`。它们各自包含一个名为 `mood` 的模块方法和一个名为 `expression` 的实例方法。

*happy_sad.rb*

module Happy
def Happy.mood # module method
return "happy"
end

def expression        # instance method
    return "smiling"
end

end
module Sad
def Sad.mood # module method
return "sad"
end

def expression        # instance method
    return "frowning"
end

end


现在类 `Person` 包含了这两个模块:

class Person
include Happy
include Sad
attr_accessor :mood

def initialize
    @mood = Happy.mood
end

end


`Person` 类的 `initialize` 方法需要使用包含模块中的一个 `mood` 方法来设置其 `@mood` 变量的值。他们都有 `mood` 方法的事实并没有问题;作为一个模块方法,`mood` 必须在模块名之前,所以 `Happy.mood` 不会与 `Sad.mood` 混淆。

但是 `Happy` 和 `Sad` 模块也各自包含一个名为 `expression` 的方法。这是一个 *实例* 方法,当这两个模块都被包含在 `Person` 类中时,可以直接调用 `expression` 方法,无需任何限定:

p1 = Person.new
puts(p1.expression)


这里对象 `p1` 使用的是哪种 `expression` 方法?结果发现它使用的是最后定义的方法。在示例案例中,这恰好是 `Sad` 模块中定义的方法,因为 `Sad` 是在 `Happy` 之后被包含的。所以,`p1.expression` 返回的是“皱眉。”如果你改变包含顺序,使得 `Happy` 在 `Sad` 之后被包含,`p1` 对象将使用 `Happy` 模块中定义的 `expression` 方法,并显示“微笑。”

在沉迷于创建大型、复杂的模块并将它们定期混入你的类之前,请记住这个潜在的问题:*包含的具有相同名称的实例方法将“覆盖”彼此*。这个问题在我的小程序中可能很容易发现。但在一个大型应用程序中可能就不那么明显了!

# 别名方法

当你使用来自多个模块的具有相似名称的方法时,避免歧义的一种方法是对这些方法进行 *别名*。别名是现有方法的副本,具有新名称。你使用 `alias` 关键字后跟新名称,然后是旧名称:

alias happyexpression expression


你也可以使用 `alias` 来复制被覆盖的方法,以便你可以具体引用其覆盖定义之前的版本:

*alias_methods.rb*

module Happy
def Happy.mood
return "happy"
end

def expression
    return "smiling"
end
alias happyexpression expression

end

module Sad
def Sad.mood
return "sad"
end

def expression
    return "frowning"
end
alias sadexpression expression

end

class Person
include Happy
include Sad
attr_accessor :mood
def initialize
@mood = Happy.mood
end
end

p2 = Person.new
puts(p2.mood) #=> happy
puts(p2.expression) #=> frowning
puts(p2.happyexpression) #=> smiling
puts(p2.sadexpression) #=> frowning


# 小心混入!

尽管每个类只能继承一个超类,但它可以混入多个模块。实际上,将一组模块混入另一组模块,然后将这些其他模块混入类,然后将这些类放入更多的模块中,然后继续这样做是完全允许的。

以下是一些代码示例,它子类化了某些类,混入了某些模块,甚至在混入的模块中子类化了类。我故意简化了以下代码,以便你能看到正在发生的事情。要查看完整的工作示例的恐怖之处,请参阅代码存档中提供的示例程序,*multimods.rb*:

*multimods.rb*

This is an example of how NOT to use modules!

module MagicThing # module
class MagicClass # class inside module
end
end

module Treasure # module
end

module MetalThing
include MagicThing # mixin
class Attributes < MagicClass # subclasses class from mixin
end
end

include MetalThing # mixin
class Weapon < MagicClass # subclass class from mixin
class WeaponAttributes < Attributes # subclass
end
end

class Sword < Weapon # subclass
include Treasure # mixin
include MagicThing # mixin
end


让我强调,之前展示并包含在存档中的代码并不是作为要仿效的模型。远非如此!它仅仅是为了展示一个过度使用模块的程序可能会变得多么难以理解,几乎无法调试。

简而言之,尽管在使用得当的情况下,模块可以帮助避免与 C++ 多重继承相关的一些复杂性,但它们仍然容易受到误用的威胁。如果程序员真的想创建具有难以理解的依赖关系的复杂类层次结构,那么他们当然可以做到。*multimods.rb* 中的代码展示了如何仅用几行代码就写出难以理解的程序。想象一下,在成千上万行代码和数十个代码文件中你能做什么!在混合模块之前要仔细思考。

# 从文件中包含模块

到目前为止,我混合了在单个源文件中定义的模块。通常,在单独的文件中定义模块并在需要时混合它们更有用。为了使用其他文件中的代码,你必须首先使用 `require` 方法加载该文件,如下所示:

*require_module.rb*

require( "./testmod.rb" )


可选地,你可以省略文件扩展名:

require( "./testmod" ) # this works too


如果没有指定路径,所需的文件必须在当前目录、搜索路径或预定义数组变量 `$:` 中列出的文件夹中。你可以使用常规的数组追加方法 `<<` 将目录添加到这个数组变量中,如下所示:

$: << "C:/mydir"


### 注意

全局变量 `$:`(一个美元符号和一个冒号)包含一个字符串数组,表示 Ruby 在查找已加载或所需的文件时搜索的目录。

在 Ruby 1.8 和 Ruby 1.9 中 `require` 的行为存在一个已记录的差异。在 Ruby 1.8 中,文件名不会被转换成绝对路径,因此 `require "a"; require "./a"` 会加载 `a.rb` 两次。而在 Ruby 1.9 中,文件名会被转换成绝对路径,所以 `require "a"; require "./a"` 不会加载 `a.rb` 两次。

此外,我发现至少在某些版本的 Ruby 1.9 中,如果你使用未指定扩展名的文件名,如 `require("testmod")`,`require` 可能无法从当前目录加载文件。在这种情况下,会抛出一个 LoadError 异常。这通常发生在全局变量 `$:` 中存储的可搜索目录数组不包含当前目录时。你可以通过运行以下代码来验证这一点:

*search_dirs.rb*

puts( $: )


搜索路径将按行显示。应该有一行显示单个点(`.`),代表当前目录。如果这个点缺失,那么当前目录中的文件不在搜索路径上,无法使用未指定扩展名的文件名来加载。

为了确保文件被加载,我在文件名前加了一个点来指定当前目录,现在这成功了:`require( "./testmod" )`。或者,你也可以使用`require_relative`方法,尽管这是 Ruby 1.9 的新特性,不能在早期版本中使用:

require_relative( "testmod.rb" ) # Ruby 1.9 only


或者,如果`$:`不包含当前目录,你可以将其添加进去。一旦这样做,`require`将能够与当前目录中文件的未限定名称一起工作:

$: << "." # add current directory to array of search paths
require( "testmod.rb" )


`require`方法如果成功加载指定的文件,则返回`true`值;否则,返回`false`。如果文件不存在,它返回一个 LoadError。如果有疑问,你可以简单地显示结果。

puts(require( "testmod.rb" )) #=> true, false or LoadError


当文件运行时通常会被执行的任何代码,在文件被`require`时也会被执行。所以,如果文件*testmod.rb*包含以下代码:

*testmod.rb*

def sing
puts( "Tra-la-la-la-la....")
end

puts( "module loaded")
sing


当运行*require_module.rb*程序并要求*testmod.rb*时,这将显示:

module loaded
Tra-la-la-la-la....


当在所需的文件中声明模块时,它可以被混合:

require_module2.rb
require( "testmod.rb")
include MyModule #mix in MyModule declared in testmod.rb


Ruby 还允许你使用`load`方法加载一个文件。在大多数方面,`require`和`load`可以被视为可互换的。但是,它们之间有一些细微的差别。特别是,`load`可以接受一个可选的第二个参数,如果这个参数是`true`,则将代码作为未命名的或匿名模块加载和执行:

load( "testmod.rb", true)


当第二个参数是`true`时,加载的文件不会将新的命名空间引入主程序,你将无法访问加载文件中的模块。在这种情况下,模块的方法、常量和实例方法将**不会**对你的代码可用:

*load_module.rb*

load( "testmod.rb", true)

puts( MyModule.greet ) #=>Error:uninitialized constant Object::MyModule
puts(MyModule::GOODMOOD) #=>Error:uninitialized constant Object::MyModule
include MyModule #=>Error:uninitialized constant Object::MyModule
puts( greet ) #=>Error:undefined local variable or method 'greet'


然而,当`load`的第二个参数是`false`或者没有第二个参数时,你将**能够**访问加载文件中的模块:

*load_module_false.rb*

load( "testmod.rb", false)

puts( MyModule.greet ) #=> I'm grumpy. How are you?
puts(MyModule::GOODMOOD) #=> happy
include MyModule #=> [success]
puts( greet ) #=> I'm happy. How are you?


注意,你必须使用`load`输入完整的文件名(*testmod*去掉*.rb*扩展名是不够的)。`load`和`require`之间的另一个区别是,`require`只加载文件一次(即使你的代码多次要求该文件),而`load`每次调用`load`都会重新加载指定的文件。假设你有一个包含以下代码的文件*test.rb*:

*test.rb*

MyConst = 1
if @a == nil then
@a = 1
else
@a += MyConst
end

puts @a


你现在**require**这个文件三次:

*require_again.rb*

require "./test"
require "./test"
require "./test"


这将是输出:

1


但如果你**加载**该文件三次 . . .

*load_again.rb*

load "test.rb"
load "test.rb"
load "test.rb"


然后,这将输出:

1
./test.rb:1: warning: already initialized constant MyConst
2
./test.rb:1: warning: already initialized constant MyConst
3


深入挖掘

模块与类究竟是如何相关的?在这里,我们回答这个问题,检查一些重要的 Ruby 模块,并了解如何使用模块来扩展对象。

模块和类

在本章中,我讨论了模块的**行为**。现在,让我们弄清楚模块究竟**是什么**。结果证明,就像 Ruby 中的大多数其他事物一样,模块是一个对象。每个命名的模块实际上都是 Module 类的实例:

*module_inst.rb*

module MyMod
end

puts( MyMod.class ) #=> Module


你不能创建**命名模块**的子类,所以这是不允许的:

module MyMod
end

module MyOtherMod < MyMod # You can't do this!
end


然而,与其他类一样,可以创建 `Module` 类的子类:

class X < Module # But you can do this
end


事实上,`Class` 类本身是 `Module` 类的子类。它继承了 `Module` 的行为并添加了一些重要的新行为,特别是创建对象的能力。您可以通过运行 *modules_classes.rb* 程序来验证 `Module` 是 `Class` 的超类,该程序显示了此层次结构:

*modules_classes.rb*

Class
Module #=> is the superclass of Class
Object #=> is the superclass of Module
BasicObject #=> (in Ruby 1.9) is the superclass of Module


预定义模块

以下模块是 Ruby 解释器内建的:`Comparable`、`Enumerable`、`FileTest`、`GC`、`Kernel`、`Math`、`ObjectSpace`、`Precision`、`Process` 和 `Signal`。

`Comparable` 是一个允许包含类实现比较运算符的混入模块。包含的类必须定义 `<=>` 运算符,该运算符将接收者与另一个对象进行比较,根据接收者是否小于、等于或大于另一个对象返回 -1、0 或 +1。

+   `Comparable` 使用 `<=>` 实现传统的比较运算符(`<`、`<=`、`==`、`>=` 和 `>`)和 `between?` 方法。

+   `Enumerable` 是一个用于枚举的混入模块。包含的类必须提供 `each` 方法。

+   `FileTest` 是一个包含文件测试函数的模块;其方法也可以从 `File` 类访问。

+   `GC` 模块提供了 Ruby 标记和清除垃圾收集机制的接口。一些底层方法也可以通过 `ObjectSpace` 模块访问。

+   `Kernel` 是由 `Object` 类包含的模块;它定义了 Ruby 的“内置”方法。

+   `Math` 是一个包含基本三角函数和超越函数模块函数的模块。它具有相同定义和名称的“实例方法”和模块方法。

+   `ObjectSpace` 是一个包含与垃圾收集设施交互的例程的模块,并允许您使用迭代器遍历所有活动对象。

+   `Precision` 是一个用于具有精度的具体数值类的混入模块。在这里,“精度”意味着实数的近似精度,因此此模块不应包含在不是实数子集的任何内容中(因此不应包含在复数或矩阵等类中)。

+   `Process` 是用于操作进程的模块。它所有的方法都是模块方法。

+   `Signal` 是处理发送给运行进程的信号的模块。可用的信号名称列表及其解释取决于系统。

以下是对三个最常用的 Ruby 模块的一个简要概述。

**Kernel**

预定义模块中最重要的一个是 `Kernel`,它提供了许多“标准”Ruby 方法,如 `gets`、`puts`、`print` 和 `require`。与 Ruby 类库的许多部分一样,`Kernel` 是用 C 语言编写的。尽管 `Kernel` 实际上“内置于”Ruby 解释器中,但从概念上讲,它可以被视为一个混合模块,就像一个正常的 Ruby 混合模块一样,它将方法直接提供给任何需要它的类。由于它被混合到所有其他 Ruby 类继承的 `Object` 类中,因此 `Kernel` 的方法对所有类都是通用的。

**Math**

`Math` 模块的方法既提供为“模块”方法又提供为“实例”方法,因此可以通过将 `Math` 混合到类中或通过使用模块名称、点和方法名称来“从外部”访问模块方法;你可以使用双冒号来访问常量:

*math.rb*

puts( Math.sqrt(144) ) #=> 12.0
puts( Math::PI ) #=> 3.141592653589793


**Comparable**

`Comparable` 模块提供了定义自己的比较“运算符”的便捷能力,如 `<`, `<=`, `==`, `>=`, 和 `>`(严格来说,这些是方法,但它们可以像其他语言中的比较运算符一样使用)。这是通过将模块混合到你的类中并定义 `<=>` 方法来实现的。然后你可以指定比较当前对象中的某个值与其他值的标准。例如,你可以比较两个整数、两个字符串的长度,或者一些更古怪的价值,比如字符串在数组中的位置。我在示例程序 *compare.rb* 中选择了这种古怪的比较类型。它使用神话生物数组中字符串的索引来比较一个生物的名字与另一个生物的名字。一个低索引,如索引 0 的 `hobbit`,被认为“小于”一个高索引,如索引 6 的 `dragon`:

*compare.rb*

class Being
include Comparable

   BEINGS = ['hobbit','dwarf','elf','orc','giant','oliphant','dragon']

   attr_accessor :name

   def <=> (anOtherName)
           BEINGS.index(@name)<=>BEINGS.index(anOtherName.name)
   end

   def initialize( aName )
           @name = aName
   end

end

elf = Being.new('elf')
orc = Being.new('orc')
giant = Being.new('giant')

puts( elf < orc ) #=> true
puts( elf > giant ) #=> false


范围解析

与类一样,你可以使用双冒号范围解析运算符来访问模块内部声明的常量(包括类和其他模块)。例如,假设你有一个嵌套的模块和类,如下所示:

module OuterMod
moduleInnerMod
class Class1
end
end
end


你可以使用 `::` 运算符来访问 `Class1`,如下所示:

OuterMod::InnerMod::Class1


### 注意

有关类中常量范围解析的介绍,请参阅第二章。

每个模块和类都有自己的范围,这意味着单个常量名可能在不同的范围内使用。因此,你可以使用 `::` 运算符来指定精确范围内的常量:

Scope1::Scope2::Scope3 #...etc


如果你在这个常量名的开头使用这个运算符,这将导致跳出当前范围并访问“顶级”范围:

::ACONST # refers to ACONST at top-level scope


以下程序提供了一些范围运算符的示例:

*scope_resolution.rb*

ACONST = "hello" # This is a top-level constant

module OuterMod
module InnerMod
ACONST=10 # OuterMod::InnerMod::ACONST
class Class1
class Class2
module XYZ
class ABC
ACONST=100 # Deeply nested ACONST
def xyz
puts( ::ACONST ) # <= This refers to top-level ACONST
end
end
end
end
end
end
end

puts(OuterMod::InnerMod::ACONST) #=> 10
puts(OuterMod::InnerMod::Class1::Class2::XYZ::ABC::ACONST) #=> 100
ob = OuterMod::InnerMod::Class1::Class2::XYZ::ABC.new
ob.xyz #=> hello


模块函数

如果你希望一个函数既作为实例方法又作为模块方法可用,你可以使用与实例方法名称匹配的 `module_function` 方法,如下所示:

*module_func.rb*

module MyModule
def sayHi
return "hi!"
end

def sayGoodbye
    return "Goodbye"

end

module_function :sayHi

end


`sayHi` 方法现在可以被混合到类中,并用作实例方法:

class MyClass
include MyModule
def speak
puts(sayHi)
puts(sayGoodbye)
end
end


它可以用作模块方法,使用点符号表示:

ob = MyClass.new
ob.speak #=> hi!\nGoodbye
puts(MyModule.sayHi) #=> hi!


由于这里的 `sayGoodbye` 方法不是一个模块函数,因此不能以这种方式使用:

puts(MyModule.sayGoodbye) #=> Error: undefined method


Ruby 在其一些标准模块中使用了 `module_function`,例如 `Math`(在 Ruby 库文件 *complex.rb* 中),来创建模块和实例方法的“匹配对”。

扩展对象

你可以使用 `extend` 方法将模块的方法添加到特定的对象(而不是整个类)中,如下所示:

*extend.rb*

module A
def method_a
puts( 'hello from a' )
end
end

class MyClass
def mymethod
puts( 'hello from mymethod of class MyClass' )
end
end

ob = MyClass.new
ob.mymethod #=> hello from mymethod of class MyClass
ob.extend(A)


现在,对象 `ob` 被扩展了模块 `A`,它可以访问该模块的实例方法 `method_a`:

ob.method_a #=> hello from a


实际上,你可以一次扩展一个对象为多个模块。在这里,模块 `B` 和 `C` 扩展了对象 `ob`:

module B
def method_b
puts( 'hello from b' )
end
end

module C
def mymethod
puts( 'hello from mymethod of module C' )
end
end

ob.extend(B, C)
ob.method_b #=> hello from b
ob.mymethod #=> hello from mymethod of module C


当一个对象被扩展为一个包含与对象类中方法同名的方法的模块时,模块中的方法将替换类中的方法。因此,当 `ob` 被扩展为 `C` 并调用 `ob.mymethod` 时,将显示字符串“hello from mymethod of module `C`”,而不是在 `ob` 扩展模块 `C` 之前显示的“hello from mymethod of class MyClass”。

冻结对象

你可以通过使用 `freeze` 方法“冻结”对象来显式防止对象被扩展:

ob.freeze


任何尝试进一步扩展此对象的行为都将导致运行时错误:

module D
def method_d
puts( 'hello from d' )
end
end
ob.extend( D ) #=> Error: can't modify frozen object (RuntimeError)


为了避免这种错误,你可以使用 `frozen?` 方法来测试一个对象是否已被冻结:

if !(ob.frozen?)
ob.extend( D )
ob.method_d
else
puts( "Can't extend a frozen object" )
end



# 第十三章。文件和 IO

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860138.png.jpg)

Ruby 提供了专门用于处理输入和输出的类(IO)。其中最重要的是一个名为 IO 的类。IO 类允许你打开和关闭 IO 流(字节的序列),并从它们读取和写入数据。

例如,假设你有一个名为*textfile.txt*的文件,其中包含一些文本行,这是你打开文件并在屏幕上显示每一行的方法:

*io_test.rb*

IO.foreach("testfile.txt") {|line| print( line ) }


在这里,`foreach`是 IO 的一个类方法,因此你不需要创建一个新的 IO 对象来使用它;相反,你只需指定文件名作为参数。`foreach`方法接受一个块,其中每个从文件中读取的行作为参数传递。当你完成读取后,你不需要打开文件进行读取并关闭它(正如你可能从其他语言的经验中预期的那样),因为 Ruby 的`IO.foreach`方法会为你完成这些操作。

IO 类有其他一些有用的方法。例如,你可以使用`readlines`方法将文件内容读取到数组中,以便进行进一步处理。以下是一个简单的示例,它再次将行打印到屏幕上:

lines = IO.readlines("testfile.txt")
lines.each{|line| print( line )}


File 类是 IO 的子类,前面的示例可以使用 File 类重写:

*file_test.rb*

File.foreach("testfile.txt") {|line| print( line ) }

lines = File.readlines("testfile.txt")
lines.each{|line| print( line )}


# 打开和关闭文件

尽管一些标准方法可以自动打开和关闭文件,但在处理文件内容时,通常需要显式地打开和关闭文件。你可以使用`new`或`open`方法打开一个文件。你必须向这些方法之一传递两个参数——文件名和文件“模式”,它将返回一个新的 File 对象。文件模式可以是操作系统特定的常量定义的整数,也可以是字符串。模式通常表示文件是用于读取(`"r"`)、写入(`"w"`)还是读写(`"rw"`)表 13-1 显示了可用的字符串模式列表。

表 13-1. 文件模式字符串

| 模式 | 含义 |
| --- | --- |
| `"r"` | 只读,从文件开头开始(默认模式) |
| `"r+"` | 读写,从文件开头开始 |
| `"w"` | 只写,截断现有文件到零长度或创建一个新文件用于写入 |
| `"w+"` | 读写,截断现有文件到零长度或创建一个新文件用于读写 |
| `"a"` | 只写,如果文件存在,从文件末尾开始;否则,创建一个新文件用于写入 |
| `"a+"` | 读写,如果文件存在,从文件末尾开始;否则,创建一个新文件用于读写 |
| `"b"` | (仅限 DOS/Windows)二进制文件模式(可以与前面列出的任何关键字一起出现) |

让我们看看一个打开、处理和关闭文件的实际示例。在 *open_close.rb* 中,我首先以写入模式(`"w"`)打开一个文件,*myfile.txt*。当一个文件以写入模式打开时,如果它不存在,将会创建一个新文件。我使用 `puts()` 将六个字符串写入文件,每个字符串占一行。最后,我关闭文件:

f = File.new("myfile.txt", "w")
f.puts( "I", "wandered", "lonely", "as", "a", "cloud" )
f.close


关闭文件不仅释放了*文件句柄*(指向文件数据的指针),还将任何数据从内存中“刷新”出来,以确保所有数据都保存到磁盘上的文件中。

将文本写入文件后,让我们看看如何打开该文件并读取数据。这次我将逐个字符读取数据,直到文件末尾(`eof`)。在这个过程中,我会记录已读取的字符数。我还会记录行数,每当读取到换行符(ASCII 码 10)时,行数会增加。为了清晰起见,我会在每行读取的末尾添加一个字符串,显示其行号。我将在屏幕上显示字符以及我的行结束字符串,当从文件中读取完所有内容后,我将关闭文件并显示我计算出的统计数据。以下是完整的代码:

*open_close.rb*

f = File.new("myfile.txt", "w")
f.puts( "I", "wandered", "lonely", "as", "a", "cloud" )
f.close # Try commenting this out!

charcount = 0
linecount = 0
f = File.new("myfile.txt", "r")
while !( f.eof ) do # while not at end of file...
c = f.getc() # get a single character
if ( c.ord == 10 ) then # test ASCII code (Ruby 1.9)
linecount += 1
puts( " <End Of Line #{linecount}>" )
else
putc( c ) # put the char to screen
charcount += 1
end
end
if f.eof then
puts( "" )
end
f.close
puts("This file contains #{linecount} lines and #{charcount} characters." )


### 注意

此代码是为 Ruby 1.9 编写的,不能在 Ruby 1.8 中运行。有关更多详细信息,请参阅以下部分。

以这种方式操作文件时,程序员有责任确保在向文件写入或从中读取数据后关闭文件。未能关闭文件可能会导致不可预测的副作用。例如,尝试注释掉前面的第一个 `f.close`(在上一段代码的第三行)看看会发生什么!你会发现当程序随后尝试读取文件内容时,找不到数据,并且返回零行和字符数!

# 字符和兼容性

*open_close.rb* 程序是为 Ruby 1.9 编写的,不能在 Ruby 1.8 中运行。这是因为当 Ruby 1.8 返回单个字符时,它被视为一个整数 ASCII 值,而在 Ruby 1.9 中,它被视为一个单字符字符串。因此,当 `getc()` 返回字符 `c` 时,Ruby 1.8 能够测试其 ASCII 值(`c == 10`),而 Ruby 1.9 必须将其作为字符串测试(`c == "\n"`)或使用 `ord` 方法将字符转换为整数:(`c.ord == 10`)。`ord` 方法在 Ruby 1.8 中不存在。

作为一条基本原则,如果你想编写能在不同版本的 Ruby 中运行的程序,你可以通过测试 `RUBY_VERSION` 常量的值来绕过不兼容性问题。这个常量返回一个表示版本号的字符串,例如 1.9.2。你可以简单地使用 `to_f` 方法将字符串转换为浮点数,然后根据值是否大于 1.8 来采取不同的行动:

if (RUBY_VERSION.to_f > 1.8) then
c = c.ord
end


或者,你也可以分析字符串以确定次要和主要版本号。例如,这里有一个非常简单的方法,它通过索引到`RUBY_VERSION`字符串来获取第一个字符作为主要版本(1 或 2)和第二个字符作为次要版本(例如,8 或 9)。如果 Ruby 版本是 1.9 或更高,则返回`true`,否则返回`false`:

`*open_close2.rb*`

def isNewRuby
newR = false # is this > Ruby version 1.8?
majorNum = RUBY_VERSION[0,1]
minorNum = RUBY_VERSION[2,1]
if ( majorNum == "2" ) || (minorNum == "9" ) then
newR = true
else
newR == false
end
return newR
end


你可以在你的代码中使用这个测试来处理兼容性问题。在这里,`ord`方法仅当 Ruby 版本为 1.9 或更高时才应用于字符`c`:

if (isNewRuby) then
c = c.ord
end


# 文件和目录

你也可以使用`File`类来操作磁盘上的文件和目录。在尝试对文件执行某些操作之前,你必须确保文件存在。毕竟,文件可能在程序开始后已被重命名或删除——或者用户可能错误地输入了文件或目录名称。

你可以使用`File.exist?`方法来验证文件的存在。这是`FileTest`模块提供给`File`类的一些测试方法之一。就`File.exist?`方法而言,目录被视为文件,因此你可以使用以下代码来测试`*C:\*`驱动器的存在(注意,在字符串中必须使用双文件分隔符`"\\"`字符,因为单个`"\"`将被视为转义字符):

`*file_ops.rb*`

if File.exist?( "C:\" ) then
puts( "Yup, you have a C:\ directory" )
else
puts( "Eeek! Can't find the C:\ drive!" )
end


如果你想要区分目录和数据文件,可以使用`directory?`方法:

def dirOrFile( aName )
if File.directory?( aName ) then
puts( "#{aName} is a directory" )
else
puts( "#{aName} is a file" )
end
end


# 复制文件

让我们通过编写一个简单的文件备份程序来让`File`类发挥实际作用。当你运行`*copy_files.rb*`时,系统会要求你选择一个要复制的目录(源目录)以及另一个要复制的目录(目标目录)。假设这两个目录都存在,程序随后会将源目录中的所有文件复制到目标目录。如果目标目录不存在,程序会询问你是否希望创建它,在这种情况下你应该输入`*Y*`来接受。我已经为你提供了一个源目录;当提示时,只需输入名称`*srcdir*`。当询问目标目录时,输入`*targetdir*`以在当前目录下创建一个同名子目录。

程序使用源目录的路径初始化变量`sourcedir`,并使用目标目录的名称初始化`targetdir`。这是执行文件复制的代码:

`*copy_files.rb*`

Dir.foreach( sourcedir ){
|f|
filepath = "#{sourcedir}\#{f}"
if !(File.directory?(filepath) ) then
if File.exist?("#{targetdir}\#{f}") then
puts("#{f} already exists in target directory" )
else
FileUtils.cp( filepath, targetdir )
puts("Copying... #{filepath}" )
end
end
}


在这里,我使用了`Dir`类的`foreach`方法,它将指定目录中每个文件的文件名`f`传递给一个块。我将在稍后详细介绍`Dir`类。代码通过将文件名附加到由`sourcedir`变量给出的目录名称来构造文件的合格路径`filepath`。我只想要复制数据文件,而不是目录,所以我测试`filepath`是否是文件而不是目录:

if !(File.directory?(filepath) )


我不希望这个程序复制已经存在的文件,因此它首先检查目标目录`targetdir`中是否已经存在名为`f`的文件:

if File.exist?("#{targetdir}\#{f}")


最后,假设所有指定的条件都满足,源文件`filepath`将被复制到`targetdir`:

FileUtils.cp( filepath, targetdir )


在这里,`cp`是`FileUtils`模块中找到的一个文件复制方法。此模块还包含许多其他有用的文件处理例程,例如`mv(source, target)`将文件从`source`移动到`target`,`rm(files)`删除`files`参数中列出的一个或多个文件,以及`mkdir`创建目录,就像我在当前程序中创建`targetdir`时做的那样:

FileUtils.mkdir( targetdir )


# 目录查询

我的备份程序一次只处理一个目录级别,这就是为什么它在尝试复制文件`f`之前会检查它不是一个目录。然而,有很多时候你可能想要遍历子目录。作为一个例子,让我们编写一个程序,计算指定根目录下所有子目录的大小。如果你想要定位最大的文件和目录,以便通过存档或删除它们来释放磁盘空间,这可能很有用。

在子目录中导航创建了一个有趣的编程问题。当你开始搜索子目录的存在时,你不知道你会找到多少个,一个也没有,或者很多。此外,你找到的任何子目录可能还包含另一个级别的子目录,每个子目录可能还包含其他子目录,以此类推,可能有很多层级。

# 对递归的探讨

此程序需要能够导航到整个子目录树中的任何数量的层级。为了能够做到这一点,你必须使用*递归*。简单来说,递归方法是一种调用自身的方法。如果你不熟悉递归编程,请参阅深入挖掘中的深入挖掘。

在程序*file_info.rb*中,`processfiles`方法是递归的:

*file_info.rb*

def processfiles( aDir )
totalbytes = 0
Dir.foreach( aDir ){
|f|
mypath = "#{aDir}\#{f}"
s = ""
if File.directory?(mypath) then
if f != '.' and f != '..' then
bytes_in_dir = processfiles(mypath) # <==== recurse!
puts( "

--->
#{mypath} contains [#{bytes_in_dir/1024}] KB" )
end
else
filesize = File.size(mypath)
totalbytes += filesize
puts ( "#{mypath} : #{filesize/1024}K" )
end
}
$dirsize += totalbytes
return totalbytes
end


你会看到,当方法首次被调用时,在源代码的底部,它通过变量`dirname`传递了目录的名称:

processfiles( dirname )


我已经分配了当前目录的父目录,由两个点表示:

dirname = ".."


如果你在这个程序的原位置(即从本书的源代码存档中提取的位置)运行此程序,这将引用包含所有示例代码文件子目录的目录。或者,你也可以将硬盘上某个目录的名称分配给变量`dirname`。如果你这样做,不要指定包含大量文件和目录的目录(在 Windows 上,*C:\Program Files*不是一个好选择,而*C:\*会更糟!)因为程序执行将花费相当长的时间。

让我们更仔细地看看 `processfiles` 方法中的代码。再次使用 `Dir.foreach` 来找到当前目录中的所有文件,并将每个文件 `f` 逐个传递给花括号之间的代码块进行处理。如果 `f` 是一个目录并且不是当前目录 (`'.'`) 或其父目录 (`'..'`),那么我将目录的完整路径传递回 `processfiles` 方法:

if File.directory?(mypath) then
if f != '.' and f != '..' then
bytes_in_dir = processfiles(mypath)


如果 `f` 不是一个目录而是一个普通的数据文件,我会用 `File.size` 来找到它的字节数,并将这个值赋给变量 `filesize`:

filesize = File.size(mypath)


随着每个连续的文件 `f` 被代码块处理,其大小被计算,并将这个值添加到变量 `totalbytes`:

totalbytes += filesize


当当前目录中的所有文件都已传递到块中时,`totalbytes` 将等于目录中所有文件的总大小。

然而,我还需要计算所有子目录中的字节数。因为方法是递归的,所以这会自动完成。记住,当 `processfiles` 方法中的花括号之间的代码确定当前文件 `f` 是一个目录时,它会将这个目录名传递回 *自身*——即 `processfiles` 方法。

让我们想象你首先用 *C:\test* 目录调用 `processfiles`。在某个时刻,变量 `f` 被分配了其子目录之一的名称,比如说,*C:\test\dir_a*。现在这个子目录被传递回 `processfiles`。在 *C:\test\dir_a* 中没有找到更多的目录,所以 `processfiles` 只计算这个子目录中所有文件的大小。当它完成这些文件的计算后,`processfiles` 方法结束并返回当前目录 `totalbytes` 中的字节数,这是最初调用该方法的代码部分:

return totalbytes


在这种情况下,这是 `processfiles` 方法内部递归调用 `processfiles` 方法的代码:

bytes_in_dir = processfiles(mypath)


因此,当 `processfiles` 完成处理子目录 `C:\test\dir_a` 中的文件时,它返回那里找到的所有文件的总大小,并将这个值赋给 `bytes_in_dir` 变量。`processfiles` 方法现在继续从它离开的地方(即,从它调用自身来处理子目录的点)处理原始目录 `C:\test` 中的文件。

无论这个方法遇到多少层子目录,它每次找到目录时都调用自身的事实确保了它自动遍历它找到的每个目录路径,计算每个目录的总字节数。

最后要注意的一点是,分配给在 `processfiles` 方法内部声明的变量的值,在每次递归级别完成时将恢复到它们的“之前”值。因此,`totalbytes` 变量最初将包含 *C:\test\test_a\test_b* 的大小,然后是 *C:\test\test_a* 的大小,最后是 *C:\test* 的大小。为了保持所有目录组合大小的累计总和,你需要将值分配给在方法外声明的变量。在这里,我使用全局变量 `$dirsize` 来实现这个目的,为每个处理的子目录添加 `totalbytes` 的值:

$dirsize += totalbytes


顺便提一下,尽管字节可能是一个方便的测量单位,用于非常小的文件,但通常最好用千字节来描述较大的文件,用兆字节来描述非常大的文件或目录。要将字节转换为千字节或千字节转换为兆字节,需要除以 1,024。要将字节转换为兆字节,需要除以 1,048,576。我程序中的最后一行代码执行这些计算,并使用 Ruby 的 `printf` 方法以格式化的字符串显示结果:

printf( "Size of this directory and subdirectories is
#{\(dirsize} bytes, #{\)dirsize/1024}K, %0.02fMB",
"#{$dirsize/1048576.0}" )


注意,我在第一个字符串中嵌入了格式化占位符 `"%0.02fMB"`,并且添加了一个逗号后的第二个字符串:`"#{$dirsize/1048576.0}"`。第二个字符串计算目录大小以兆字节为单位,然后这个值被替换为第一个字符串中的占位符。占位符的格式化选项 `"%0.02f"` 确保兆字节值以带有两位小数的浮点数 `"f"` 显示。

# 按大小排序

目前这个程序按字母顺序打印文件和目录的名称及其大小。但我更感兴趣的是它们的 *相对* 大小。因此,如果文件按大小而不是按名称排序,将更有用。

要能够对文件进行排序,你需要一种方式来存储所有文件大小的完整列表。一个明显的方法是将文件大小添加到一个数组中。在 *file_info2.rb* 中,我创建了一个空数组 `$files`,每次处理一个文件时,我都会将其大小追加到数组中:

*file_info2.rb*

$files << fsize


然后,我可以对文件大小进行排序,以显示从低到高的值,或者(通过排序然后反转数组)以显示从高到低的值:

\(files.sort # sort low to high \)files.sort.reverse # sort high to low


这个问题的唯一麻烦是,我现在得到了一个没有关联文件名的文件大小数组。一个更好的解决方案是使用哈希而不是数组。我在 *file_info3.rb* 中实现了这一点。首先,我创建了两个空哈希:

*file_info3.rb*

\(dirs = {} \)files = {}


现在,当 `processfiles` 方法遇到目录时,它会使用完整的目录路径 `mypath` 作为键,使用目录大小 `dsize` 作为值,在 `$dirs` 哈希中添加一个新的条目:

$dirs[mypath] = dsize


类似地,键值对被添加到 `$files` 哈希中。当子目录和文件的整个结构通过递归调用 `processfiles` 方法处理后,`$dirs` 哈希变量将包含目录名称和大小之间的键值对,而 `$files` 哈希将包含文件名称和大小之间的键值对。

现在剩下的只是对这些哈希进行排序并显示。对于哈希的标准 `sort` 方法是按照键排序,而不是值。我想按值(大小)排序,而不是按键(名称)。为此,我定义了一个自定义排序方法(参考第四章和第五章,了解如何使用 `<=>` 定义自定义比较):

$files.sort{|a,b| a[1]<=>b[1]}


在这里,`sort` 方法将 `$files` 哈希转换为嵌套的 `[key,value]` 对数组,并将其中两个,`a` 和 `b`,传递到花括号之间的块中。每个 `[key,value]` 对的第二个项目(索引 `[1]`)提供了值。排序本身是通过 Ruby 的 `<=>` 比较方法在值上进行的。最终结果是,这个程序现在首先显示一个按大小升序排列的文件列表,然后是一个类似排序的目录列表。这是其输出的一个示例:

..\ch19\blog\app\models\post.rb : 36 bytes
..\ch19\say_hello.html.erb : 41 bytes
..\ch13\testfile.txt : 57 bytes
..\ch01\2helloname.rb : 67 bytes
..\ch9\div_by_zero.rb : 71 bytes
..\ch12\test.rb : 79 bytes
..\ch4\dir_array.rb : 81 bytes
..\ch3\for_to.rb : 89 bytes


深入挖掘

递归是一种重要的编程技术,然而,它可能相当难以理解。在这里,我将一步一步地解释递归。

简化递归

如果你以前从未使用过递归,本章中递归的“目录遍历”方法可能需要一些解释。为了阐明递归是如何工作的,让我们看看一个更简单的例子。加载 *recursion.rb* 程序:

*recursion.rb*

$outercount = 0

def addup( aNum )
aNum += 1
$outercount +=1
puts( "aNum is #{aNum}, \(outercount is #{\)outercount}" )
if \(outercount < 3 then addup( aNum ) #<= recursive call to addup method end puts("At END: aNum is #{aNum},outercount is #{\)outercount}")
end

addup( 0 ) #<= This is where it all begins


这包含了一个递归方法 `addup`,它一生的唯一目的就是从 1 数到 3。`addup` 方法接收一个整数作为输入参数,`aNum`。

addup( aNum )


此外,还有一个全局变量 `$outercount`,它“存在于”`addup` 方法之外。每当 `addup` 方法执行时,`aNum` 增加 1,`$outercount` 也增加 1。只要 `$outercount` 小于 3,`addup` 方法内部的代码就会再次调用相同的方法(`addup`),并将新的 `aNum` 值传递给它:

if $outercount < 3 then
addup( aNum )
end


让我们跟随这个过程。这个过程是从调用 `addup` 并传递值 0 开始的:

addup( 0 )


`addup` 方法将 1 添加到 `aNum` 和 `$outercount`,因此这两个变量现在都有值 1。测试(`$outercount < 3`)评估为真,所以 `aNum` 作为参数传递给 `addup`。再次,两个变量都增加了 1,所以 `aNum` 现在是 2,而 `$outercount` 也是 2。现在再次将 `aNum` 传递给 `addup`。再次将 1 添加到两个变量中,使每个变量都变为 3。然而,这次测试条件失败,因为 `$outercount` 不再小于 3。因此,调用 `addup` 的代码被跳过,你到达了方法中的最后一行:

puts( "At END: aNum is #{aNum}, outercount is #{$outercount}" )


这将打印出`aNum`和`$outercount`的值,正如你所预期的那样,这两个值都是 3。到达这个方法的末尾后,“控制流”会返回到最初调用该方法的代码之后的下一行。在这里,调用`addup`方法的代码恰好位于方法内部。这里是它:

addup( aNum )


接下来的第一个可执行行(再次)是方法的最后一行,它打印出两个变量的值:

puts( "At END: aNum is #{aNum}, outercount is #{$outercount}" )


因此,你回到了一个更早的“执行点”——你递归调用`addup`方法的那一刻。当时,`aNum`的值是 2,现在它仍然是这个值。如果这看起来很困惑,只需尝试想象如果`aNum`的值是 2,然后你调用其他一些无关的方法会发生什么。从那个其他方法返回时,`aNum`当然仍然会有 2 的值。这就是这里发生的一切。唯一的区别是,这个方法恰好调用的是它自己而不是其他方法。

再次,方法执行结束,控制权返回到调用该方法的代码之后的下一个可执行行,`aNum`的值又向前迈出一步回到其历史中——现在它的值是 1。然而,`$outercount`变量是存在于方法之外的,并且不受递归的影响,所以它仍然是 3。

如果你能够访问一个可视化的调试器,整个过程将变得更加清晰:你可以在第 9 行(`if $outercount < 3 then`)设置一个断点,将`aNum`和`$outercount`添加到监视窗口中,并在触碰到断点后反复进入代码。

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860162.png)

这张截图显示了在 IDE Ruby In Steel 中可视化的递归程序调试。我可以逐行通过源代码,使用调用栈来跟踪当前的“递归级别”(`addup`方法被调用的次数),并使用监视窗口来监控变量的当前值。


# 第十四章。YAML

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860138.png.jpg)

在某个时候,大多数桌面应用程序都希望将结构化数据保存到磁盘并从中读取。你已经看到了如何使用简单的 IO 例程,如 `gets` 和 `puts` 来读取和写入数据。但如果你要保存和恢复来自混合对象类型列表的数据,你会怎么做?使用 Ruby,一种简单的方法是使用 YAML。

### 注意

YAML 是一个首字母缩略词,有人认为它代表“另一种标记语言”(Yet Another Markup Language),或者(递归地)代表“YAML 不是标记语言”(YAML Ain’t Markup Language)。

# 转换为 YAML

YAML 定义了一种序列化(数据保存)格式,以人类可读的文本形式存储信息。YAML 可以与各种编程语言一起使用;为了在 Ruby 中使用它,你的代码需要访问 Ruby 的 *yaml.rb* 文件中的例程。通常,这会在代码单元的顶部通过以下方式加载或“要求”文件:

require "yaml"


完成这一步后,你将能够访问各种将 Ruby 对象转换为 YAML 格式的方法,以便将它们的数据写入文件。随后,你将能够读取这些保存的数据,并使用它来重建 Ruby 对象。要将对象转换为 YAML 格式,你可以使用 `to_yaml` 方法。这将转换标准对象类型,如字符串、整数、数组、哈希等。例如,这是转换字符串的方法:

*to_yaml1.rb*

"hello world".to_yaml


这就是转换数组的方法:

["a1", "a2" ].to_yaml


这是数组转换的结果 YAML 格式,其中每个键值对都放置在新的一行上:


  • a1
  • a2

注意定义 YAML “文档”开始的三条横线和定义列表中每个新元素的单条横线。在 YAML 术语中,文档不是磁盘上的单独文件,而是单独的 YAML 定义;一个磁盘文件可能包含多个 YAML 文档。有关 YAML 格式的更多信息,请参阅 深入挖掘 中的 深入挖掘。

你还可以将非标准类型的对象转换为 YAML。例如,假设你创建了这个类和对象:

*to_yaml2.rb*

class MyClass
def initialize( anInt, aString )
@myint = anInt
@mystring =aString
end
end

ob1 = MyClass.new( 100, "hello world" ).to_yaml


该对象的 YAML 表示形式将先于文本 `!ruby/object:`,然后是类名,变量名后跟冒号(但去掉 `@`),以及它们的值,每行一个:

--- !ruby/object:MyClass
myint: 100
mystring: hello world


如果你想要打印出对象的 YAML 表示形式,你可以使用方法 `y()`,这是 YAML 的一个类似于熟悉 `p()` 方法的等效方法,用于检查和打印正常的 Ruby 对象:

*yaml_test1.rb*

y( ['Bert', 'Fred', 'Mary'] )


这将显示以下内容:


  • Bert
  • Fred
  • Mary

你可以类似地显示一个哈希:

y({ 'fruit' => 'banana', :vegetable => 'cabbage', 'number' => 3 })


在这种情况下,每个键值对都放置在新的一行上:


fruit: banana
:vegetable: cabbage
number: 3


### 注意

哈希元素的顺序可能因你使用的 Ruby 版本而异(见第四章)。在处理哈希时,最好假设没有内在的顺序。

或者,你可以显示你自己的“自定义”对象:

t = Treasure.new( 'magic lamp', 500 )
y( t )


这显示了数据格式,就像在之前的示例中我使用`to_yaml`时一样,类名在顶部,变量名和值成对地出现在连续的行上。这是包含实例变量`@name`和`@value`的 Treasure 对象的 YAML 表示:

--- !ruby/object:Treasure
name: magic lamp
value: 500


你甚至可以使用`y()`来显示嵌套数组等相当复杂的对象:

*yaml_test2.rb*

arr1 = [ ["The Groovesters", "Groovy Tunes", 12 ],
[ "Dolly Parton", "Greatest Hits", 38 ]
]
y( arr1 )


这是`arr1`的 YAML 表示:


    • The Groovesters
    • Groovy Tunes
    • 12
    • Dolly Parton
    • Greatest Hits
    • 38

这里是另一个包含用户定义类型对象的数组的示例:

class CD
def initialize( anArtist, aName, theNumTracks )
@artist = anArtist
@name = aName
@numtracks = theNumTracks
end
end

arr2 = [CD.new("The Beasts", "Beastly Tunes", 22),
CD.new("The Strolling Bones","Songs For Senior Citizens",38)
]

y( arr2 )


这将输出以下 YAML:


  • !ruby/object:CD
    artist: The Beasts
    name: Beastly Tunes
    numtracks: 22
  • !ruby/object:CD
    artist: The Strolling Bones
    name: Songs For Senior Citizens
    numtracks: 38

# 嵌套序列

当相关的数据序列(如数组)嵌套在其他数据序列内部时,这种关系通过缩进来表示。例如,假设你在 Ruby 中声明了这个数组:

*nested_arrays.rb*

arr = [1,[2,3,[4,5,6,[7,8,9,10],"end3"],"end2"],"end1"]


当以 YAML 格式呈现(例如,通过`y(arr)`)时,它将如下所示:


  • 1
    • 2
    • 3
      • 4
      • 5
      • 6
        • 7
        • 8
        • 9
        • 10
      • end3
    • end2
  • end1

# 保存 YAML 数据

将你的 Ruby 对象转换为 YAML 格式的另一种方便的方法是由`dump`方法提供的。在最简单的情况下,它将你的 Ruby 数据转换为 YAML 格式并将其“写入”字符串:

*yaml_dump1.rb*

arr = ["fred", "bert", "mary"]
yaml_arr = YAML.dump( arr )
# yaml_arr is now: "--- \n- fred\n- bert\n- mary\n"


更有用的是,`dump`方法可以接受第二个参数,这是一种 IO 对象,通常是文件。你可以打开一个文件并将数据写入它:

*yaml_dump2.rb*

f = File.open( 'friends.yml', 'w' )
YAML.dump( ["fred", "bert", "mary"], f )
f.close


或者你可以打开文件(或某种其他类型的 IO 对象)并将其传递给一个关联的块:

File.open( 'morefriends.yml', 'w' ){ |friendsfile|
YAML.dump( ["sally", "agnes", "john" ], friendsfile )
}


在每种情况下,数组的 YAML 表示都将作为纯文本保存到指定的文件中。例如,当之前的代码执行时,它将以下文本写入*morefriends.yml*文件:

*morefriends.yml*


  • sally
  • agnes
  • john

如果你使用一个块,文件将在退出块时自动关闭;否则,你应该显式地使用`close`方法关闭文件。你也可以以类似的方式使用块来打开文件并读取 YAML 数据:

File.open( 'morefriends.yml' ){ |f|
$arr= YAML.load(f)
}


假设*morefriends.yml*包含之前保存的数据,一旦它被加载并分配给之前显示的块中的全局变量`$arr`,`$arr`将包含这个字符串数组:

["sally", "agnes", "john"]


# 保存时省略变量

如果出于某种原因,你希望在序列化对象时省略一些实例变量,你可以通过定义一个名为`to_yaml_properties`的方法来实现。在这个方法的主体中,放置一个字符串数组。每个字符串都应该匹配要保存的实例变量的名称。任何未指定的变量将不会被保存。看看这个例子:

*limit_y.rb*

class Yclass
def initialize(aNum, aStr, anArray)
@num = aNum
@str = aStr
@arr = anArray
end

def to_yaml_properties
    ["@num", "@arr"]     #<= @str will not be saved!
end

end


在这里,`to_yaml_properties`限制了在调用`YAML.dump`时将保存的变量,即`@num`和`@arr`。字符串变量`@str`将不会被保存。如果你想根据保存的 YAML 数据重建对象,你有责任确保任何“缺失”的变量要么不是必需的(在这种情况下可以忽略),要么如果它们是必需的,它们应该被初始化为一些有意义的值。

ob = Yclass.new( 100, "fred", [1,2,3] )
# ...creates object with @num=100, @str="fred", @arr=[1,2,3]

yaml_ob = YAML.dump( ob )
#...dumps to YAML only the @num and @arr data (omits @str)

ob2 = YAML.load( yaml_ob )
#...creates ob2 from dumped data with @num=100, @arr=[1,2,3]
# but without @str


# 多文档,一个文件

之前我提到,三个短横线用于标记一个名为*document*的新 YAML 部分的开始。例如,假设你想要将两个数组`arr1`和`arr2`保存到一个文件中,*multidoc.yml*。在这里,`arr1`是一个包含两个嵌套数组的数组,而`arr2`是一个包含两个 CD 对象的数组:

*multi_docs.rb*

arr1 = [ ["The Groovesters", "Groovy Tunes", 12 ],
[ "Dolly Parton", "Greatest Hits", 38 ]
]

arr2 = [ CD.new("Gribbit Mcluskey", "Fab Songs", 22),
CD.new("Wayne Snodgrass", "Singalong-a-Snodgrass", 24)
]


这是我的将数组转换为 YAML 并写入文件的常规操作(如第十三章 Files and IO 中所述),`'w'`参数导致文件以写入模式打开):

File.open( 'multidoc.yml', 'w' ){ |f|
YAML.dump( arr1, f )
YAML.dump( arr2, f )
}


如果你现在查看*multidoc.yml*文件,你会看到数据已经被保存为两个独立的“文档”,每个文档都以三个短横线开始:


    • The Groovesters
    • Groovy Tunes
    • 12
    • Dolly Parton
    • Greatest Hits
    • 38

  • !ruby/object:CD
    artist: Gribbit Mcluskey
    name: Fab Songs
    numtracks: 22
  • !ruby/object:CD
    artist: Wayne Snodgrass
    name: Singalong-a-Snodgrass
    numtracks: 24

现在,我需要找到一种方法通过读取两个文档来重建这些数组。这就是`load_documents`方法发挥作用的地方。`load_documents`方法调用一个块,并将每个连续的文档传递给它。以下是如何使用此方法从两个 YAML 文档中重建两个数组(放置在另一个数组`$new_arr`中)的示例:

File.open( 'multidoc.yml' ) {|f|
YAML.load_documents( f ) { |doc|
$new_arr << doc
}
}


你可以通过执行以下操作来验证`$new_arr`是否已初始化为两个数组:

p( $new_arr )


这显示了包含加载数据的两个嵌套数组的数组:

[[["The Groovesters", "Groovy Tunes", 12], ["Dolly Parton",
"Greatest Hits", 38]], [#<CD:0x2c30e98 @artist="Gribbit Mcluskey", @name="Fab
Songs", @numtracks=22>, #<CD:0x2c30ad8 @artist="Wayne Snodgrass",
@name="Singalong-a-Snodgrass", @numtracks=24>]]


因为这有点难以管理,你可能更喜欢使用外数组的索引来单独显示每个嵌套数组:

p( $new_arr[0] )
p( $new_arr[1] )


之前的假设是你事先知道可用的嵌套数组的数量。作为替代,这里有一个更通用的方法来做同样的事情,使用`each`方法将所有可用的项目传递到一个块中;这适用于任何数量的数组:

$new_arr.each{ |arr| p( arr ) }


# YAML 数据库

为了查看一个稍微复杂一点的示例,该示例以 YAML 格式保存和加载数据,请查看*cd_db.rb*示例程序。它实现了一个简单的 CD 数据库。它定义了三种类型的 CD 对象:一个包含名称、艺术家和曲目数量的基本`CD`;以及两个更专业的派生对象,`PopCD`,它添加了关于流派(例如,摇滚或乡村)的数据,以及`ClassicalCD`,它添加了关于指挥家和作曲家的数据:

*cd_db.rb*

class CD
def initialize( arr )
@name = arr[0]
@artist = arr[1]
@numtracks = arr[2]
end

def getdetails
    return[@name, @artist, @numtracks]
end

end

class PopCD < CD

def initialize( arr )
    super( arr  )
    @genre = arr[3]
end

def getdetails
    return( super << @genre )
end

end

class ClassicalCD < CD
def initialize( arr )
super( arr )
@conductor = arr[3]
@composer = arr[4]
end

def getdetails
    return( super << @conductor << @composer )
end

end


当程序运行时,用户可以输入数据以创建任何这三种类型的新的 CD 对象。还有一个选项可以将数据保存到磁盘上。当应用程序随后再次运行时,现有的数据将被重新加载。

数据本身在代码中组织得非常简单(甚至可以说是简单的),每个对象的数据在创建对象之前被读入一个数组。整个 CD 对象数据库被保存到全局变量`$cd_arr`中,并使用 YAML 方法写入磁盘并重新加载到内存中:

def saveDB
File.open( \(fn, 'w' ) { |f| f.write(\)cd_arr.to_yaml)
}
end

def loadDB
input_data = File.read( $fn )
$cd_arr = YAML::load( input_data )
end


请记住,这个程序是为了简单而不是美观而编写的。在现实世界的应用中,你肯定希望创建一些更优雅的数据结构来管理你的 Dolly Parton 收藏!

# YAML 冒险之旅

作为使用 YAML 的最后一个示例,我提供了一个冒险游戏的基本框架(*gamesave_y.rb*)。这创建了一些宝藏对象和一些房间对象。宝藏对象被“放入”房间对象中(即,它们被放置在房间包含的数组中),然后房间对象被放入地图对象中。这相当于构建了一个中等复杂的数据结构,其中一个类型的对象(地图)包含任意数量的另一个类型的对象(房间),每个房间可能包含零个或多个其他类型的对象(宝藏)。

初看之下,找到一种方法将整个混合对象类型的网络存储到磁盘上并在以后阶段重建这个网络可能看起来像是一个编程噩梦。实际上,多亏了 Ruby YAML 库提供的序列化功能,保存和恢复这些数据几乎可以轻松完成。这是因为序列化可以免除你逐个保存每个对象的麻烦。相反,你只需要“转储”顶层对象;在这里,即地图对象,`mymap`。

完成此操作后,顶层对象“包含”的任何对象(如房间)或包含的对象本身所包含的任何对象(如宝藏)都将自动为你保存。然后,只需通过一次性加载所有保存的数据并将其分配给“顶层”对象(在这里是地图)即可重新构建它们:

*gamesave_y.rb*

Save mymap

File.open( 'game.yml', 'w' ){ |f|
YAML.dump( mymap, f )
}

Reload mymap

File.open( 'game.yml' ){ |f|
mymap = YAML.load(f)
}


这个程序的完整代码太长,无法在此展示,所以我建议你尝试源代码存档中提供的程序,以欣赏使用 YAML 保存和加载相当复杂的数据结构是多么简单。

深入挖掘

本节总结了 YAML 数据文件的结构,并解释了如何在 YAML 格式中保存嵌套哈希。

YAML 指南简述

如我之前提到的,YAML 以称为 *documents* 的文本块的形式存储信息,这些文档包含数据的 *sequences*。每个文档以三个连字符(`---`)开始,列表中的每个单独元素都以单个连字符(`-`)字符开始。例如,以下是一个包含一个文档和两个列表项的 YAML 数据文件:


  • artist: The Groovesters
    name: Groovy Tunes
    numtracks: 12
  • artist: Dolly Parton
    name: Greatest Hits
    numtracks: 38

在前面的示例中,你可以看到每个列表项由两部分组成:一个名称,如 `artist:`(在列表项中相同),以及其右侧的数据,如 `Dolly Parton`,这可能因列表项而异。这些项类似于 Ruby 哈希中的键值对。YAML 将键值列表称为 *maps*。

以下是一个包含两个项目的 YAML 文档,每个项目包含三个项目;换句话说,它是包含两个三项目“嵌套”数组的数组的 YAML 表示:


    • The Groovesters
    • Groovy Tunes
    • 12
    • Dolly Parton
    • Greatest Hits
    • 38

现在我们来看看 YAML 如何处理嵌套哈希。考虑这个哈希:

*hash_to_yaml.rb*

hsh = { :friend1 => 'mary',
:friend2 => 'sally',
:friend3 => 'gary',
:morefriends => { :chap_i_met_in_a_bar => 'simon',
:girl_next_door => 'wanda'
}
}


正如您已经看到的,在 YAML 中,哈希自然地表示为键值对列表。然而,在前面展示的示例中,键 `:morefriends` 与其值关联的是一个嵌套的哈希。YAML 是如何表示这个嵌套哈希的呢?结果是,与数组(参见 嵌套序列)一样,它只是简单地缩进嵌套的哈希:

:friend1: mary
:friend2: sally
:friend3: gary
:morefriends:
:chap_i_met_in_a_bar: simon
:girl_next_door: wanda


### 注意

如需深入了解 YAML,请参阅 [`www.yaml.org/`](http://www.yaml.org/)。

Ruby 伴随的 YAML 库相当庞大且复杂,其中包含的方法比本章描述的要多得多。然而,您现在应该对 YAML 有足够的了解,可以在自己的程序中有效地使用它。您可以在空闲时间探索 YAML 库的外围。不过,结果却是 YAML 并非在 Ruby 中序列化数据的唯一方式。您将在下一章中看到另一种方法。


# 第十五章:Marshal

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860138.png.jpg)

Ruby 的 Marshal 库提供了一种保存和加载数据的替代方法。它有一组类似 YAML 的方法,使你能够将数据保存到磁盘并从磁盘加载数据。

# 保存和加载数据

将以下程序与上一章的 *yaml_dump2.rb* 进行比较:

*marshal1.rb*

f = File.open( 'friends.sav', 'w' )
Marshal.dump( ["fred", "bert", "mary"], f )
f.close

File.open( 'morefriends.sav', 'w' ){ |friendsfile|
Marshal.dump( ["sally", "agnes", "john" ], friendsfile )
}

File.open( 'morefriends.sav' ){ |f|
$arr= Marshal.load(f)
}
myfriends = Marshal.load(File.open( 'friends.sav' ))
morefriends = Marshal.load(File.open( 'morefriends.sav' ))

p( myfriends ) #=> ["fred", "bert", "mary"]
p( morefriends ) #=> ["sally", "agnes", "john"]
p( $arr ) #=> ["sally", "agnes", "john"]


这两个程序几乎完全相同,只是将每个 `YAML`(如 `YAML.dump` 和 `YAML.load`)的出现都替换成了 `Marshal`。此外,Marshal 是 Ruby 的标准库之一,所以你不需要 `require` 任何额外的文件来使用它。

如果你查看生成的数据文件(例如 *friends.sav*),你会立即看到有一个主要区别。然而,YAML 文件是纯文本格式,而 Marshal 文件是二进制格式。所以尽管你可能能够读取 *一些* 字符,例如字符串中的字符,但你无法简单地加载保存的数据并在文本编辑器中修改它。

与 YAML 类似,大多数数据结构可以通过简单地序列化顶层对象并在需要重建其下所有对象时加载它来自动序列化使用 Marshal。例如,看看我的小型冒险游戏程序。在前一章中,我解释了如何通过序列化和加载 Map 对象 `mymap`(见 Adventures in YAML 中的 *gamesave_y.rb*)来保存和恢复包含宝物的房间。你可以用 Marshal 而不是 YAML 来做同样的事情:

*gamesave_m.rb*

File.open( 'game.sav', 'w' ){ |f|
Marshal.dump( mymap, f ) # save data to file
}

File.open( 'game.sav' ){ |f|
mymap = Marshal.load(f) # reload saved data from file
}


在一些特殊情况下,对象不能如此轻易地序列化。这些异常在 Ruby 的 `Marshal` 模块(*marshal.c*)的代码中有记录,它指出,“如果待序列化的对象包括绑定、过程或方法对象、IO 类的实例或单例对象,将引发 TypeError。” 我将在讨论如何使用序列化保存单例对象时展示一个例子。

# 保存时省略变量

与 YAML 序列化一样,使用 Marshal 序列化时可以限制要保存的变量。在 YAML 中,你通过编写一个名为 `to_yaml_properties` 的方法来完成此操作。对于 Marshal,你需要编写一个名为 `marshal_dump` 的方法。在这个方法的代码中,你应该创建一个包含要保存的 *实际变量* 的数组(在 YAML 中,你创建了一个包含变量 *名称* 的字符串数组的数组)。

这是一个示例:

def marshal_dump
[@variable_a, @variable_b]
end


另一个区别是,使用 YAML,你可以简单地加载数据以重新创建对象。而使用 Marshal,你需要添加一个名为 `marshal_load` 的特殊方法,将任何加载的数据作为参数传递给它。当你调用 `Marshal.load` 时,它将自动被调用,并将数据以数组的形式传递。之前保存的对象可以从这个数组中解析出来。你还可以为在保存数据时省略的任何变量(例如这里的 `@some_other_variable`)分配值:

def marshal_load(data)
@variable_a = data[0]
@variable_b = data[1]
@some_other_variable = "a default value"
end


这是一个完整的程序,它保存和恢复了 `@num` 和 `@arr` 变量,但省略了 `@str`:

*limit_m.rb*

class Mclass
def initialize(aNum, aStr, anArray)
@num = aNum
@str = aStr
@arr = anArray
end

def marshal_dump
    [@num, @arr]
end

def marshal_load(data)
    @num = data[0]
    @arr = data[1]
    @str = "default"

end
end

ob = Mclass.new( 100, "fred", [1,2,3] )
p( ob )

=> #<Mclass:0x2be7278 @num=100, @str="fred", @arr=[1, 2, 3]>

marshal_data = Marshal.dump( ob )
ob2 = Marshal.load( marshal_data )
p( ob2 )

=> #<Mclass:0x2be70e0 @num=100, @str="default", @arr=[1, 2, 3]>


注意,尽管这里的序列化是在内存中完成的,但相同的技巧也可以用于使用 Marshal 将对象保存到磁盘和从磁盘加载。

# 保存单例

让我们看看之前提到的一个具体问题,即无法使用 marshaling 来保存和加载单例。在 *singleton_m.rb* 中,我创建了一个 Object 实例,名为 `ob`,然后以单例类的形式扩展它,并给它添加了一个额外的 `xxx` 方法:

*singleton_m.rb*

ob = Object.new

class << ob
def xxx( aStr )
@x = aStr
end
end


当我尝试使用 `Marshal.dump` 将这些数据保存到磁盘时,Ruby 显示了一个错误信息:“singleton can’t be dumped (TypeError)。”

## YAML 和 单例

在考虑如何处理这个问题之前,让我们简要地看看 YAML 在这种情况下会如何应对。程序 *singleton_y.rb* 尝试使用 `YAML.dump` 保存我刚才创建的单例,与 `Marshal.dump` 不同,它成功了——好吧,算是吧:

*singleton_y.rb*

YAML version of singleton-save

ob.xxx( "hello world" )

File.open( 'test.yml', 'w' ){ |f|
YAML.dump( ob, f )
}

ob.xxx( "new string" )

File.open( 'test.yml' ){ |f|
ob = YAML.load(f)
}


如果你查看保存的 YAML 文件,名为 *test.yml*,你会发现它定义了一个普通的 Object 实例,并附加了一个名为 `x` 的变量,该变量的字符串值为 `hello world`:

--- !ruby/object
x: hello world


这一切看起来都很好。然而,当你通过加载保存的数据重新构建对象时,新的 `ob` 将是一个标准的 Object 实例,它恰好包含一个额外的实例变量,`@x`。由于它不再是原始的单例,这个 `ob` 将无法访问在该单例中定义的任何方法(这里是指 `xxx` 方法)。因此,尽管 YAML 序列化在保存和加载数据项方面对在单例中创建的数据项更为宽容,但它不会在重新加载保存的数据时自动重新创建单例。

## Marshal 和 单例

现在,让我们回到这个程序的 Marshal 版本。我需要做的第一件事是找到至少保存和加载数据项的方法。一旦我做到了这一点,我就会尝试找出如何在重新加载时重建单例。

要保存特定的数据项,我可以定义前面解释过的`marshal_dump`和`marshal_load`方法(参见*limit_m.rb*)。这些方法通常应该定义在单例派生的类中,而不是单例本身。这是因为,正如前面解释的,当数据被保存时,它将以单例派生类的表示形式存储。这意味着尽管你确实可以向从类 X 派生的单例添加`marshal_dump`,但在重构对象时,你将加载一个通用类型 X 的对象的数据,而不是特定单例实例的数据。

此代码创建了一个类 X 的单例`ob`,保存其数据,然后重新创建了一个类 X 的通用对象:

*singleton_m2.rb*

class X
def marshal_dump
[@x]
end

def marshal_load(data)
    @x = data[0]
end

end

ob = X.new

class << ob
def xxx( aStr )
@x = aStr
end
end

ob.xxx( "hello" )
p( ob )

File.open( 'test2.sav', 'w' ){ |f|
Marshal.dump( ob, f )
}

ob.xxx( "new string" )
p( ob )
File.open( 'test2.sav' ){ |f|
ob = Marshal.load(f)
}

p( ob )


此处使用的代码使用`Marshal.dump`保存类 X 的对象`ob`,然后调用单例方法`xxx`在重新加载保存的数据之前将不同的字符串分配给`@x`变量,然后使用`Marshal.load`重新加载保存的数据,并使用这些数据重新创建对象。使用`p()`显示`ob`的内容,在保存之前,然后在新字符串分配给它之后,最后在重新加载时再次显示。这让你可以验证在重新加载的对象重构时`@x`被分配了保存时的值:

<X:0x2b86cc0 @x="hello"> # value when saved

<X:0x2b86cc0 @x="new string"> # new value then assigned

<X:0x2b869f0 @x="hello"> # value after saved data loaded


在包含的数据方面,保存的对象和重新加载的对象是相同的。然而,重新加载的对象对单例类一无所知。单例类中包含的`xxx`方法并不构成重构对象的一部分。因此,以下操作将失败:

ob.xxx( "this fails" )


此代码的 Marshal 版本与前面给出的 YAML 版本等效。它正确地保存和恢复了数据,但它不会重构单例。那么,如何从保存的数据中重构单例呢?无疑,有许多巧妙和微妙的方法可以实现这一点。然而,我将选择一个非常简单的技术:

*singleton_m3.rb*

FILENAME = 'test2.sav'

class X
def marshal_dump
[@x]
end

def marshal_load(data)
    @x = data[0]
end

end

ob = X.new

a) if File exists, load data into ob - a generic X object

if File.exists?(FILENAME) then
File.open(FILENAME){ |f|
ob = Marshal.load(f)
}
else
puts( "Saved data can't be found" )
end

b) Now transform ob in a singleton

class << ob
def xxx=( aStr )
@x = aStr
end

def xxx
    return @x
end

end


此代码首先检查是否可以找到包含保存数据的文件。(这个示例被故意保持简单——在实际应用中,你当然需要编写一些异常处理代码来处理读取无效数据的可能性。)如果找到文件,数据将被加载到一个通用类型`X`的对象中。

只有完成此操作后,此对象才会以通常的方式“转换”为单例。换句话说,对象被加载,然后执行以`class << ob`开始的代码(简单地因为单例创建代码在加载代码之后,所以由 Ruby 解释器按顺序执行)。这为对象提供了额外的`xxx`单例方法。然后你可以将新数据保存回磁盘,并在稍后阶段按照前面解释的方法重新加载和重新创建修改后的单例:

if ob.xxx == "hello" then
ob.xxx = "goodbye"
else
ob.xxx = "hello"
end

File.open( FILENAME, 'w' ){ |f|
Marshal.dump( ob, f )
}


如果你想在真实的应用程序中保存和加载单例,单例“重建”代码自然可以有自己的方法,这样你就不必像上一个例子那样依赖于它在代码中的位置。

*singleton_m4.rb*

def makeIntoSingleton( someOb )
class << someOb
def xxx=( aStr )
@x = aStr
end

  def xxx
     return @x
  end

end
return someOb
end


深入挖掘

如果你尝试加载使用不同版本的 Marshal 库保存的数据,可能会遇到问题。在这里,你将学习如何验证 Marshal 的版本。

Marshal 版本号

Marshal 库的嵌入式文档(一个名为 *marshal.c* 的 C 语言文件)声明如下:“序列化的数据存储了与对象信息一起的主版本号和次版本号。在正常使用中,序列化只能加载与相同主版本号和相同或更低次版本号的数据。”

这明显提出了一个潜在问题,即序列化创建的数据文件格式可能与当前的 Ruby 应用程序不兼容。顺便提一下,Marshal 版本号不依赖于 Ruby 版本号,因此仅基于 Ruby 版本做出兼容性的假设是不安全的。

这种不兼容的可能性意味着在尝试加载之前,你应该始终检查保存数据的版本号。但你怎么获取版本号呢?再次,嵌入式文档提供了线索。它声明,“你可以通过读取序列化数据的前两个字节来提取版本号。”

Ruby 1.8 提供了以下示例:

str = Marshal.dump("thing")
RUBY_VERSION #=> "1.8.0"
str[0] #=> 4
str[1] #=> 8


好的,那么让我们尝试在一个完整的代码块中实现这个功能。下面是代码:

*version_m.rb*

x = Marshal.dump( "hello world" )
print( "Marshal version: #{x[0]}:#{x[1]}\n" )


在之前的代码中,`x` 是一个字符串,其前两个字节是主版本号和次版本号。在 Ruby 1.8 中,它会输出以下内容:

Marshal version: 4:8


然而,在 Ruby 1.9 中,没有显示任何数字。这是因为 Ruby 1.8 中前两个字节作为整数返回,但在 Ruby 1.9 中作为字符串返回。这些字符串不一定可打印。你可以通过使用 `p()` 方法来显示数组 `x` 的索引 0 和索引 1 的元素来简单地看到这一点:

p( x[0] ) #=> 4 (Ruby 1.8) "\x04" (Ruby 1.9)
p( x[1] ) #=> 8 (Ruby 1.8) "\b" (Ruby 1.9)


Ruby 1.9 返回的字符串可以显示为十六进制值或转义字符。在这里,你可以看到,对于 Marshal 版本 4.8,第一个值是 \x04,这是 4 的十六进制表示,而第二个值是 \b,它是退格符的转义字符,恰好具有 ASCII 值 8。可以使用 `ord` 方法将字符串转换为整数。这是 Ruby 1.9 的版本:

print( "Marshal version: #{x[0].ord}:#{x[1].ord}\n" )


现在正确地显示了版本号:`4:8`。当然,如果你使用的是 Marshal 库的不同版本,显示的数字将不同。Marshal 库还声明了两个常量,`MAJOR_VERSION` 和 `MINOR_VERSION`,它们存储当前正在使用的 Marshal 库的版本号。所以,乍一看,似乎应该很容易比较保存数据的版本号和当前版本号。

只有一个问题:当你将数据保存到磁盘上的文件时,`dump` 方法接受一个 IO 或 File 对象,并返回一个 IO(或 File)对象,而不是一个字符串:

*version_error.rb*

f = File.open( 'friends.sav', 'w' )
x = Marshal.dump( ["fred", "bert", "mary"], f )
f.close #=> x is now: #<File:friends.sav (closed)>


如果你现在尝试获取 `x[0]` 和 `x[1]` 的值,你会收到一个错误信息:

p( x[0] )

=> Error: undefined method '[]' for #<File:friends.sav (closed)> (NoMethodError)


从文件中重新加载数据并没有什么指导意义:

File.open( 'friends.sav' ){ |f|
x = Marshal.load(f)
}

puts( x[0] )
puts( x[1] )


这里的两个 `puts` 语句并没有(正如我天真地希望的那样)打印出被序列化数据的版本号的主版本和次版本号;实际上,它们打印出的是名称“fred”和“bert”——也就是说,从数据文件 *friends.sav* 中加载到数组 `x` 的前两个元素。

那么,究竟如何从保存的数据中获取版本号呢?我必须承认,我不得不阅读 *marshal.c* 中的 C 代码(这不是我最喜欢的活动!)并检查保存文件中的十六进制数据来找出答案。结果证明,正如文档所说,“你可以通过读取序列化数据的前两个字节来提取版本号。”然而,这并不是自动完成的。你必须显式地读取这些数据,如下所示:

*version_m2.rb*

f = File.open('test2.sav')
if (RUBY_VERSION.to_f > 1.8) then
vMajor = f.getc().ord
vMinor = f.getc().ord
else
vMajor = f.getc()
vMinor = f.getc()
end
f.close


在这里,`getc` 方法从输入流中读取下一个 8 位字节。请注意,我再次编写了一个测试,使其与 Ruby 1.8 兼容,在 Ruby 1.8 中 `getc` 返回一个数值字符值,以及与 Ruby 1.9 兼容,在 Ruby 1.9 中 `getc` 返回一个必须使用 `ord` 转换为整数的单字符字符串。

我的示例项目 *version_m2.rb* 展示了一种简单的方法,用于比较保存数据的版本号与当前 Marshal 库的版本号,以便在尝试重新加载数据之前确定数据格式是否可能兼容。

if vMajor == Marshal::MAJOR_VERSION then
puts( "Major version number is compatible" )
if vMinor == Marshal::MINOR_VERSION then
puts( "Minor version number is compatible" )
elsif vMinor < Marshal::MINOR_VERSION then
puts( "Minor version is lower - old file format" )
else
puts( "Minor version is higher - newer file format" )
end
else
puts( "Major version number is incompatible" )
end



# 第十六章。正则表达式

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860138.png.jpg)

正则表达式为你提供了强大的方法来在文本中查找和修改模式——不仅是在命令提示符中可能输入的短文本片段,还包括在磁盘上的文件中可能找到的大量文本。

正则表达式采用与字符串进行比较的模式的形式。正则表达式还提供了修改字符串的手段,例如,你可能通过将它们转换为大写来更改特定字符,你可能将“Diamond”的每个出现替换为“Ruby”,或者你可能读取一个编程代码文件,提取所有注释,并输出一个包含所有注释但不含代码的新文档文件。你将很快了解到如何编写注释提取工具。不过,首先,让我们看看一些非常简单的正则表达式。

# 匹配

几乎最简单的正则表达式是想要在字符串中找到的一系列字符(例如“abc”)。要匹配“abc”,可以通过在两个正斜杠分隔符之间放置这些字母来创建正则表达式:`/abc/`。你可以使用`=˜`运算符方法来测试匹配,如下所示:

*regex0.rb*

p( /abc/ =˜ 'abc' ) #=> 0


如果匹配成功,将返回一个表示字符串中字符位置的整数。如果没有匹配成功,将返回`nil`。

p( /abc/ =˜ 'xyzabcxyzabc' ) #=> 3
p( /abc/ =˜ 'xycab' ) #=> nil


你也可以指定一个字符组,放在方括号内,在这种情况下,匹配将在字符串中的任何一个字符上完成。例如,第一个匹配是在“c”上完成的;然后返回该字符在字符串中的位置:

p( /[abc]/ =˜ 'xycba' ) #=> 2


虽然我在之前的例子中使用了正斜杠作为分隔符,但定义正则表达式还有其他方法:你可以创建一个新的以字符串初始化的 Regexp 对象,或者你可以在正则表达式前加上`%r`并使用自定义的分隔符——非字母数字字符,就像在字符串中使用的那样(参见第三章)。在下面的例子中,我使用了花括号作为分隔符:

*regex1.rb*

regex1 = Regexp.new('[1]\(') regex2 = /^[a-z]*\)/
regex3 = %r{[2]
$}


每个之前的例子都定义了一个正则表达式,用于匹配全小写字符串(我将在稍后解释表达式的细节)。这些表达式可以用来测试如下字符串:

def test( aStr, aRegEx )
if aRegEx =˜ aStr then
puts( "All lowercase" )
else
puts( "Not all lowercase" )
end
end

test( "hello", regex1 ) #=> matches: "All lowercase"
test( "hello", regex2 ) #=> matches: "All lowercase"
test( "Hello", regex3 ) #=> no match: "Not all lowercase"


要测试匹配,你可以使用`if`和`=˜`运算符:

if /def/ =˜ 'abcdef'


如果匹配成功(并返回一个整数),则前面的表达式计算结果为真;如果没有匹配成功(并返回`nil`),则计算结果为假:

*if_test.rb*

RegEx = /def/
Str1 = 'abcdef'
Str2 = 'ghijkl'

if RegEx =˜ Str1 then
puts( 'true' )
else
puts( 'false' )
end #=> displays: true

if RegEx =˜ Str2 then
puts( 'true' )
else
puts( 'false' )
end #=> displays: false


经常情况下,尝试从字符串的起始位置匹配某些表达式是有用的;你可以使用字符`^`后跟一个匹配项来指定这一点。也可能有用的是从字符串的末尾进行匹配;你使用字符`$`后跟一个匹配项来指定这一点。

*start_end1.rb*

puts( /^a/ =˜ 'abc' ) #=> 0
puts( /^b/ =˜ 'abc' ) #=> nil
puts( /c\(/ =˜ 'abc' ) #=> 2 puts( /b\)/ =˜ 'abc' ) #=> nil


### 注意

如前所述,当将 `nil` 值传递给 Ruby 1.9 中的 `print` 或 `puts` 时,不会显示任何内容。在 Ruby 1.8 中,会显示 `nil`。为了确保在 Ruby 1.9 中显示 `nil`,请使用 `p` 而不是 `puts`。

从字符串的开始或结束进行匹配,当它成为更复杂表达式的一部分时更有用。通常这样的表达式试图匹配零个或多个指定的模式实例。`*` 字符用于表示它后面的模式的零个或多个匹配。形式上,这被称为 *量词*。考虑以下示例:

*start_end2.rb*

p( /[3]*$/ =˜ 'well hello 123' )


在这里,正则表达式指定了方括号之间的字符范围。这个范围包括所有小写字母(a–z)、所有数字(0–9)以及空格字符(这是表达式中 `z` 和 `0` 之间的空格)。`^` 字符表示匹配必须从字符串的开头开始,范围后面的 `*` 字符表示必须匹配零个或多个该范围内的字符,而 `$` 字符表示匹配必须一直进行到字符串的末尾。换句话说,这个模式只会匹配从字符串开头到末尾包含小写字母、数字和空格的字符串:

puts( /[4]*\(/ =˜ 'well hello 123' ) # match at 0 puts( /^[a-z 0-9]*\)/ =˜ 'Well hello 123' ) # no match due to ^ and upcase W


实际上,这个模式也会匹配空字符串,因为 `*` 表示零个或多个匹配是可以接受的:

puts( /[5]*$/ =˜ '' ) # this matches!


如果你想要排除空字符串,请使用 `+`(以匹配 *一个或多个* 模式出现):

puts( /[6]+$/ =˜ '' ) # no match


尝试运行 *start_end2.rb* 以获取更多关于如何将 `^`、`$`、`*` 和 `+` 与范围结合以创建不同匹配模式的示例。

你可以使用这些技术来确定字符串的特定特征,例如,确定一个给定的字符串是大写、小写还是混合大小写:

*regex2.rb*

aStr = "HELLO WORLD"

case aStr
when /[7]*\(/ puts( "Lowercase" ) when /^[A-Z 0-9]*\)/
puts( "Uppercase" )
else
puts( "Mixed case\n" )
end


由于分配给 `aStr` 的字符串目前全部为大写,所以前面的代码显示“大写”字符串。但如果 `aStr` 被分配为 `hello world`,它将显示“小写”,如果 `aStr` 被分配为 `Hello World`,它将显示“混合大小写”。

经常使用正则表达式来处理磁盘上文件中的文本。例如,假设你想显示 Ruby 文件中的所有完整行注释,但省略所有代码和部分行注释。你可以通过尝试从每行的开头(`^`)匹配零个或多个空白字符(空白字符用 `\s` 表示)直到注释字符(`#`)来实现这一点。

*regex3a.rb*

displays all the full-line comments in a Ruby file

File.foreach( 'regex1.rb' ){ |line|
if line =˜ /^\s*#/ then
puts( line )
end
}


# 匹配组

你还可以使用正则表达式来匹配一个或多个子字符串。为此,你应该将正则表达式的一部分放在括号内。这里我有两个组(有时称为 *捕获*):第一个尝试匹配字符串“hi”,第二个尝试匹配以“h”开头后跟任意三个字符(点表示“匹配任意单个字符”,所以这里的三个点将匹配任意三个连续字符)并以“o”结尾的字符串:

*groups.rb*

/(hi).*(h...o)/ =˜ "The word 'hi' is short for 'hello'."


在正则表达式中评估组之后,将分配若干变量,其数量等于组的数量。这些变量采用以下形式:一个 `$` 符号后跟一个数字:`$1`、`$2`、`$3`,依此类推。执行前面的代码后,我可以这样访问变量 `$1` 和 `$2`:

print( $1, " ", $2, "\n" ) #=> hi hello


注意,如果整个正则表达式没有匹配,则不会初始化任何组变量。例如,如果字符串中包含“hi”但“hello”不包含,就会发生这种情况。两个组变量都将为 `nil`。

这里有一个例子,它返回三个组,由一对括号(`()`)表示,每个括号包含一个由点表示的单个字符:`(.)`。然后显示组 `$1` 和 `$3`:

/(.)(.)(.)/ =˜ "abcdef"
print( $1, " ", $3, "\n" ) #=> a c


这里是之前给出的注释匹配程序的新版本(*regex3a.rb*);现在它已经被修改为使用包含点后跟星号(`(.*)`)的组 `()` 的值,以返回正则表达式前一部分匹配的字符串(此处为 `^\s*#`)之后的所有字符(零个或多个)。这个新版本从指定的文件中读取文本,并匹配从当前行开始(`^`)到第一个出现井号(`#`)之前的零个或多个空白字符(`\s*`):

*regex3b.rb*

File.foreach( 'regex1.rb' ){ |line|
if line =˜ /^\s#(.)/ then
puts( $1 )
end
}


结果是,只有第一可打印字符是 `#` 的行才会匹配;`$1` 打印出这些行的文本,减去 `#` 字符本身。您很快就会看到,这种简单技术为从 Ruby 文件中提取文档提供了一个有用的工具的基础。

您不仅限于提取和显示字符;您还可以修改文本。这个例子显示了 Ruby 文件中的文本,但将所有 Ruby 行注释字符(`#`)更改为 C 风格的行注释(`//`):

*regex4.rb*

File.foreach( 'regex1.rb' ){ |line|
line = line.sub(/(^\s)#(.)/, '\1//\2')
puts( line )
}


在这个例子中,使用了 String 类的 `sub` 方法;它将正则表达式作为其第一个参数(`/(^\s*)#(.*)/`)和替换字符串作为第二个参数(`'\1//\2'`)。替换字符串可以包含编号占位符,如 `\1` 和 `\2`,以匹配正则表达式中的任何组——这里有两个括号之间的组:(`^\s*`)和(`(.*)`)。`sub` 方法返回一个新的字符串,其中正则表达式所做的匹配被替换到替换字符串中,而任何未匹配的元素(此处为 `#` 字符)被省略。例如,假设在输入文件中找到了以下注释:

aStr = "hello world"

aStr = "Hello World"


使用我们的正则表达式替换后,显示的输出如下:

// aStr = "hello world"
// aStr = "Hello World"


# MatchData

`=˜` 运算符不是找到匹配的唯一方法。Regexp 类还有一个 `match` 方法。这与 `=˜` 的工作方式类似,但在匹配成功时,它返回一个 MatchData 对象而不是一个整数。MatchData 对象包含模式匹配的结果。乍一看,这似乎是一个字符串。

*match.rb*

puts( /cde/ =˜ 'abcdefg' ) #=> 2
puts( /cde/.match('abcdefg') ) #=> cde


事实上,这是一个包含字符串的 MatchData 类的实例:

p( /cde/.match('abcdefg') ) #=> #<MatchData: "cde" >


MatchData 对象可能包含组,或 *捕获*,并且可以使用 `to_a` 或 `captures` 方法以数组的形式返回它们,如下所示:

*matchdata.rb*

x = /(^.)(#)(.)/.match( 'def myMethod # This is a very nice method' )
x.captures.each{ |item| puts( item ) }


之前的显示如下:

def myMethod

This is a very nice method


注意,`captures` 和 `to_a` 方法之间存在细微的区别。第一个只返回捕获的内容:

x.captures #=>["def myMethod ","#"," This is a very nice method"]


第二个返回原始字符串(索引 0)后跟捕获的内容:

x.to_a #=>["def myMethod # This is a very nice method","def myMethod
","#"," This is a very nice method"]


# 预匹配和后匹配

MatchData 类提供了 `pre_match` 和 `post_match` 方法来返回匹配之前或之后的字符串。例如,我正在对注释字符 `#` 进行匹配:

*pre_post_match.rb*

x = /#/.match( 'def myMethod # This is a very nice method' )
puts( x.pre_match ) #=> def myMethod
puts( x.post_match ) #=> This is a very nice method


或者,您可以使用特殊变量 `` $` ``(带有反引号)和 `$'`(带有普通引号),分别访问前匹配和后匹配:

x = /#/.match( 'def myMethod # This is a very nice method' )
puts( $` ) #=> def myMethod
puts( $' ) #=> This is a very nice method


当使用 `match` 与组一起使用时,您可以使用数组样式索引来获取特定项。索引 0 是原始字符串;更高的索引是组:

*match_groups.rb*

puts( /(.)(.)(.)/.match("abc")[2] ) #=> "b"


您可以使用特殊变量 `$˜` 来访问最后一个 MatchData 对象,并且您还可以通过数组样式索引来引用组:

puts( $˜[0], $˜[1], $˜[3] )


然而,为了使用 Array 类的全部方法,您必须使用 `to_a` 或 `captures` 来将匹配组作为数组返回:

puts( $˜.sort ) # this doesn't work!
puts( $˜.captures.sort ) # this does


# 贪婪匹配

当一个字符串包含多个潜在的匹配时,您有时可能希望返回到 *第一个* 匹配的字符串(即与匹配模式一致的最小字符串),而在其他时候,您可能希望返回到 *最后一个* 匹配的字符串(即尽可能多的字符串)。

在后一种情况下(尽可能多地获取字符串),匹配被称为 *贪婪的*。`*` 和 `+` 模式量词是贪婪的。然而,您可以通过在它们后面放置 `?` 来限制它们的贪婪程度,使它们返回尽可能少的内容:

*greedy1.rb*

puts( /.at/.match('The cat sat on the mat!') ) #=> The cat sat on the mat
puts( /.
?at/.match('The cat sat on the mat!') ) #=> The cat


您可以控制模式匹配的贪婪性,以执行诸如处理目录路径(这里匹配 `\` 字符)之类的操作:

*greedy2.rb*

puts( /.+\/.match('C:\mydirectory\myfolder\myfile.txt') )
#=> C:\mydirectory\myfolder
puts( /.+?\/.match('C:\mydirectory\myfolder\myfile.txt') )
#=> C:\


# 字符串方法

到目前为止,我在处理字符串时使用了 Regexp 类的方法。实际上,模式匹配可以双向进行,因为 String 类本身也有一些正则表达式方法。这些包括 `=˜` 和 `match`(因此你可以在匹配时切换 String 和 Regexp 对象的顺序),以及 `scan` 方法,它遍历字符串寻找尽可能多的匹配项。每个匹配项都被添加到一个数组中。例如,我正在寻找字母 *a*、*b* 或 *c* 的匹配项。`match` 方法返回第一个匹配项(“a”)并封装在 MatchData 对象中,但 `scan` 方法会继续扫描字符串,并返回它找到的所有匹配项作为数组的元素:

*match_scan.rb*

TESTSTR = "abc is not cba"
puts( "\n--match--" )
b = /[abc]/.match( TESTSTR ) #=> "a" (MatchData)
puts( "--scan--" )
a = TESTSTR.scan(/[abc]/) #=> ["a", "b", "c", "c", "b", "a"]


`scan` 方法可以可选地传递一个块,以便 `scan` 创建的数组元素可以被以某种方式处理:

a = TESTSTR.scan(/[abc]/){|c| print( c.upcase ) } #=> ABCCBA


可以使用正则表达式与许多其他 String 方法一起使用。`String.slice` 方法的一个版本接受一个正则表达式作为参数,并返回任何匹配的子字符串,同时不修改原始(接收者)字符串。`String.slice!` 方法(注意末尾的 `!`)从接收者字符串中删除匹配的子字符串,并返回该子字符串:

*string_slice.rb*

s = "def myMethod # a comment "

puts( s.slice( /m.d/ ) ) #=> myMethod
puts( s ) #=> def myMethod # a comment
puts( s.slice!( /m.
d/ ) ) #=> myMethod
puts( s ) #=> def # a comment


`split` 方法根据模式将字符串分割成子字符串。结果(减去模式)作为数组返回:

*string_ops.rb*

s = "def myMethod # a comment"

p( s.split( /m.*d/ ) ) #=> ["def ", " # a comment"]
p( s.split( /\s/ ) ) #=> ["def", "myMethod", "#", "a", "comment"]


你也可以根据空模式(`//`)进行分割:

p( s.split( // ) )


在这种情况下,返回一个字符数组:

["d", "e", "f", " ", "m", "y", "M", "e", "t", "h", "o", "d", " ", "#", " ",
"a", " ", "c", "o", "m", "m", "e", "n", "t"]


你可以使用 `sub` 方法来匹配正则表达式,并将其第一次出现替换为字符串。如果没有匹配,则返回未更改的字符串:

s = "def myMethod # a comment"
s2 = "The cat sat on the mat"
p( s.sub( /m.*d/, "yourFunction" ) ) #=> "def yourFunction # a comment"
p( s2.sub( /at/, "aterpillar" ) ) #=> "The caterpillar sat on the mat"


`sub!` 方法与 `sub` 方法类似,但修改了原始(接收者)字符串。或者,你可以使用 `gsub` 方法(或 `gsub!` 来修改接收者)来替换模式的所有出现:

p( s2.gsub( /at/, "aterpillar" ) )
#=> "The caterpillar saterpillar on the materpillar"


# 文件操作

我之前提到正则表达式通常用于处理存储在磁盘文件中的数据。在一些早期的例子中,我从磁盘文件中读取数据,进行了一些模式匹配,并将结果显示在屏幕上。这里有一个额外的例子,其中我统计文件中的单词。你这样做是通过按顺序扫描每一行来创建一个单词数组(即数字字符序列),然后将每个数组的长度添加到变量 `count` 中:

*wordcount.rb*

count = 0
File.foreach( 'regex1.rb' ){ |line|
count += line.scan( /[a-z0-9A-Z]+/ ).size
}
puts( "There are #{count} words in this file." )


如果你想要验证单词计数是否正确,你可以显示从文件中读取的单词的编号列表。这就是我在这里所做的事情:

*wordcount2.rb*

File.foreach( 'regex1.rb' ){ |line|
line.scan( /[a-z0-9A-Z]+/ ).each{ |word|
count +=1
print( "[#{count}] #{word}\n" )
}
}


现在我们来看看如何同时处理两个文件——一个用于读取,另一个用于写入。下一个示例打开 *testfile1.txt* 文件进行写入,并将文件变量 `f` 传递到一个块中。我现在打开第二个文件 *regex1.rb* 进行读取,并使用 `File.foreach` 将从该文件读取的每一行文本传递到第二个块中。我使用一个简单的正则表达式来创建一个新的字符串以匹配具有 Ruby 风格注释的行;当注释字符(`//`)是行上的第一个非空白字符时,代码将 C 风格注释字符替换为 Ruby 注释字符(`#`),并将每一行写入 *testfile1.txt*,代码行保持不变(因为没有匹配项),而注释行则改为 C 风格注释行:

*regexp_file1.rb*

File.open( 'testfile1.txt', 'w' ){ |f|
File.foreach( 'regex1.rb' ){ |line|
f.puts( line.sub(/(^\s)#(.)/, '\1//\2') )
}
}


这展示了使用正则表达式和非常少的代码可以完成多少工作。下一个示例展示了你可能如何读取一个文件(这里是指文件 *regex1.rb*)并写入两个新文件——其中一个(*comments.txt*)只包含行注释,而另一个(*nocomments.txt*)包含所有其他行。

*regexp_file2.rb*

file_out1 = File.open( 'comments.txt', 'w' )
file_out2 = File.open( 'nocomments.txt', 'w' )

File.foreach( 'regex1.rb' ){ |line|
if line =˜ /^\s*#/ then
file_out1.puts( line )
else
file_out2.puts( line )
end
}

file_out1.close
file_out2.close


深入挖掘

本节提供了一个正则表达式的便捷总结,后面是一些 Ruby 代码中的简短示例。

正则表达式元素

这是一个可以使用在正则表达式中的元素列表:

| `^` | 行或字符串的开始 |
| --- | --- |
| `| `^` | 行或字符串的开始 |
| --- | --- |
 | 行或字符串的末尾 |
| `.` | 任何字符(除了换行符) |
| `*` | 前一个正则表达式的零个或多个重复 |
| `*?` | 零个或多个前面的正则表达式(非贪婪) |
| `+` | 前一个正则表达式的多个重复 |
| `+?` | 前一个正则表达式的多个重复(非贪婪) |
| `[]` | 范围指定(例如,`[a-z]` 表示 a-z 范围内的字符) |
| `\w` | 一个字母数字字符 |
| `\W` | 一个非字母数字字符 |
| `\s` | 一个空白字符 |
| `\S` | 一个非空白字符 |
| `\d` | 一个数字 |
| `\D` | 一个非数字字符 |
| `\b` | 退格符(当在范围指定中时) |
| `\b` | 单词边界(当不在范围指定中时) |
| `\B` | 非单词边界 |
| `*` | 前一个字符的零个或多个重复 |
| `+` | 前一个字符的一个或多个重复 |
| `{m,n}` | 前一个字符至少重复 m 次,最多重复 n 次 |
| `?` | 前一个字符最多重复一次 |
| `&#124;` | 前一个或下一个表达式可以匹配 |
| `()` | 一个分组 |

正则表达式示例

这里有一些更多的示例正则表达式:

*overview.rb*

match chars...

puts( 'abcdefgh'.match( /cdefg/ ) ) # literal chars
#=> cdefg
puts( 'abcdefgh'.match( /cd..g/ ) ) # dot matches any char
#=> cdefg

list of chars in square brackets...

puts( 'cat'.match( /[fc]at/ )
#=> cat
puts( "batman's father's cat".match( /[fc]at/ ) )
#=> fat
p( 'bat'.match( /[fc]at/ ) )
#=> nil

match char in a range...

puts( 'ABC100x3Z'.match( /[A-Z][0-9][A-Z0-9]/ ) )
#=> C10
puts( 'ABC100x3Z'.match( /[a-z][0-9][A-Z0-9]/ ) )
#=> x3Z

escape 'special' chars with \

puts( 'ask who?/what?'.match( /who?/w..t?/ ) )
#=> who?/what?
puts( 'ABC 100x3Z'.match( /\s\S\d\d\D/ ) )
#=> 100x (note the leading space)

scan for all occurrences of pattern 'abc' with at least 2 and

no more than 3 occurrences of the letter 'c'

p( 'abcabccabcccabccccabccccccabcccccccc'.scan( /abc{2,3}/ ) )
#=> ["abcc", "abccc", "abccc", "abccc", "abccc"]

match either of two patterns

puts( 'my cat and my dog'.match( /cat|dog/ ) ) #=> cat

puts( 'my hamster and my dog'.match( /cat|dog/ ) ) #=> dog


符号和正则表达式

Ruby 1.9 允许你使用 `match` 与符号。符号将被转换为字符串,并返回匹配的索引。在 Ruby 1.8 中不能以这种方式使用符号。

*regexp_symbols.rb*

p( :abcdefgh.match( /cdefg/ ) ) #=> 2
p( :abcdefgh.match( /cd..g/ ) ) #=> 2
p( :cat.match( /[fc]at/ ) ) #=> 0
p( :cat.match( /[xy]at/ ) ) #=> nil
p( :ABC100x3Z.match( /[A-Z][0-9][A-Z0-9]/ ) ) #=> 2
p( :ABC100x3Z.match( /[a-z][0-9][A-Z0-9]/ ) ) #=> 6



# 第十七章。线程

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860138.png.jpg)

有时候,你的程序可能需要同时执行多个操作。例如,你可能想进行一些磁盘操作,同时向用户显示一些反馈。或者你可能想在用户继续在“前台”执行其他任务的同时,在“后台”复制或上传一些文件。

在 Ruby 中,如果你想同时执行多个任务,你可以为每个任务运行一个自己的 *线程*。线程就像程序中的程序。它独立于其他线程运行一些特定的代码。

然而,正如你很快就会看到的,多个线程可能需要找到相互合作的方法,以便例如,它们可以共享相同的数据,并且不会占用所有可用的处理时间,从而防止其他线程运行。在阅读本章时,你需要意识到 Ruby 1.9 及更高版本中线程的行为与 1.8 及更早版本中的线程有显著不同。我将在稍后解释原因。

# 创建线程

线程可以像创建任何其他对象一样创建,使用 `new` 方法。当你这样做时,你必须向线程传递一个包含你想要线程运行的代码的块。

以下是我尝试创建两个线程的第一次尝试,其中一个应该打印四个字符串,而另一个应该打印十个数字:

*threads1.rb*

This is a simple threading example that, however,

doesn't work as anticipated!

words = ["hello", "world", "goodbye", "mars" ]
numbers = [1,2,3,4,5,6,7,8,9,10]

Thread.new{
words.each{ |word| puts( word ) }
}

Thread.new{
numbers.each{ |number| puts( number ) }
}


很可能,当你运行这个程序时,你什么也看不到,或者至少非常少。它可能显示一些字符串和一些数字,但不是所有的,也不是任何容易预测的顺序。在存档中的示例代码中,我添加了程序执行时间的报告,这表明这个讨厌的东西在开始之前就结束了!

# 运行线程

这里是解决线程运行问题的简单方法。在代码的末尾,添加以下内容:

*threads2.rb*

sleep( 5 )


这将插入五秒的延迟。现在当你再次运行代码时,你应该能看到所有的字符串和所有的数字,尽管有点混乱,如下所示:

hello1

2world
3

4goodbye

5mars
6
7
8
9


实际上,这正是你想要的,因为它表明时间现在正在两个线程之间分配。这就是为什么单词和数字会混乱,有时甚至会把 `puts` 语句打印的回车符也混在一起,要么没有回车符,要么一次显示两个。这是因为线程正在疯狂地竞争可用的资源——首先一个线程执行并显示一个单词,然后下一个线程执行并显示一个数字,然后执行回到第一个线程,以此类推,直到第一个线程结束(当所有四个单词都显示完毕),此时第二个线程可以无干扰地运行。

现在将这个与程序的第一版进行比较。在那个程序中,我创建了两个线程,但当 Ruby 正在准备运行每个线程内部的代码时——*砰!*——它到达了程序的末尾并关闭了一切,包括我的两个线程。所以,实际上,线程在有时间做任何有趣的事情之前就被杀死了。

但是,当我添加一个调用`sleep( 5 )`的语句来插入五秒延迟时,Ruby 有足够的时间在程序退出之前运行线程。这个技术只有一个问题——这是一个*大*问题。为了让线程运行而向程序中添加不必要的延迟,这违背了练习的目的。现在计时器显示程序运行了整整五秒钟,这比严格必要的多出大约 4.99 秒!你很快就会学习到更文明地处理线程的方法。然而,首先我需要说几句关于 Ruby 1.8 和 Ruby 1.9 中线程之间一个重要区别的话。

# 原生化

在 Ruby 的所有版本中,包括 1.8.*x*,都没有访问“原生”线程(即由操作系统处理的线程)。实际上,Ruby 1.8 的线程存在于 Ruby 程序的封闭世界中,每个线程都分配了时间,在单个进程中使用称为*时间切片*的程序。Ruby 1.9(及更高版本)使用一个新的解释器,YARV(另一种 Ruby 虚拟机)。这使得 Ruby 1.9 能够使用原生线程,尽管有一些限制,我稍后会解释。

原则上,原生线程允许更高效的执行(使用*抢占式多任务处理*),其中操作系统负责在一个或多个处理器上执行线程。尽管 Ruby 1.9 使用原生线程,但它并不执行抢占式多任务处理。出于与现有 Ruby 程序兼容性的原因,Ruby 1.9 的原生线程以类似 Ruby 1.8 非原生(或*绿色*)线程的方式工作。换句话说,尽管 Ruby 1.9 实际上可能运行原生线程,但线程的执行调度是由 Ruby 虚拟机而不是操作系统来完成的。这意味着 Ruby 线程牺牲了效率;然而,它们至少受益于可移植性:在一个操作系统上编写的线程也可以在不同的操作系统上运行。

# 主线程

即使你没有明确创建任何线程,也始终至少有一个线程正在执行——那就是运行你的 Ruby 程序的主线程。你可以通过输入以下内容来验证这一点:

*thread_main.rb*

p( Thread.main )


这将显示类似以下的内容:

<Thread:0x28955c8 run>


这里,Thread 是线程的类,`0x28955c8`(或另一个数字)是其十六进制对象标识符,而`run`是线程的当前状态。

# 线程状态

每个线程都有一个状态,可能是以下之一:

| `run` | 当线程正在执行时 |
| --- | --- |
| `sleep` | 当线程正在睡眠或等待 I/O 时 |
| `aborting` | 当线程正在中止时 |
| `false` | 当线程正常终止时 |
| `nil` | 当线程因异常而终止时 |

你可以使用 `status` 方法获取线程的状态。当检查线程时,状态也会显示,此时要么显示为 `nil` 或 `false` 的状态,表示`dead`。

*thread_status.rb*

puts( Thread.main.inspect ) #=> #<Thread:0x28955c8 run>
puts( Thread.new{ sleep }.kill.inspect ) #=> #<Thread:0x28cddc0 dead>
puts( Thread.new{ sleep }.inspect ) #=> #<Thread:0x28cdd48 sleep>
thread1 = Thread.new{ }
puts( thread1.status ) #=> false
thread2 = Thread.new{ raise( "Exception raised!" ) }
puts( thread2 ) #=> nil


注意,显示的状态可能会根据所使用的 Ruby 版本以及程序运行的时间不同而有所差异。这是因为线程上的操作可能不会立即发生,状态变化的时间可能会随着每次执行而变化。例如,有时你可能看到已终止线程的状态显示为“aborting”,而有时则显示为“dead”。线程在死亡之前会中止,其状态的变化可能发生在毫秒级别。以下是从 Ruby 类库文档中摘取的一个示例。每个线程的文档状态在注释中显示:

*thread_status2.rb*

p d.kill #=> #<Thread:0x401b3678 aborting>
p a.status #=> nil
p b.status #=> "sleep"
p c.status #=> false
p d.status #=> "aborting"
p Thread.current.status #=> "run"


但当我用 Ruby 1.9 运行这段代码时,状态变化很大,并不总是与前面文档示例中显示的状态相匹配。有时候,我看到的是:

<Thread:0x401b3678 aborting>

"run"
"sleep"
false
false
"run"


但当我再次运行它时,我看到的是:

<Thread:0x401b3678 aborting>

"run"
"run"
"run"
false
"run"


现在看看这个程序:

*thread_status3.rb*

t = Thread.new{ }
p t
p t.kill

sleep( 1 ) # try uncommenting this

puts( t.inspect )


再次运行时,输出每次都不同。我经常看到以下内容,这表明即使我已经“杀死”了线程,在测试其状态时它可能仍然处于“aborting”状态:

<Thread:0x2be6420 run>

<Thread:0x2be6420 aborting>

<Thread:0x2be6420 aborting>


现在我通过调用 `sleep` 方法强制引入一秒的时间延迟:

sleep( 1 )
puts( t.inspect )


这次线程有足够的时间被终止,显示如下:

<Thread:0x2be6420 dead>


这些时间问题在 Ruby 1.9 中比在旧版本中更可能出现。你需要意识到这些问题,并在必要时反复检查线程的状态,以验证它在任何给定时刻是否处于预期的状态。

# 确保线程执行

让我们回到之前程序中遇到的问题。回想一下,我创建了两个线程,但程序在它们有机会完全运行之前就结束了。我通过使用 `sleep` 方法插入固定长度的延迟来解决这个问题。故意在程序中引入无谓的延迟并不是你想要的一般做法。幸运的是,Ruby 有一种更文明的方式来确保线程有足够的时间执行。`join` 方法强制调用线程(例如,*主* 线程)暂停其自己的执行(这样它就不会只是终止程序),直到调用 `join` 的线程完成:

*join.rb*

words = ["hello", "world", "goodbye", "mars" ]
numbers = [1,2,3,4,5,6,7,8,9,10]

Thread.new{
words.each{ |word| puts( word ) }
}.join

Thread.new{
numbers.each{ |number| puts( number ) }
}.join


初看之下,这似乎是进步,因为两个线程都得到了它们执行所需的时间,你也没有引入任何不必要的延迟。然而,当你查看输出时,你会发现线程是按顺序运行的——*第二个线程在第一个线程完成后才开始运行*。这就是为什么输出首先显示所有单词,它们在第一个线程中显示,然后是所有数字,在第二个线程中显示。但你所真正想要的是让两个线程同时运行,Ruby 在它们之间切换,给每个线程分配一段可用的处理时间。

下一个程序,*threads3.rb*,展示了实现这一目标的一种方法。它创建了两个线程,就像之前一样;然而,这次它将每个线程分配给一个变量,即,`wordsThread` 和 `numbersThread`:

*threads3.rb*

wordsThread = Thread.new{
words.each{ |word| puts( word ) }
}
numbersThread = Thread.new{
numbers.each{ |number| puts( number ) }
}


现在,它将这些线程放入一个数组中,并调用 `each` 方法将它们传递到一个块中,在该块中它们通过块变量 `t` 被接收,该变量简单地调用每个线程的 `join` 方法:

[wordsThread, numbersThread].each{ |t| t.join }


正如你将从输出中看到的,现在两个线程“并行”运行,所以它们的输出是混乱的,但没有人工延迟,总执行时间可以忽略不计。

# 线程优先级

到目前为止,我已经给了 Ruby 在线程之间分配时间的完全自由。但有时一个线程比其他线程更重要。例如,如果你正在编写一个文件复制程序,一个线程用于实际复制,另一个线程用于显示进度条,那么给文件复制线程分配大部分时间是有意义的。

### 注意

有时候,当前正在执行的线程可能特别想要将执行时间让给其他线程。这是通过调用 `Thread.pass` 方法来实现的。然而,这可能不会产生你期望的结果。`pass` 方法在 深入挖掘 的 深入挖掘 中有更详细的讨论。

Ruby 允许你分配整数值来表示每个线程的优先级。从理论上讲,优先级较高的线程比优先级较低的线程分配更多的执行时间。但在实践中,事情并不那么简单,因为其他因素(例如线程运行的顺序)可能会影响分配给每个线程的时间。此外,在非常短的程序中,改变优先级的效果可能无法确定。你之前使用的简短的字词和数字线程示例远远不足以展示任何明显的差异。因此,让我们看看一个稍微复杂一些的程序——一个运行三个线程的程序,每个线程调用一个方法五十次来计算 50 的阶乘。对我们来说,理解代码如何计算阶乘并不重要。然而,请记住,它使用了在第六章中解释的缩写(三元运算符)*if..else*表示法(*`< Test Condition >`* `?` *`<if true do this> : <else do this>`*):

*threads4.rb*

def fac(n)
n == 1 ? 1 : n * fac(n-1)
end

t1 = Thread.new{
0.upto(50) {fac(50); print( "t1\n" )}
}

t2 = Thread.new{
0.upto(50) {fac(50); print( "t2\n" )}
}

t3 = Thread.new{
0.upto(50) {fac(50); print( "t3\n" )}
}


你现在可以为每个线程设置特定的优先级:

t1.priority = 0
t2.priority = 0
t3.priority = 0


在这种情况下,每个线程的优先级相同,因此,从原则上讲,没有线程会被分配到最大的执行份额,所有三个线程的结果应该以通常的混乱方式出现。这确实是 Ruby 1.8 的情况,但请注意,在某些版本的 Ruby 1.9 中,线程优先级可能不会产生预期的结果。

Ruby 1.9 的线程优先级问题

在 Ruby 1.9 中,线程优先级并不总是按文档所述工作。以下是一个从 Ruby 类库文档中摘取的例子:

count1 = count2 = 0
a = Thread.new do
loop { count1 += 1 }
end
a.priority = −1

b = Thread.new do
loop { count2 += 1 }
end

b.priority = −2
p sleep 1 #=> 1
p count1 #=> 622504

p count2 #=> 5832


*priority_test.rb*

从原则上讲,`count1` 在优先级较高的线程(`b`)上递增,而 `count2`(在线程 `a` 上)则较低,因此,它应该总是产生一个比注释中所示更高的数值。但在实践中(至少当使用 Ruby 1.9.2 在 Windows 上运行此程序时),`count1` 有时比 `count2` 高,有时比 `count2` 低。这种行为已被报告并记录,其作为“错误”或“特性”的状态尚有争议。我个人认为这是不希望的,并仍然希望它能得到修复。然而,在使用自己的程序之前,你必须确保验证线程优先级的效果。本章大部分关于线程优先级的讨论假设你使用的是文档中所述优先级正常工作的 Ruby 版本。

现在,在 *threads4.rb* 中尝试更改 `t3` 的优先级:

t3.priority = 1


这次当你运行代码时,`t3`(至少在 Ruby 1.8 中)将占用大部分时间,并在其他线程之前执行(主要是)。其他线程可能在开始时有机会,因为它们是以相同的优先级创建的,并且优先级是在它们开始运行之后改变的。当 `t3` 完成,`t1` 和 `t2` 应该大致平均分配时间。

那么,假设你想要`t1`和`t2`先运行,时间分配大致相等,只有在那些两个线程完成之后才运行`t3`。这是我的第一次尝试;你可能想亲自试试:

t1.priority = 2
t2.priority = 2
t3.priority = 1


嗯,最终结果并不是我想要的!看起来线程是按顺序运行的,完全没有时间片分片!好吧,只是为了好玩,让我们尝试一些负数:

t1.priority = −1
t2.priority = −1
t3.priority = −2


哈哈!这次好多了。这次(至少在 Ruby 1.8 中),`t1`和`t2`是并发运行的,尽管你可能会看到在设置线程优先级之前`t3`短暂执行;然后`t3`开始运行。那么,为什么负值有效而正值无效呢?

负值本身并没有什么特殊之处。然而,你需要记住,每个进程至少有一个正在运行的线程——*主线程*,它也有一个优先级。它的优先级恰好是 0。

# 主线程优先级

你可以轻松地验证主线程的优先级:

*main_thread.rb*

puts( Thread.main.priority ) #=> 0


因此,在之前的程序(*threads4.rb*)中,如果你将`t1`的优先级设置为 2,它将“超越”主线程本身,然后将被分配所有需要的执行时间,直到下一个线程`t2`到来,依此类推。通过将优先级设置为主线程以下,你可以迫使三个线程只与自己竞争,因为主线程总是会超越它们。如果你更喜欢使用正数,你可以将主线程的优先级特别设置为比其他所有线程都高的值:

Thread.main.priority=100


Ruby 1.9 可能不会尊重以这种方式分配的所有值。例如,当我显示将线程的优先级设置为 100 时,Ruby 1.9 显示 3,而 Ruby 1.8 显示 100。

如果你想要`t2`和`t3`具有相同的优先级,而`t1`具有较低的优先级,你需要为这三个线程以及主线程设置优先级:

*threads5.rb*

Thread.main.priority = 200
t1.priority = 0
t2.priority = 1
t3.priority = 1


再次强调,这假设你使用的是尊重线程优先级的 Ruby 版本(例如 Ruby 1.8)。如果你仔细查看输出,你可能会注意到一个微小但不受欢迎的副作用。有可能(不是*确定*,而是*可能*)你会看到`t1`线程的一些输出,就在`t2`和`t3`启动并声明它们的优先级之前。这是之前提到的问题:每个线程都试图在创建后立即开始运行,而`t1`可能会在其他线程的优先级“升级”之前获得自己的动作份额。为了防止这种情况,你可以在创建时使用`Thread.stop`特别挂起线程,如下所示:

*stop_run.rb*

t1 = Thread.new{
Thread.stop
0.upto(50){print( "t1\n" )}
}


现在,当你想要启动线程运行(在这种情况下,在设置线程优先级之后),你调用它的`run`方法:

t1.run


注意,某些 Thread 方法的使用可能会导致 Ruby 1.9 中的*死锁*。死锁发生在两个或多个线程都在等待对方释放资源时。为了避免死锁,你可能更喜欢使用互斥锁,正如我接下来要解释的。

# 互斥锁

有时候,多个线程可能需要访问某种全局资源。这可能导致错误的结果,因为全局资源的当前状态可能被一个线程修改,而这个修改后的值在由其他线程使用时可能是不可预测的。为了一个简单的例子,看看这段代码:

*no_mutex.rb*

$i = 0

def addNum(aNum)
aNum + 1
end

somethreads = (1..3).collect {
Thread.new {
1000000.times{ \(i = addNum(\)i) }
}
}

somethreads.each{|t| t.join }
puts( $i )


我的意图是创建并运行三个线程,每个线程将全局变量 `$i` 增加 100 万次。我通过从 1 到 3 进行枚举,并使用 `collect` 方法(`map` 方法与 `collect` 同义,因此也可以使用)从块返回的结果创建一个数组来实现这一点。这个线程数组 `somethreads` 随后通过 `join` 将每个线程 `t` 传递给一个要执行的块,如前所述。每个线程调用 `addNum` 方法来增加 `$i` 的值。在这次操作结束时,`$i` 的预期结果自然是 300 万。但实际上,当我运行这个程序时,`$i` 的最终值是 1,068,786(尽管你可能看到不同的结果)。

这种解释是,三个线程实际上是在竞争访问全局变量 `$i`。这意味着在某些时候,线程 `a` 可能会获取 `$i` 的当前值(假设它恰好是 100),同时线程 `b` 也会获取 `$i` 的当前值(仍然是 100)。现在,`a` 增加了它刚刚获取的值(`$i` 变为 101),而 `b` 增加了它刚刚获取的值,这个值是 100(所以 `$i` 再次变为 101)。换句话说,当多个线程同时访问一个共享资源时,其中一些可能在使用过时的值,也就是说,这些值没有考虑到其他线程所做的任何修改。随着时间的推移,这些操作产生的错误会累积,最终导致的结果与预期的结果大相径庭。

为了解决这个问题,你需要确保当一个线程访问全局资源时,它会阻止其他线程的访问。这另一种说法是,多个线程对全局资源的访问应该是“互斥的”。你可以使用 Ruby 的 Mutex 类来实现这一点,它使用一个信号量来指示资源是否正在被访问,并提供 `synchronize` 方法来防止在块内部访问资源。请注意,原则上,你必须 `require 'thread'` 来使用 Mutex 类,但在 Ruby 的某些版本中,这可能是自动提供的。以下是我的重写代码:

*mutex.rb*

require 'thread'

$i = 0
semaphore = Mutex.new

def addNum(aNum)
aNum + 1
end

somethreads = (1..3).collect {
Thread.new {
semaphore.synchronize{
1000000.times{ \(i = addNum(\)i) }
}
}
}

somethreads.each{|t| t.join }
puts( $i )


这次,`$i` 的最终结果是 3,000,000。

最后,为了更实用地展示线程的使用示例,请查看 *file_find2.rb*。这个示例程序使用 Ruby 的 `Find` 类遍历磁盘上的目录。对于非线程示例,请参阅 *file_find.rb*。将其与 Sorting by Size 中的 *file_info3.rb* 程序进行比较,该程序使用 `Dir` 类。

这个程序启动了两个线程。第一个线程 `t1` 调用 `processFiles` 方法来查找并显示文件信息(你需要编辑 `processFiles` 的调用,以便传递系统上的目录名)。第二个线程 `t2` 简单地打印一条消息,并且当 `t1` “存活”(即正在运行或睡眠)时,这个线程在运行:

*file_find2.rb*

require 'find'
require 'thread'

\(totalsize = 0 \)dirsize = 0

semaphore = Mutex.new

def processFiles( baseDir )
Find.find( baseDir ) { |path|
$dirsize += \(dirsize # if a directory if (FileTest.directory?(path)) && (path != baseDir ) then print( "\n#{path} [#{\)dirsize / 1024}K]" )
$dirsize = 0
else # if a file
\(filesize = File.size(path) print( "\n#{path} [#{\)filesize} bytes]" )
$dirsize += $filesize
$totalsize += $filesize
end
}
end

t1 = Thread.new{
semaphore.synchronize{
processFiles( '..' ) # you may edit this directory name
}
}

t2 = Thread.new{
semaphore.synchronize{
while t1.alive? do
print( "\n\t\tProcessing..." )
Thread.pass
end
}
}

t2.join

printf( "\nTotal: #{\(totalsize} bytes, #{\)totalsize/1024}K, %0.02
fMB\n\n", "#{\(totalsize/1048576.0}" ) puts( "Total file size: #{\)filesize}, Total directory size: #{$dirsize}" )


在实际应用中,你可以将这种技术适应以在某个密集型过程(如目录遍历)进行时提供某种用户反馈。

# Fibers

Ruby 1.9 引入了一个名为 Fiber 的新类,它有点像线程,又有点像块。Fiber 的目的是实现“轻量级并发”。这广泛意味着它们像块一样操作(参见第十章 Blocks, Procs, and Lambdas),其执行可以被暂停和重新启动,就像线程一样。然而,与线程不同的是,Fiber 的执行不是由 Ruby 虚拟机调度;它必须由程序员显式控制。线程和 Fiber 之间的另一个区别是,线程在创建时自动运行;而 Fiber 不自动运行。要启动一个 Fiber,你必须调用它的 `resume` 方法。要向 Fiber 外部的代码让出控制权,你必须调用 `yield` 方法。

让我们看看一些简单的示例:

*fiber_test.rb*

f = Fiber.new do
puts( "In fiber" )
Fiber.yield( "yielding" )
puts( "Still in fiber" )
Fiber.yield( "yielding again" )
puts( "But still in fiber" )
end

puts( "a" )
puts( f.resume )
puts( "b" )
puts( f.resume )
puts( "c" )
puts( f.resume )
puts( "d" )
puts( f.resume ) # dead fiber called


在这里,我创建了一个新的 Fiber,`f`,但并没有立即启动它运行。首先我显示“a”,`puts( "a" )`,然后我启动 Fiber,`f.resume`。Fiber 开始执行并显示“在 Fiber 中”的消息。然后它调用 `yield` 并传递“yielding”字符串。这暂停了 Fiber 的执行,并允许 Fiber 外部的代码继续。调用 `f.resume` 的代码现在会打印出被 `yield` 的字符串,因此“yielding”被显示。再次调用 `f.resume` 会从上次停止的地方重新启动 Fiber,因此显示“仍在 Fiber 中”,依此类推。每次调用 `yield`,执行都会返回到 Fiber 外部的代码。当该代码调用 `f.resume` 时,Fiber 中剩余的代码会被执行。一旦没有更多的代码要执行,Fiber 就会终止。当通过 `f.resume` 调用一个非活动(或 *已死亡*)的 Fiber 时,会引发 FiberError。这是前面程序输出的内容:

a
In fiber
yielding
b
Still in fiber
c
But still in fiber
d
C:/bookofruby/ch17/fiber_test.rb:18:in resume': dead fiber called (FiberError) from C:/bookofruby/ch17/fiber_test.rb:18:in

'


你可以通过使用 `alive?` 方法测试 Fiber 的状态来避免“已死亡 Fiber”错误。如果 Fiber 是活动的,它返回 true;如果是不活动的,它返回 false。你必须 `require 'fiber'` 才能使用此方法:

*fiber_alive.rb*

require 'fiber'

if (f.alive?) then
puts( f.resume )
else
puts("Error: Call to dead fiber" )
end


`resume` 方法接受任意数量的参数。在第一次调用 `resume` 时,它们作为块参数传递。否则,它们成为 `yield` 调用的返回值。以下示例取自 Ruby 类库中的文档:

*fiber_test2.rb*

fiber = Fiber.new do |first|
second = Fiber.yield first + 2
end

puts fiber.resume 10 #=> 12
puts fiber.resume 14 #=> 14
puts fiber.resume 18 #=> dead fiber called (FiberError)


这里有一个简单示例,说明了两个 Fiber 的使用:

f = Fiber.new {
| s |
puts( "In Fiber #1 (a) : " + s )
puts( "In Fiber #1 (b) : " + s )
Fiber.yield
puts( "In Fiber #1 (c) : " + s )
}

f2 = Fiber.new {
| s |
puts( "In Fiber #2 (a) : " + s )
puts( "In Fiber #2 (b) : " + s )
Fiber.yield
puts( "In Fiber #2 (c) : " + s )
}

f.resume( "hello" )
f2.resume( "hi" )
puts( "world" )
f2.resume
f.resume


这启动了第一条纤维,`f`,它一直运行到调用`yield`。然后它启动第二条纤维,`f2`,它也一直运行到它也调用`yield`。然后主程序显示字符串“world”,最后`f2`和`f`被恢复。这是输出:

In Fiber #1 (a) : hello
In Fiber #1 (b) : hello
In Fiber #2 (a) : hi
In Fiber #2 (b) : hi
world
In Fiber #2 (c) : hi
In Fiber #1 (c) : hello


深入挖掘

在这里,你将学习如何将执行权从一个线程传递到另一个线程。你将发现一些 Ruby 文档没有告诉你的内容,以及 Ruby 不同版本的一些奇怪之处。

将执行权传递给其他线程

有时你可能希望特定的线程将执行权让给任何正在运行的线程。例如,如果你有多个线程正在执行稳定的图形操作或显示各种“实时”统计信息,你可能想确保一旦一个线程绘制了 X 数量的像素或显示了 Y 数量的统计信息,其他线程将保证有机会做些事情。

理论上,`Thread.pass`方法负责处理这个问题。Ruby 的源代码文档指出`Thread.pass`“调用线程调度器将执行传递给另一个线程。”这是 Ruby 文档提供的示例:

*pass0.rb*

a = Thread.new { print "a"; Thread.pass;
print "b"; Thread.pass;
print "c" }
b = Thread.new { print "x"; Thread.pass;
print "y"; Thread.pass;
print "z" }
a.join
b.join


根据文档,这段代码在运行时会产生输出`axbycz`。确实如此,它确实产生了这样的输出。因此,理论上,这似乎表明通过在每次调用`print`之后调用`Thread.pass`,这些线程将执行权传递给另一个线程,这就是为什么两个线程的输出交替出现。

由于我有一种怀疑的心态,我想知道移除对`Thread.pass`的调用会有什么效果。第一个线程是否会一直占用所有时间,直到完成才将控制权交给第二个线程?找出答案的最佳方式是尝试一下:

*pass1.rb*

a = Thread.new { print "a";
print "b";
print "c" }
b = Thread.new { print "x";
print "y";
print "z" }
a.join
b.join


如果我的理论是正确的(即线程`a`将一直占用所有时间直到完成),这将是最预期的输出:`abcdef`。事实上(令我惊讶的是!),实际产生的输出实际上是`axbycz`。

换句话说,无论是否有所有那些对`Thread.pass`的调用,结果都是相同的。那么,如果有什么的话,`Thread.pass`在做什么?当它声称`pass`方法调用线程调度器将执行传递给另一个线程时,文档是错误的吗?

在一个短暂而愤世嫉俗的时刻,我承认我考虑了这样的可能性,即文档可能是错误的,`Thread.pass`根本不做任何事情。查看 Ruby 的 C 语言源代码很快消除了我的疑虑;`Thread.pass`确实做了些什么,但它的行为并不像 Ruby 文档似乎暗示的那样可预测。在解释原因之前,让我们尝试一个我自己的例子:

*pass2.rb*

s = 'start '
a = Thread.new { (1..10).each{
s << 'a'
Thread.pass
}
}
b = Thread.new { (1..10).each{
s << 'b'
Thread.pass
}
}

a.join
b.join
puts( "#{s} end" )


初看,这似乎与上一个例子非常相似。它启动了两个线程,但与反复打印内容不同,这些线程反复向一个字符串中添加字符——`a`由 `a` 线程添加,`b`由 `b` 线程添加。每次操作后,`Thread.pass` 将执行权传递给另一个线程。最后,整个字符串被显示出来。当使用 Ruby 1.8 运行时,字符串包含交替的`a`和`b`字符序列并不令人惊讶:`abababababababababab`。然而,在 Ruby 1.9 中,字符不会交替,这是我看到的结果:`aaaaaaaaaabbbbbbbbbb`。在我看来,`pass` 方法在 Ruby 1.9 中并不可靠,接下来的讨论仅适用于 Ruby 1.8。

现在,记住在之前的程序中,即使我移除了 `Thread.pass` 的调用,我也获得了相同的交替输出。基于那个经验,我猜如果我在这个程序中删除 `Thread.pass`,我应该期待类似的结果。让我们试试:

*pass3.rb*

s = 'start '
a = Thread.new { (1..10).each{
s << 'a'
}
}
b = Thread.new { (1..10).each{
s << 'b'
}
}

a.join
b.join
puts( "#{s} end" )


这次,这是输出结果:`aaaaaaaaaabbbbbbbbbb`。

换句话说,这个程序展示了我在第一个程序(我从 Ruby 的嵌入式文档中复制出来的那个程序)中最初预期的不同行为,也就是说,当两个线程被留给自己运行时,第一个线程 `a` 会独占所有时间,只有当它完成时,第二个线程 `b` 才有机会。但是,通过在 Ruby 1.8 中显式添加 `Thread.pass` 的调用,你可以强制每个线程将执行权传递给其他线程。

那么,如何解释这种行为差异呢?本质上,*pass0.rb* 和 *pass3.rb* 做的是相同的事情——运行两个线程并显示每个线程的字符串。唯一的真正区别在于,在 *pass3.rb* 中,字符串是在线程内部连接的,而不是打印。这看起来可能不是什么大问题,但结果证明,打印一个字符串比连接一个字符串要花费更多的时间。因此,调用 `print` 会引入时间延迟。正如你之前发现的(当我故意使用 `sleep` 引入延迟时),时间延迟对线程有深远的影响。

如果你仍然不相信,尝试我重写的 *pass0.rb* 版本,我创造性地将其命名为 *pass0_new.rb*。这仅仅是将打印替换为连接。现在,如果你注释和取消注释 `Thread.pass` 的调用,你确实会在 Ruby 1.8 中看到不同的结果:

*pass0_new.rb*

s = ""
a = Thread.new { s << "a"; Thread.pass;
s << "b"; Thread.pass;
s << "c" }

b = Thread.new { s << "x"; Thread.pass;
s << "y"; Thread.pass;
s << "z" }

a.join
b.join
puts( s )


使用 `Thread.pass`,Ruby 1.8 会显示以下内容:

axbycz


没有使用 `Thread.pass`,Ruby 1.8 会显示以下内容:

abcxyz


在 Ruby 1.9 中,`Thread.pass` 的存在与否没有明显的影响。无论是否有它,都会显示以下内容:

abcxyz


偶然的是,我的测试是在运行 Windows 的 PC 上进行的。在其他操作系统上可能会看到不同的结果。这是因为 Ruby 调度器的实现不同,它控制着分配给线程的时间量,在 Windows 和其他操作系统上有所不同。

作为最后的例子,你可能想看看 *pass4.rb* 程序,这个程序仅适用于 Ruby 1.8。它创建了两个线程并立即挂起它们(`Thread.stop`)。在每一个线程的主体中,线程的信息,包括其 `object_id`,被追加到一个数组 `arr` 中,然后调用 `Thread.pass`。最后,运行并合并这两个线程,并显示数组 `arr`。尝试取消注释 `Thread.pass` 来进行实验,以验证其效果(请密切注意线程的执行顺序,如它们的 `object_id` 标识符所示):

*pass4.rb*

arr = []
t1 = Thread.new{
Thread.stop
(1..10).each{
arr << Thread.current.to_s
Thread.pass
}
}
t2 = Thread.new{
Thread.stop
(1..10).each{ |i|
arr << Thread.current.to_s
Thread.pass
}
}
puts( "Starting threads..." )
t1.run
t2.run
t1.join
t2.join
puts( arr )



# 第十八章。调试与测试

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860138.png.jpg)

任何现实世界的应用程序的开发都是逐步进行的。我们大多数人更愿意向前迈出更多步伐而不是向后。为了最小化由编码错误或未预见的副作用引起的后退步伐,你可以利用测试和调试技术。

本章简要概述了 Ruby 程序员可用的一些最有用的调试工具。然而,请注意,如果你使用的是专门的 Ruby IDE,你可能会有更多强大的可视化调试工具可用。在本章中,我将仅讨论 Ruby 可用的“标准”工具。

# IRB:交互式 Ruby

有时你可能只是想用 Ruby “尝试做某事”。你可以使用标准的 Ruby 解释器来做这件事:在命令提示符下输入 `ruby`,然后逐行输入你的代码。然而,这远非一个理想的交互式环境。首先,你输入的代码只有在输入文件结束字符(例如 `^Z` 或 `^D`,即 Windows 上的 Ctrl-Z 或某些其他操作系统上的 Ctrl-D)时才会执行。因此,为了执行像显示 1 加 1 的值这样简单的事情,你需要输入以下序列命令(记住输入你操作系统所需的文件结束字符)。

ruby
1+1
^Z


只有在输入文件结束字符(这里为 `^Z`)后,Ruby 才会执行代码并显示结果:

2


为了更好地与 Ruby 交互,请使用交互式 Ruby 壳(IRB)。要启动 IRB,请转到命令提示符并输入以下内容:

irb


你现在应该看到一个类似于以下提示符的界面:

irb(main):001:0>


现在你已经准备好开始输入一些 Ruby 代码了。你可以输入多行表达式;一旦表达式完成,IRB 将评估它并显示结果。尝试以下操作(在第一行的 `+` 后按回车键):

x = ( 10 +
( 2 * 4 ) )


最后,在闭括号后按回车键。现在 IRB 将评估表达式并显示结果:

=> 18


你现在可以评估 `x`。输入以下内容:

x


IRB 显示如下:

=> 18


因此,到目前为止,你的整个 IRB 会话应该看起来像这样:

irb(main):001:0> x = (10 +
irb(main):002:1* (2*4))
=> 18
irb(main):003:0> x
=> 18
irb(main):004:0>


但是要小心。尝试输入以下内容:

x = (10

  • (2*4))

这次的结果如下:

=> 8


实际上,这是正常的 Ruby 行为。这是正常的,因为换行符充当终止符,而 `+` 操作符在开始新行时充当一元操作符(也就是说,它不是将两个值相加——一个在其左侧,一个在其右侧——而是断言 `+` 后面的单个表达式是正的)。你可以在 深入挖掘 中找到对这个问题的更全面解释。深入挖掘。

目前,请注意,当逐行输入表达式时,行断点的位置很重要!当使用 IRB 时,您可以判断解释器是否认为您已经结束了一个语句。如果您已经这样做,则会显示一个以`>`结尾的提示符,如下所示:

irb(main):013:1>


如果一个语句完整并返回一个结果,则会显示一个`=>`提示符,后跟结果。例如:

=> 18


如果语句不完整,提示符以星号结尾:

irb(main):013:1*


要结束 IRB 会话,请在提示符中输入单词`quit`或`exit`。如果您愿意,可以通过在运行 IRB 时传递程序名称来将 Ruby 程序加载到 IRB 中,如下所示:

irb myprogram.rb


您还可以使用各种选项调用 IRB,例如加载一个模块(`irb -r` *`[load-module]`*)或显示 IRB 版本号(`irb -v`)。许多可用的 IRB 选项相当晦涩,并且不太可能被大多数用户需要。可以通过在命令行输入以下内容来列出所有选项:

irb --help


虽然 IRB 可能对尝试一些代码很有用,但它并不提供您进行程序调试所需的所有功能。然而,Ruby 确实提供了一个命令行调试器。

# 调试

默认的 Ruby 调试器允许您在程序执行时设置断点和监视点以及评估变量。要在调试器中运行程序,请在启动 Ruby 解释器时使用`-r debug`选项(其中`-r`表示“require”,`debug`是调试库的名称)。例如,这样就可以调试名为*debug_test.rb*的程序:

ruby -r debug debug_test.rb


一旦调试器启动,您就可以输入各种命令来逐步执行代码,设置断点以在特定行暂停执行,设置监视点以监视变量的值,等等。以下是可以用的调试命令:

**`b[reak] [file|class:]<line|method>`** 和 **`b[reak] [file|class:]<line|method>`**

设置断点到某个位置

**`b[reak] [class.]<line|method>`**

设置断点到某个位置

**`wat[ch] <expression>`**

设置监视点到某个表达式

**`cat[ch] <an Exception>`**

设置捕获点为异常

**`b[reak]`**

列出断点

**`cat[ch]`**

显示捕获点

**`del[ete][ nnn]`**

删除一些或所有断点

**`disp[lay] <expression>`**

将表达式添加到显示表达式列表

**`undisp[lay][ nnn]`**

删除一个特定的或所有显示表达式

**`c[ont]`**

运行到结束或断点

**`s[tep][ nnn]`**

步进(进入代码)一行或到行`nnn`

**`n[ext][ nnn]`**

跳过一行或直到行`nnn`

**`w[here]`**

显示框架

**`f[rame]`**

是`where`的别名

**`l[ist][ (-|nn-mm)]`**

列出程序,列出反向`nn-mm`,列出指定的行

**`up[ nn]`**

移动到更高的框架

**`down[ nn]`**

移动到较低的框架

**`fin[ish]`**

返回外部框架

**`tr[ace] (on|off)`**

设置当前线程的跟踪模式

**`tr[ace] (on|off) all`**

设置所有线程的跟踪模式

**`q[uit]`**

退出调试器

**`v[ar] g[lobal]`**

显示全局变量

**`v[ar] l[ocal]`**

显示局部变量

**`v[ar] i[nstance] <object>`**

显示对象的实例变量

**`v[ar] c[onst] <object>`**

显示对象的常量

**`m[ethod] i[nstance] <obj>`**

显示对象的方法

**`m[ethod] <class|module>`**

显示类或模块的实例方法

**`th[read] l[ist]`**

列出所有线程

**`th[read] c[ur[rent]]`**

显示当前线程

**`th[read] [sw[itch]] <nnn>`**

将线程上下文切换到 `nnn`

**`th[read] stop <nnn>`**

停止线程 `nnn`

**`th[read] resume <nnn>`**

恢复线程 `nnn`

**`p expression`**

评估表达式并打印其值

**`h[elp]`**

打印此帮助

**`<everything else>`**

评估

Ubygems?什么是 Ubygems?

在某些情况下,如果你输入命令 `ruby -r debug`,你可能会看到一个难以理解的类似以下的消息:

c:/ruby/lib/ruby/site_ruby/1.8/ubygems.rb:4:require 'rubygems'


如果发生这种情况,当你开始调试时,你会发现自己试图调试文件 *ubygems.rb* 而不是你的程序!这个问题可能发生在某些软件(例如,某些定制的 Ruby 安装程序)设置了环境变量 `RUBYOPT=-rubygems` 的时候。在大多数情况下,这会有一个期望的效果,即允许你的 Ruby 程序使用 Ruby Gems“打包系统”,这有助于安装 Ruby 库。然而,当你尝试使用 `-r` 选项时,这会被解释为 `-r ubygems`,这就是为什么会尝试加载文件 *ubygems.rb*。Ruby 方便地(或者可能是令人困惑地?)提供了一个名为 *ubygems.rb* 的文件,该文件除了要求 *rubygems.rb* 外不做任何事情。处理这个问题有两种方法。你可以永久地删除 `RUBYOPT`,或者暂时禁用它。但是,如果你选择永久删除它,那么在以后使用 Ruby Gems 时可能会遇到副作用。环境变量添加或删除的方式因操作系统而异。在 Windows 上,你需要点击开始菜单(如果使用 XP,则点击设置),然后点击控制面板(如果使用 Vista,则点击系统和维护),然后点击系统(在 Vista 上,你现在应该点击高级系统设置)。在系统属性对话框中,选择高级选项卡。接下来,点击环境变量;最后,在系统变量面板中找到 `RUBYOPT` 并删除它。一个更安全的替代方法是,在加载调试器之前在命令提示符中禁用变量。为此,请输入以下内容:

set RUBYOPT=


这将仅为此命令会话禁用 `RUBYOPT` 环境变量。你可以通过输入以下内容来验证这一点:

set RUBYOPT


你应该看到以下消息:

Environment variable RUBYOPT not defined


然而,打开另一个命令窗口并输入 `set RUBYOPT`,你会看到这里的环境变量保留了其默认值。

让我们看看这些命令如何在实际的调试会话中使用。打开一个系统提示符,导航到包含文件 *debug_test.rb* 的目录,该文件包含在本章的示例代码中。通过输入以下内容启动调试器:

*debug_test.rb*

ruby -r debug debug_test.rb


现在,让我们尝试几个命令。在这些示例中,我已用`[Enter]`来表示您应该在每个命令后按回车键。首先让我们查看代码列表(请注意,`l`是一个小写的*L*字符):

l [Enter]


您应该看到这个,这是文件*debug_test.rb*的部分列表:

debug_test.rb:2: class Thing
(rdb:1) l
[-3, 6] in debug_test.rb
1 # Thing
=> 2 class Thing
3
4 attr_accessor( :name )
5
6 def initialize( aName )
(rdb:1)


### 注意

如果此时您看到的是名为*ubygems.rb*的文件列表而不是您的程序,请参阅 Ubygems? 什么是 Ubygems?中的 Ubygems? 什么是 Ubygems?部分,了解如何纠正此问题。

您输入的`l`是“list”命令,它指示调试器以小块形式列出代码。实际行数将根据正在调试的代码而变化。让我们列出更多:

l [Enter]
l [Enter]


现在列出特定数量的行。输入字母`l`后跟数字`1`,一个连字符,然后是`100`:

l 1-100 [Enter]


让我们在第 78 行设置一个断点:

b 78 [Enter]


Ruby 调试器应该回复如下:

Set breakpoint 1 at debug_test.rb:78


您也可能设置一个或多个*观察点*。观察点可以用来在创建简单变量(例如,输入`wat @t2`会在创建`@t2`对象时中断)时触发中断;或者它可以设置为匹配特定值(例如,`i == 10`)。在这里,我想设置一个当`@t4`的`name`属性为“wombat”时中断的观察点。输入以下内容:

wat @t4.name == "wombat" [Enter]


调试器应该确认这一点:

Set watchpoint 2:@t4.name == "wombat"


注意观察点编号是 2。如果您随后决定删除观察点,您将需要这个编号。好的,现在让我们继续(`c`)执行:

c [Enter]


程序将运行直到遇到断点。您将看到类似于以下的消息:

Breakpoint 1, toplevel at debug_test.rb:78
debug_test.rb:78: puts("Game start" )


这里显示了它停止的行号和该行的代码。让我们继续:

c [Enter]


这次它在这里中断:

Watchpoint 2, toplevel at debug_test.rb:85
debug_test.rb:86: @t5 = Treasure.new("ant", 2)


这是紧随成功评估观察点条件之后的行。通过列出指示的行号来检查:

l 86


调试器显示了一组带有当前执行行(86)的行,前面有`=>`标记:

[81, 90] in debug_test.rb
81 @t1 = Treasure.new("A sword", 800)
82 @t4 = Treasure.new("potto", 500 )
83 @t2 = Treasure.new("A dragon Horde", 550)
84 @t3 = Treasure.new("An Elvish Ring", 3000)
85 @t4 = Treasure.new("wombat", 10000)
=> 86 @t5 = Treasure.new("ant", 2)
87 @t6 = Treasure.new("sproggit", 400)
88
89 # ii) Rooms
90 @room1 = Room.new("Crystal Grotto", [])


如您所见,第 86 行包含匹配观察点条件的代码。请注意,执行并没有在创建`@t4`的原始位置(第 82 行)停止,因为那里的观察点条件未满足(其`name`属性是“potto”而不是“wombat”)。如果您想在断点或观察点暂停时检查变量的值,只需输入其名称。试一试:

@t4 [Enter]


调试器将显示以下内容:

<Treasure:0x315617c @value=10000, @name="wombat">


您可以类似地输入其他要评估的表达式。试一试:

@t1.value [Enter]


这显示了`800`。

或者输入一些任意的表达式,例如这个:

10+4/2 [Enter]


这显示了`12`。

现在删除观察点(回想一下,其编号是 2):

del 2 [Enter]


然后继续直到程序退出:

c [Enter]


您可以使用更多命令以这种方式调试程序,您可能想尝试之前给出的表格中显示的命令。您还可以通过输入`help`或只是`h`来查看调试会话期间的命令列表:

h [Enter]


要退出调试会话,输入 `quit` 或 `q`:

q [Enter]


虽然标准的 Ruby 调试器有其用途,但它远不如集成开发环境提供的图形调试器简单或方便使用。此外,它相当慢。在我看来,它适合调试简单的脚本,但不建议用于调试大型和复杂的程序。

# 单元测试

*单元测试* 是一种调试后的测试技术,它允许你尝试程序的一部分,以验证它们是否按预期工作。一些程序员习惯性地使用单元测试,除了或甚至代替交互式调试;其他程序员很少或从不使用它。关于单元测试的技术和方法已经写成了整本书,我这里只介绍其基础。

单元测试的基本思想是你可以编写一系列“断言”,声明某些结果应该是某些行为的后果。例如,你可能断言某个特定方法的返回值应该是 100,它应该是一个布尔值,或者它应该是一个特定类的实例。如果在测试运行时,断言被证明是正确的,则测试通过;如果它是不正确的,则测试失败。

这里有一个例子,如果对象 `t` 的 `getVal` 方法返回的任何值不是 100,它将会失败:

assert_equal(100, t.getVal)


但是你不能只是随意在你的代码中添加这种类型的断言。这个游戏有精确的规则。首先,你必须 require `test/unit` 文件。然后,你需要从一个在 `Test` 模块中的 `TestCase` 类派生一个测试类,该类本身位于 `Unit` 模块中:

class MyTest < Test::Unit::TestCase


在这个类中,你可以编写一个或多个方法,每个方法都包含一个或多个断言的测试。方法名必须以 `test` 开头(因此名为 `test1` 或 `testMyProgram` 的方法是允许的,但名为 `myTestMethod` 的方法则不行)。以下方法包含一个测试,该测试断言 `TestClass.new(100).getVal` 的返回值应该是 1,000:

def test2
assert_equal(1000,TestClass.new(100).getVal)
end


以下是一个完整的(尽管简单)测试套件,其中我定义了一个名为 MyTest 的 TestCase 类来测试 TestClass。在这里(稍加想象!),TestClass 可以代表我想要测试的整个程序:

*test1.rb*

require 'test/unit'

class TestClass
def initialize( aVal )
@val = aVal * 10
end

def getVal
    return @val
end

end

class MyTest < Test::Unit::TestCase
def test1
t = TestClass.new(10)
assert_equal(100, t.getVal)
assert_equal(101, t.getVal)
assert(100 != t.getVal)
end

def test2
    assert_equal(1000,TestClass.new(100).getVal)
end

end


这个测试套件包含两个测试:`test1`(包含三个断言)和 `test2`(包含一个)。要运行测试,你只需运行程序;你不需要创建 MyClass 的实例。你将看到结果报告,显示有两个测试,三个断言和一个失败:

  1. Failure:
    test1(MyTest) [C:/bookofruby/ch18/test1.rb:19]:
    <101> expected but was
    <100>.

2 tests, 3 assertions, 1 failures, 0 errors


事实上,我做出了 *四个* 断言。然而,在测试失败之后的断言不会被评估。在 `test1` 中,这个断言失败了:

assert_equal(101, t.getVal)


失败后,下一个断言将被跳过。如果我现在纠正这个失败的断言(断言 100 而不是 101),下一个断言也将被测试:

assert(100 != t.getVal)


但这也失败了。这次,报告指出有四个断言被评估,其中有一个失败:

2 tests, 4 assertions, 1 failures, 0 errors, 0 skips


当然,在现实生活中的情况下,你应该努力编写正确的断言,并且当报告任何失败时,应该重写失败的代码而不是断言!

对于一个稍微复杂一些的测试示例,请参阅*test2.rb*程序(需要名为*buggy.rb*的文件)。这是一个小型冒险游戏,包括以下测试方法:

*test2.rb*

def test1
@game.treasures.each{ |t|
assert(t.value < 2000, "FAIL: #{t} t.value = #{t.value}" )
}
end

def test2
assert_kind_of( TestMod::Adventure::Map, @game.map)
assert_kind_of( Array, @game.map)
end


在这里,第一个方法`test1`对一个传递给块的对象数组执行`assert`测试,当`value`属性不小于 2,000 时失败。第二个方法`test2`使用`assert_kind_of`方法测试两个对象的类类型。在这个方法的第二个测试中,当`@game.map`被发现是`TestMod::Adventure::Map`类型而不是断言的`Array`类型时失败。

代码还包含两个名为`setup`和`teardown`的额外方法。当定义了这些名称的方法时,它们将在每个测试方法之前和之后运行。换句话说,在*test2.rb*中,以下方法将按此顺序运行:

| 1\. `setup` | 2\. `test1` | 3\. `teardown` |
| --- | --- | --- |
| 4\. `setup` | 5\. `test2` | 6\. `teardown` |

这给了你在运行每个测试之前重新初始化任何变量到特定值的机会,或者在这个例子中,重新创建对象以确保它们处于已知状态,如下面的例子所示:

def setup
@game = TestMod::Adventure.new
end

def teardown
@game.endgame
end


深入挖掘

本节包含单元测试断言的摘要,解释了为什么 IRB 可能对看似相同的代码显示不同的结果,并考虑了更高级调试工具的优点。

单元测试可用的断言

下面的断言列表提供方便参考。每个断言的完整解释超出了本书的范围。您可以在[`ruby-doc.org/stdlib/`](http://ruby-doc.org/stdlib/)找到测试库的完整文档。在这个网站上,选择`Test::Unit::Assertions`的类列表,以查看可用断言的完整文档以及许多演示它们用法的代码示例。

**`assert(boolean, message=nil)`**

断言布尔值不是 false 或 nil。

**`assert_block(message="assert_block failed.") {|| ...}`**

所有其他断言基于的断言。如果块返回 true 则通过。

**`assert_equal(expected, actual, message=nil`**`)**

如果`expected`等于`actual`则通过。

**`assert_in_delta(expected_float, actual_float, delta, message="")`**

如果`expected_float`和`actual_float`在`delta`容差内相等则通过。

**`assert_instance_of(klass, object, message="")`**

如果对象`.instance_of?` klass 则通过。

**`assert_kind_of(klass, object, message="")`**

如果对象`.kind_of?` klass 则通过。

**`assert_match(pattern, string, message="")`**

如果字符串`=˜`模式则通过。

**`assert_nil(object, message="")`**

如果对象是`nil`则通过。

**`assert_no_match(regexp, string, message="")`**

如果正则表达式 `!˜` 字符串,则通过。

**`assert_not_equal(expected, actual, message="")`**

如果预期 `!=` 实际,则通过。

**`assert_not_nil(object, message="")`**

如果 `!` 对象 .nil?,则通过。

**`assert_not_same(expected, actual, message="")`**

如果 `!` 实际 `.equal?` 预期,则通过。

**`assert_nothing_raised(*args) {|| ...}`**

如果块没有引发异常,则通过。

**`assert_nothing_thrown(message="", &proc)`**

如果块没有抛出任何东西,则通过。

**`assert_operator(object1, operator, object2, message="")`**

使用操作符比较 `object1` 和 `object2`。如果 `object1.send(operator, object2)` 为真,则通过。

**`assert_raise(*args) {|| ...}`**

如果块引发给定的任何一个异常,则通过。

**`assert_raises(*args, &block)`**

`assert_raise` 的别名。(在 Ruby 1.9 中已弃用,并在 2.0 中将被删除。)

**`assert_respond_to(object, method, message="")`**

如果对象 `.respond_to?` 方法,则通过。

**`assert_same(expected, actual, message="")`**

如果实际 `.equal?` 预期(换句话说,它们是同一个实例),则通过。

**`assert_send(send_array, message="")`**

如果方法发送返回一个真值,则通过。

**`assert_throws(expected_symbol, message="", &proc)`**

如果块抛出 `expected_symbol`,则通过。

**`build_message(head, template=nil, *arguments)`**

构建一个失败消息;在模板之前添加标题,`*arguments` 位置替换模板中的问号。

**`flunk(message="Flunked")`**

`flunk` 总是失败。

换行符很重要

我之前说过,在交互式 Ruby 控制台(IRB)中输入换行符时需要小心,因为换行符的位置可能会改变 Ruby 代码的含义。所以,例如,以下:

*linebreaks.rb*

x = ( 10 +
( 2 * 4 ) )


将 `18` 分配给 `x`,但以下:

x = (10

  • (2*4))

将 `8` 分配给 `x`。

这不仅仅是 IRB 的一个怪癖。这是 Ruby 代码的正常行为,即使是在文本编辑器中输入并执行时也是如此。第二个例子只是展示了在第一行上评估 `10`,认为这是一个完全可接受的价值,并立即忘记它;然后它评估 `+ (2*4)`,它也认为这是一个可接受的价值(`8`),但它与前面的值(`10`)没有关联,所以 `8` 被返回并分配给 `x`。

如果你想让 Ruby 评估跨越多行的表达式,忽略换行符,你可以使用行续行符(`\`)。这就是我在这里所做的事情:

x = (10 \

  • (2*4) )

这次,`x` 被赋予了值 `18`。

图形调试器

对于严重的调试,我强烈推荐使用图形调试器。例如,Ruby In Steel IDE 中的调试器允许您通过单击编辑器的边缘来设置断点和观察点。

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860166.png)

它允许你在单独的浮动窗口中监控所选的“监视变量”或所有局部变量的值。它维护一个指向当前执行点的所有方法调用的“调用栈”,并允许你通过调用栈“向后导航”以查看变量的变化值。它还具有变量的“钻取”展开功能,允许你展开数组、散列并查看复杂对象内部。这些功能远远超出了标准 Ruby 调试器的功能。有关 Ruby IDE 的信息,请参阅附录 D。


# 第十九章。Ruby on Rails

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860138.png.jpg)

Rails 已经与 Ruby 结合得如此紧密,以至于现在人们谈论编程“在” Ruby on Rails 时,好像“Ruby on Rails”就是编程语言的名字。

实际上,Rails 是一个框架——一组工具和代码库,可以与 Ruby 一起使用。它使您能够开发响应用户交互的数据库驱动型网站。例如,用户可以在一个页面上输入和保存数据,在其他页面上搜索数据。这使得 Rails 适合创建动态网站,这些网站可以“即时”生成网页,而不是加载静态、预先设计的页面。典型应用包括协作网站,如在线社区、多作者书籍或维基百科、购物网站、讨论论坛和博客。

我将很快提供创建博客的实战指南。首先,让我们更详细地了解一下 Rails 框架的细节。

### 注意

本章将为您展示在 Ruby on Rails 中开发的感觉。然而,请注意,Rails 是一个庞大且复杂的框架,我只会介绍其基本功能。在撰写本文时,Rails 3 是最新版本,但 Rails 2 仍然被广泛使用;因此,本章将涵盖这两个版本。

# 安装 Rails

Rails 不是 Ruby 的标准部分,因此您可能需要将其作为单独的操作安装。请注意,Ruby 和 Rails 在某些操作系统(如 Mac OS X)中作为标准软件提供,但无法保证这些是最新版本,您可能需要手动更新默认安装。

## DIY ...

安装 Rails 有多种方式。最简单的方法是使用一站式安装程序(本章中描述了一些替代方案)。然而,您也可以逐个安装 Rails 和它所需的工具。Rails 可以使用 Ruby Gems “软件包管理器”进行安装。只要您连接到互联网,Ruby Gems 就会在线查找并安装 Rails 的最新版本。

### 注意

要获取 Ruby Gems 文档,请访问 [`docs.rubygems.org/`](http://docs.rubygems.org/)。

在命令提示符下,输入以下内容:

gem install rails


如果您不想安装最新版本,而是想安装特定版本的 Rails,您应该在输入上一条命令时附加 `--version=` 后跟适当的版本号。例如,要安装 Rails 2.3.8,请输入以下内容:

gem install rails --version=2.3.8


或者,你可以从 Ruby on Rails 网站 [`www.rubyonrails.org/`](http://www.rubyonrails.org/) 下载并安装 Rails。大多数 Rails 应用程序都需要一个数据库,你将需要单独安装它。许多人使用 SQLite 或免费的 MySQL 数据库服务器。MySQL 是这两个系统中功能更强大的一个,被许多专业网站使用。然而,许多人发现 SQLite 对于 Ruby 应用的本地开发来说更容易使用。SQLite 的安装根据所使用的操作系统而有所不同。SQLite3 预安装在 Mac OS X Leopard 上。

在 Windows 上安装 SQLite3 可能相当棘手。你应该首先在命令行中运行以下命令:

gem install sqlite3-ruby


仔细注意执行时显示的消息。这会告诉你需要安装哪个版本的 SQLite3 DLL 以及你可以从中下载它的网址。这个 DLL 是必需的。如果没有安装它,SQLite3 将无法与 Rails 一起使用。此消息将类似于以下内容:

You've installed the binary version of sqlite3-ruby.
It was built using SQLite3 version 3.7.3.
It's recommended to use the exact same version to avoid potential issues.

At the time of building this gem, the necessary DLL files where available
in the following download:

http://www.sqlite.org/sqlitedll-3_7_3.zip

You can put the sqlite3.dll available in this package in your Ruby bin
directory, for example C:\Ruby\bin


一定要按照说明操作,下载正确的 DLL,并将其复制到 Ruby 安装下的 *\bin* 目录中。

请参考 SQLite 网站,获取有关 SQLite 的更多信息:[`www.sqlite.org/docs.html`](http://www.sqlite.org/docs.html). 你可以在本书的附录 B(apb.html "附录 B. 为 Ruby on Rails 安装 MySQL")中找到有关 MySQL 安装的说明。还可以使用许多其他数据库服务器,包括 Microsoft SQL Server、PostgreSQL 和 Oracle。

### 注意

如果你计划从头开始安装或更新 Rails,或者如果你需要更新操作系统预先安装的版本,你应该参考 Rails 指南网站:[`guides.rubyonrails.org/getting_started.html`](http://guides.rubyonrails.org/getting_started.html). 这些指南提供了有关 Rails 3 的详细操作系统特定信息。几个 Rails 维基也提供了有关支持旧版 Rails 的信息——例如,[`en.wikibooks.org/wiki/Ruby_on_Rails`](http://en.wikibooks.org/wiki/Ruby_on_Rails)。

## 或者使用“一体化”安装程序

可用的各种 Ruby 和 Rails 一体化设置程序包括针对 Windows、Linux 和 Mac 的 Bitnami RubyStack 安装程序:[`www.bitnami.org/stack/rubystack/`](http://www.bitnami.org/stack/rubystack/). Windows 用户还可以使用来自 RubyForge 的 Rails 安装程序:[`www.rubyforge.org/frs/?group_id=167`](http://www.rubyforge.org/frs/?group_id=167). 这些安装程序提供了它们自己的安装指南。

# 模型-视图-控制器

Rails 应用分为三个主要区域:模型、视图和控制器。简单来说,模型是数据部分——数据库以及在该数据上执行的所有程序性操作(如计算)。视图是最终用户看到的内容;在 Rails 术语中,这通常意味着浏览器中出现的网页。控制器是编程逻辑——将模型与视图连接在一起的“胶水”。

模型-视图-控制器(MVC)方法被各种编程语言和框架以各种形式使用。它在深入挖掘中的深入挖掘部分有更详细的描述。为了简洁起见,我将从此称其为 MVC。

# 第一个 Ruby on Rails 应用

不再赘述,让我们开始用 Rails 编程。我将假设你已经安装了 Rails,以及一个网络服务器。我恰好使用的是 WEBrick 服务器,它是随 Rails 标准安装的,但你也可以使用其他服务器,如 Apache、LightTPD 或 Mongrel。你可以在附录 D 中找到有关网络服务器的更多信息。

### 注意

一个网络服务器是一个使用超文本传输协议(HTTP)提供数据(如网页)的程序。你不需要理解它是如何工作的。你只需要知道,你需要一个网络服务器来与 Rails 一起使用。

本章假设你只使用“原始”Rails 开发工具——从命令行执行的程序——以及至少一个文本编辑器和网络浏览器;因此,你会发现你经常需要在系统提示符下输入命令。如果你使用的是 Rails 的集成开发环境,你可能会发现使用 IDE 提供的工具可以更容易地完成这些任务。

与本书其他章节提供的源代码示例不同,本章中 Ruby on Rails 应用的示例代码并不完整,也不是“可直接运行”的。这有三个原因:

+   每个 Rails 应用都包含大量文件和文件夹,其中大部分是由 Rails 自动生成的,因此分别分发它们是没有意义的。

+   我还必须为每个数据库提供数据,你必须在使用之前导入它。导入数据库通常比创建自己的数据库更困难。

+   不仅自己创建 Rails 应用更简单,而且这样做也有助于你理解 Rails 是如何工作的。然而,我已经提供了一些示例文件——一个完整应用的组成部分——你可以用它们来比较你的代码,以防你遇到问题。

# 创建一个 Rails 应用

为了简化,这个第一个应用将完全不使用数据库。这将让你在不必担心模型复杂性的情况下探索视图和控制器。

首先,打开系统提示符(在 Windows 中,选择开始菜单,并在运行或搜索框中输入 **`cmd`**)。导航到你打算放置 Rails 应用程序的目录。让我们假设这是 *C:\railsapps*。检查 Rails 是否已安装,并且其主目录是否在系统路径上。为此,请输入以下内容:

rails


如果一切顺利,你现在应该能看到关于使用 `rails` 命令的帮助信息的一屏内容。如果你没有看到这个,那么你的 Rails 安装可能存在问题,你需要修复它才能继续。请参考安装 Rails 中的安装 Rails。

### 注意

当 Rails 2 和 Rails 3 的命令或代码存在差异时,文本中会在示例旁边用 Rails 版本号(Rails 2 或 Rails 3)来指明。

假设 Rails 正在运行,你现在可以创建一个应用程序。输入以下命令:

*Rails 3*

rails new helloworld


*Rails 2*

rails helloworld


在你的硬盘嗡嗡作响了一段时间后,你应该会看到 Rails 刚刚创建的文件列表(实际的列表相当长,并且 Rails 2 和 Rails 3 中创建的一些项是不同的):

create app
create app/controllers/application_controller.rb
create app/helpers/application_helper.rb
create app/mailers
create app/models
create app/views/layouts/application.html.erb
create config
(etcetera)


使用你的计算机文件管理器查看这些文件。在运行 Rails 命令的目录(*\helloworld*)下,你会看到已经创建了几个新的目录:*\app*,*\config*,*\db* 等等。其中一些有子目录。例如,*\app* 目录包含 *\controllers*,*\helpers*,*\models* 和 *\views*。*\views* 目录本身还包含一个子目录,*\layouts*。

Rails 应用程序的目录结构远非随机;目录(或*文件夹*)以及它们包含的文件名称定义了应用程序各个部分之间的关系。背后的想法是,通过采用一个定义良好的文件和文件夹结构,你可以避免编写大量的配置文件来连接应用程序的各个部分。Rails 默认目录结构的简化指南可以在深入挖掘中的深入挖掘找到。

现在,在系统提示符下,将目录更改为新创建的 Rails 应用程序的最高级文件夹(*\helloworld*)。假设你仍然在 *C:\railsapps* 目录中,并且你将 Rails 应用程序命名为 *helloworld*,如之前建议的,你会在 Windows 中输入以下命令来切换到该目录:

cd helloworld


现在运行服务器。如果你(像我一样)使用 WEBrick,你应该输入以下命令:

*Rails 3*

rails server


*Rails 2*

ruby script/server


注意,除了 WEBrick 之外的服务器可能以不同的方式启动,如果前面的方法不起作用,你需要查阅服务器的文档。你现在应该看到以下类似的内容:

=> Booting WEBrick...
=> Rails application started on http://0.0.0.0:3000
=> Ctrl-C to shutdown server; call with --help for options
[2006-11-20 13:46:01] INFO WEBrick 1.3.1
[2006-11-20 13:46:01] INFO ruby 1.8.4 (2005-12-24) [i386-mswin32]
[2006-11-20 13:46:01] INFO WEBrick::HTTPServer#start: pid=4568 port=3000


问题?

如果您没有看到确认服务器已启动的消息,而是看到错误消息,请检查您是否已按照所使用的 Rails 版本准确输入了服务器命令,并检查它是否在适当的目录中运行:*\helloworld*。

如果您仍然有问题,可能是默认端口(3000)已经被占用——例如,如果您在同一台 PC 上已经安装了 Apache 服务器。在这种情况下,尝试使用其他值,例如 `3003`,在运行脚本时将此数字放在 `-p` 之后:

rails server -p3003


ruby script/server -p3003


如果您看到包含文本 `no such file to load -- sqlite3` 的错误消息,请确保您已正确安装 SQLite3,如 安装 Rails 中所述。如果您正在尝试使用 MySQL,并且错误消息包含文本 `no such file to load–mysql`,请参阅 附录 B。

*Rails 3*

*Rails 2*

现在,打开一个网页浏览器。将主机名和冒号以及端口号输入其地址栏中。主机名应该是(通常)*localhost*,端口号应该与启动服务器时使用的端口号匹配,否则默认为 3000。以下是一个示例:

http://localhost:3000/


浏览器现在应该显示一个欢迎您加入 Rails 的页面。如果没有,请确认您的服务器正在运行在 URL 中指定的端口上。

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/bk-rb/img/httpatomoreillycomsourcenostarchimages860169.png.jpg)

# 创建控制器

如前所述,控制器是您大部分 Ruby 代码存放的地方。它是应用程序中位于视图(浏览器中显示的内容)和模型(数据发生的事情)之间的部分。因为这是一个“Hello world”应用程序,让我们创建一个控制器来显示“hello”。本着原创精神,我将称之为 *SayHello* 控制器。再次提醒,您可以通过在系统提示符下运行脚本来创建它。您需要打开另一个命令窗口,该窗口位于您之前运行服务器脚本的目录中(例如,*C:\railsapps\helloworld*)。您不能重复使用现有的命令窗口,因为服务器正在该窗口中运行,您需要关闭它才能回到提示符——这将停止您的 Rails 应用程序工作!

在提示符下,输入以下内容(请确保使用与 `SayHello` 显示的相同的大小写):

*Rails 3*

rails generate controller SayHello


*Rails 2*

ruby script/generate controller SayHello


几分钟后,您将被告知已创建各种文件和目录,包括以下内容:

app/views/say_hello
app/controllers/say_hello_controller.rb
test/functional/say_hello_controller_test.rb
app/helpers/say_hello_helper.rb


### 注意

`generate controller` 脚本还在 Rails 3 中创建了 *application_controller.rb* 文件,或在 Rails 2 中创建了 *application.rb* 文件,这是整个应用程序的控制器,还有一个文件夹,*/views/say_hello*,您将很快使用到它。

注意 Rails 如何将名称 `SayHello` 解析为两个由下划线分隔的小写单词 `say` 和 `hello`,并使用此名称作为生成 Ruby 文件(如 `say_hello_controller.rb`)的第一部分。这只是 Rails 使用“约定配置”方法的一个例子。

定位控制器文件 *say_hello_controller.rb*,该文件已创建在 *\helloworld\app\controllers* 中。在文本编辑器中打开此文件。此空方法已被自动生成:

class SayHelloController < ApplicationController
end


在这个类中,你可以编写一些代码,当显示某个页面时执行。编辑类定义以匹配以下内容:

class SayHelloController < ApplicationController
def index
render :text => "Hello world"
end

def bye
    render :text => "Bye bye"
end

end


现在包含两个方法,`index` 和 `bye`。每个方法包含一行代码。尽管我省略了括号(许多 Rails 开发者喜欢轻量级的括号风格),但你可能推断出 `render` 是一个接受哈希作为参数的方法;该哈希本身包含一个由符号和字符串组成的键值对。对于喜欢括号的开发者,`index` 方法可以重写如下:

def index
render( { :text => "Hello world" } )
end


有了你的第一个真正的 Rails 应用程序。要尝试它,你需要回到网页浏览器并输入你刚刚编写的两个函数的完整“地址”。但在做之前,你可能需要重新启动服务器。只需在运行服务器的命令窗口中按 ctrl-C。当服务器退出后,通过输入以下内容重新启动:

*Rails 3*

rails server


*Rails 2*

ruby script/server


在 Rails 3 中,你还需要做一件事。你需要告诉 Rails 如何通过在网页浏览器中输入的地址找到“路由”。这一步在 Rails 2 中不是必需的。在 Rails 3 中,打开 *helloworld\config\* 文件夹中的 *routes.rb* 文件。现在编辑它以匹配以下内容(或者简单地取消注释文件底部的代码行):

match ':controller(/:action(/:id(.:format)))'


你现在可以测试应用程序了。为此,你只需输入一个地址来访问控制器方法。地址的格式为主机和端口(与之前输入的相同——例如,*http://localhost:3000*),然后是控制器的名称(*/say_hello*),最后是特定方法的名称(*/index* 或 */bye*)。尝试将这些内容,如以下所示,输入到浏览器的地址栏中,同时确保如果你不是使用 3000 端口,请使用正确的端口号:

http://localhost:3000/say_hello/index
http://localhost:3000/say_hello/bye


你的浏览器应该分别显示“Hello world”和“Bye bye”对应于每个地址。如果到目前为止一切正常,你可以沉浸在你创建了第一个 Ruby on Rails 应用程序的温暖光辉中。然而,如果你看到 MySQL 数据库错误,请阅读 找不到数据库? 在 配置 MySQL 中,并在继续之前修复问题。

顺便提一下,Rails 使用 `index` 方法作为默认值,因此你可以使用 index 视图作为你的主页,并在将地址输入到浏览器时省略该部分:

http://localhost:3000/say_hello


# 简单 Rails 应用的解剖

在继续之前,让我们仔细看看你在本应用中创建的类。Rails 通过在运行控制器生成脚本时指定的名称后添加`*Controller*`来命名这个类(`HelloWorld`),并且使其成为`ApplicationController`类的后代:

class SayHelloController < ApplicationController


但`ApplicationController`类究竟是什么呢?你可能还记得,我提到你之前运行的`generate controller`脚本默默地创建了一个名为`*application_controller.rb*`(Rails 3)或`*application.rb*`(Rails 2)的文件,位于`/app/controllers`文件夹内。这个文件就是应用控制器,如果你打开它,你会看到它包含一个名为以下内容的类:

ApplicationController < ActionController::Base


因此,`SayHelloController`类继承自`ApplicationController`类,而`ApplicationController`类本身又是`ActionController`模块中`Base`类的后代。你可以通过回溯层次结构并要求每个类显示自己来证明这一点。顺便说一句,这也给你提供了一个机会,在`SayHelloController`类中尝试做一些真正的 Ruby 编程。

只需编辑`say_hello_controller.rb`文件的内容,使其与以下内容匹配(或者从本章代码存档中的`*sayhello1.rb*`文件复制粘贴代码):

`*sayhello1.rb*`

class SayHelloController < ApplicationController
def showFamily( aClass, msg )
if (aClass != nil) then
msg += "
#{aClass}"
showFamily( aClass.superclass, msg )
else
render :text => msg
end
end

def index
    showFamily( self.class, "Class Hierarchy of self..." )
end

end


要查看结果,请将以下地址输入到你的浏览器中(再次提醒,如果需要,请更改端口号):

http://localhost:3000/say_hello


你的网络浏览器现在应该显示以下内容(在 Rails 3 中):

Class Hierarchy of self...
SayHelloController
ApplicationController
ActionController::Base
ActionController::Metal
AbstractController::Base
Object
BasicObject


在 Rails 2 中,它将显示以下内容:

Class Hierarchy of self...
SayHelloController
ApplicationController
ActionController::Base
Object


不要担心实际的类继承关系;Rails 框架的内部实现细节并不立即引起兴趣。重要的是要理解,控制器是一个真正的 Ruby 对象,它从`ApplicationController`类及其祖先那里继承行为。你编写的任何 Rails 控制器类,或者通过运行脚本自动生成的控制器类,都可以包含正常的 Ruby 代码,就像你在前几章中编写的所有其他类一样。在控制器内部,你可以使用所有常见的 Ruby 类,如字符串和散列。

但请记住,最终结果需要在网页上显示。这带来了一些后果。例如,不要在字符串中放入换行符(`"\n"`),而应使用 HTML 段落(`<P>`)或换行(`<br />`)标签,并且每次页面显示时只能调用一次`render`方法,这也解释了为什么我在递归调用方法的过程中构建了一个字符串,然后在最后直接将这个字符串传递给`render`方法:

def showFamily( aClass, msg )
if (aClass != nil) then
msg += "
#{aClass}"
showFamily( aClass.superclass, msg )
else
render :text => msg
end
end


# 生成控制器脚本的总结

在继续之前,让我们总结一下运行 Rails `generate controller` 脚本的基本细节,并学习一些在创建视图时可以使用的额外技巧。每次生成新的控制器时,它都会在 *app/controllers* 目录中创建一个 Ruby 代码文件,文件名与输入的名称匹配,但全部小写,任何非首字母大写的名称前面都带有下划线,并且附加了 *_controller*。所以,如果你输入了 *SayHello*,控制器文件将被命名为 *say_hello_controller.rb*。控制器将包含一个类定义,例如 `SayHelloController`。你可以随后向这个类添加一些“视图方法”,例如 `index` 和 `bye`。或者,你可以使用 `generate` 脚本在执行脚本时通过包含那些视图名称来自动创建一个或多个空的视图方法。

例如,你可以运行以下脚本:

*Rails 3*

rails generate controller SayHello index bye


*Rails 2*

ruby script/generate controller SayHello index bye


Rails 现在将创建名为 *say_hello_controller.rb* 的文件,其中包含以下代码:

class SayHelloController < ApplicationController
def index
end

def bye
end

end


无论你是否指定了视图,都会在 */views* 目录下创建一个与控制器名称匹配的文件夹(*views/say_hello*)。实际上,脚本还会创建一些其他文件,包括 */helpers* 文件夹中的更多 Ruby 文件,但在我们的简单应用程序中,你可以忽略这些文件。

如果你在运行控制器脚本时指定了视图名称,将添加一些与名称匹配且扩展名为 *.html.erb* 的文件到相应的视图文件夹中。例如,如果你输入了以下命令:

ruby script/generate controller SayHello xxx


*/views/say_hello* 目录现在应该包含一个名为 *xxx.html.erb* 的文件。另一方面,如果你输入了以下命令:

ruby script/generate controller Blather xxx bye snibbit


*/views/blather* 目录现在应该包含三个文件:*xxx.html.erb*、*bye.html.erb* 和 *snibbit.html.erb*。

# 创建视图

你完全可以在控制器内部编写所有代码,并在视图方法内部进行所有格式化来创建整个应用程序。然而,你很快就会得到一些相当丑陋的网页。要应用更多格式化,你需要创建一个更精确定义网页布局的视图。

你可以将视图想象成一个当有人登录到特定网址时将显示的 HTML 页面——在这种情况下,视图的名称构成了地址的最后一部分,就像之前的例子中 */index* 和 */bye* 部分将你带到显示控制器中 `index` 和 `bye` 方法提供的数据的视图一样。

你可以创建与这些网页地址和相应方法名称匹配的 HTML 视图“模板”。使用 HTML(或纯文本)编辑器,在 *\app\views\say_hello* 目录中创建一个名为 *index.html.erb* 的文件,如果该模板尚未存在。记住,如前所述(在 The Generate Controller Script Summarized 中),在最初生成控制器时,你可以选择自动创建一个或多个视图模板。

现在你有了视图模板,你可以编辑它来控制网页中数据的显示方式。这意味着你将不再需要使用控制器中的 `render` 方法来显示未经格式化的文本。但是,由于视图不再受控制器控制(或者说,是这样),控制器如何将数据传递给视图呢?结果是,它可以通过将数据分配给实例变量来实现这一点。

编辑 *say_hello_controller.rb* 中的代码(或者删除它,并将源代码存档中提供的 *sayhello2.rb* 文件中的代码粘贴进来)以确保它符合以下要求:

*sayhello2.rb*

class SayHelloController < ApplicationController
def showFamily( aClass, msg )
if (aClass != nil) then
msg += "

  • #{aClass}
  • "
    showFamily( aClass.superclass, msg )
    else
    return msg
    end
    end

    def index
        @class_hierarchy = "<ul>#{showFamily( self.class, "" )}</ul>"
    end
    

    end

    
    这个版本调用 `showFamily()` 方法,以便在两个 HTML “无序列表” 标签 `<ul>` 和 `</ul>` 内部构建一个字符串。每次找到一个类名时,它就被放置在两个 HTML “列表项” 标签 `<li>` 和 `</li>` 之间。完整的字符串形成一个有效的 HTML 片段,而 `index` 方法只是将这个字符串分配给一个名为 `@class_hierarchy` 的变量。
    
    控制器中的 HTML 标签?
    
    一些 Ruby on Rails 开发者反对在控制器代码中包含任何 HTML 标签,无论它们多么 - 微不足道。在我看来,如果你打算在网页中显示最终结果,你放置的 `<p>`、`<ul>` 或 `<li>` 标签的位置几乎无关紧要。尽管 MVC 范式鼓励控制器程序代码和视图布局定义之间有很强的分离,但你不可避免地必须做出一些妥协——至少是将一些程序代码放入视图。避免在控制器中使用 HTML 标签在很大程度上是一种审美上的反对,而不是实际的反对。尽管如此(警告!),其他人对此有很强的看法。
    
    现在你需要做的就是找到一种方法将这个 HTML 片段放入一个完整的 HTML 页面中。这就是视图的作用所在。打开你刚刚创建的视图文件,*index.html.erb*,在 *app\views\say_hello* 文件夹中。根据 Rails 的命名约定,这是与 *say_hello_controller.rb* 文件配对的默认视图(“index”页面)。由于 Rails 根据文件、文件夹、类和方法名称来确定关系,因此你不需要按名称加载或要求任何文件,也不需要编写任何配置细节。
    
    在 *index.html.erb* 文件中,添加以下内容:
    
    

    This is the Controller's Class Hierarchy

    <%= @class_hierarchy %> ```

    第一行仅仅是普通的 HTML 格式化,它将 <h1></h1> 标签内的文本定义为标题。下一行更有趣。它包含变量 @class_hierarchy。回顾一下 say_hello_controller.rb 中的 index 方法,你会看到这是你分配字符串的变量。在这里的视图中,@class_hierarchy 被放置在两个看起来奇怪的定界符 <%=%> 之间。这些是特殊的 Rails 标签。它们用于嵌入在浏览器显示网页之前将执行的 Ruby 代码片段。最终显示的页面将是一个完整的 HTML 页面,包括视图模板中的任何 HTML 片段以及嵌入的 Ruby 代码执行后的结果。现在尝试一下,通过在浏览器中输入页面地址来试试:

    http://localhost:3000/say_hello/
    

    这应该现在以大号粗体字母显示标题“这是控制器的类层次结构”,后面跟着一个类列表,每个元素前面都有一个点。在 Rails 2 中,你会看到如下内容:

    • SayHelloController
    • ApplicationController
    • ActionController::Base
    • Object
    

    然而,在 Rails 3 中,你似乎遇到了一个问题。不是列表,HTML 标签被直接渲染,如下所示:

    <ul><li>SayHelloController</li><li>ApplicationController</
    li><li>ActionController::Base</li><li>ActionController::Metal</
    li><li>AbstractController::Base</li><li>Object</li><li>BasicObject</li></ul>
    

    这绝对不是你想要的。这种解释的原因是,Rails 2 和 Rails 3 之间字符串中嵌入的 HTML 标签的默认处理方式已经发生了变化。在 Rails 2 中,标签被未经修改地传递到视图中。在 Rails 3 中,进行了替换以确保 HTML 标签在屏幕上显示,而不是由浏览器渲染。例如,<li> 标签被更改为 &lt;li&gt;,其中 &lt;&gt; 是 HTML 代码中的尖括号 (<>)。为了确保 HTML 标签不以这种方式替换,你需要使用 raw 方法,并传递一个字符串参数。这是为 Rails 3 重写的 index.html.erb

    <h1>This is the Controller's Class Hierarchy</h1>
    <%= raw( @class_hierarchy ) %>
    

    现在当你登录到 Rails 3 的地址 localhost:3000/say_hello 时,你应该看到以无 HTML 标签显示的列表形式显示的类名。

    如果你想的话,可以通过在控制器中创建标题并将结果字符串分配给另一个变量来从视图文件中移除所有 HTML。你可以通过编辑 say_hello_controller.rb 中的 index 方法来实现这一点:

    def index
        @heading = "<h1>This is the Controller's Class Hierarchy</h1>"
        @class_hierarchy = "<ul>#{showFamily( self.class, "" )}</ul>"
    end
    

    然后编辑视图文件 (/app/views/say_hello/index.html.erb) 以匹配下面的代码(或者将代码从示例文件复制粘贴到 index.html.erb)以用于 Rails 3:

    say_hello_rails3.html.erb

    <%= raw( @heading ) %>
    <%= raw( @class_hierarchy ) %>
    

    对于 Rails 2,使用此代码:

    say_hello.html.erb

    <%= @heading %>
    <%= @class_hierarchy %>
    

    如果你这样做,网页上显示的最终结果将保持不变。所发生的一切只是将一些格式化从视图模板移动到了控制器中。

    Rails 标签

    你可以在 Rails HTML 嵌入式 Ruby (ERb) 模板文件中放置两种 Rails 标签的变体。你迄今为止使用的那些包括在开标签中的等号:<%=.

    这些标签不仅使 Rails 评估 Ruby 表达式,还将结果显示在网页上。如果你从开头的分隔符中省略等号,那么代码将被评估,但结果将不会显示:<%>

    注意

    ERb 文件包含 HTML 标记和 Ruby 代码的混合,这些代码位于<%=%>等标签之间。Rails 在最终页面在浏览器中显示之前处理这些文件,执行嵌入式 Ruby 并构建 HTML 页面。

    如果你想,你可以在<%%>标签之间放置相当长的代码块——甚至是你整个 Ruby 程序!——然后当你想在网页中显示某些内容时,使用<%=%>。实际上,你可以通过完全省略控制器并将所有内容放入视图来重写你的应用程序。尝试通过编辑*app/views/say_hello/index.html.erb*来匹配以下内容(或者根据所使用的 Rails 版本,从文件*embed_ruby_rails2.html.erb**embed_ruby_rails3.html.erb*中复制并粘贴代码):

    embed_ruby_rails3.rhtml

    <% def showFamily( aClass, msg )
         if (aClass != nil) then
            msg += "<li>#{aClass}</li>"
            showFamily( aClass.superclass, msg )
         else
           return msg
         end
       end %>
    
    <%= raw( "<ul>#{showFamily( self.class, "" )}</ul>" ) %>
    

    在这个特定的情况下,网页上显示的文本将与之前略有不同,因为它现在显示的是视图类的类层次结构,而不是控制器类的类层次结构。正如你将看到的,视图是从 ActionView::Base 类派生出来的。

    你也可以通过将单独的行放在<%%>标签之间来分割连续的代码块,而不是将整个块放在单个标签对中。这样做的好处是,它允许你将标准 HTML 标签放在 Ruby 代码的单独分隔行之外。例如,你可以将其放入一个视图中:

    <% arr = ['h','e','l','l','o',' ','w','o','r','l','d'] %>
    
    <% # sort descending from upper value down to nil
    reverse_sorted_arr = arr.sort{
        |a,b|
            b.to_s <=> a.to_s
        } %>
    
    <% i = 1 %>
    <ul>
    <% reverse_sorted_arr.each{ |item| %>
    <li><%= "Item [#{i}] = #{item}" %></li>
    <% i += 1 %>
    <% } %>
    </ul>
    

    在这里,我已经将一个字符数组分配给变量arr,并在一组标签之间编写了一个块来逆序排序数组,并将结果分配给另一组标签之间的另一个变量。然后我将 1 分配给变量i;最后,我编写了这个方法:

    reverse_sorted_arr.each{ |item|
        "Item [#{i}] = #{item}"
        i += 1
    }
    

    但是,我并没有将方法包含在单个<% %>标签对中,而是将每行代码分别包含在其自己的标签对中。我为什么要这样做呢?好吧,有两个原因。首先,我想在块中间显示字符串,所以我在那里需要使用<%=标签:

    <%= "Item [#{i}] = #{item}" %>
    

    其次,我想将整个字符串集显示为一个 HTML 列表。因此,我在 Ruby 代码块前后放置了<ul></ul>标签,并将显示每个数组项的代码行放在<li></li>标签内。请注意,这些标签在 Ruby 代码块内部,但在特定行的嵌入式 Ruby 标签外部:

    <li><%= "Item [#{i}] = #{item}" %></li>
    

    因此,通过将连续的 Ruby 代码块分割成单独分隔的行,我就不再被迫构建包含 HTML 标签的字符串。相反,我能够做到将 HTML 混合到 Ruby 代码本身中的有用技巧。说实话,我并没有真正混合它们——Ruby 代码仍然封闭在标签内;我所做的是告诉 Rails 在网页浏览器显示页面之前在特定点混合 HTML。

    顺便说一句,你可能觉得将所有嵌入的 Ruby 代码放入视图(index.html.erb)的应用程序版本与之前版本进行比较很有趣,在之前的版本中,所有的代码都放入了控制器(在示例文件sayhello2.rb中提供的say_hello_controller.rb版本)中,只有一小部分嵌入的 Ruby(几个变量)被放入了视图:

    <%= @heading %>
    <%= @class_hierarchy %>
    

    你可能会同意,第一个版本,其中编程逻辑被放入控制器而不是嵌入到视图中,更为整洁。总的来说,Ruby 代码属于 Ruby 代码文件,HTML 格式化属于 HTML 文件。尽管嵌入的 Ruby 提供了一种让视图和控制器通信的简单方法,但通常最好将嵌入的 Ruby 代码保持简短和简单,并将更复杂的 Ruby 代码放入 Ruby 代码文件中。

    让我们创建一个博客吧!

    对于许多人来说,真正让他们对 Ruby on Rails 产生兴趣的是 Rails 的创造者 David Heinemeier Hansson 提供的 20 分钟演示,他展示了如何创建一个简单的博客。这个演示最初是用 Rails 1 完成的,后来已经更新(并且有所改变)以适应 Rails 2 和 Rails 3。你可以在www.rubyonrails.com/screencasts/上观看最新的演示。

    博客是一个很好的方式来展示使用 Rails 创建一个相当复杂的应用程序是多么容易。在本章的剩余部分,我将解释如何创建一个非常简单的博客应用程序。我将使用一个名为migrations的功能,这将减少创建模型数据库结构的许多繁琐工作。

    请记住,我已经尽量使这个应用程序的创建尽可能简单。它并不是 David Heinemeier Hansson 的教程的精确复制,它只包含了一个功能齐全的博客的子集功能(例如,没有用户评论和没有管理界面)。一旦你完成了我的博客应用程序,你可能想研究前面提到的屏幕录像教程。这些教程将展示如何以不同的方式产生类似的结果,并且它们也会帮助你进一步学习如何创建一个更复杂的博客。

    注意

    您可以将您的博客应用程序的代码与我创建的代码进行比较。我的代码包含在本章代码的 \blog 子目录中。然而,这个博客应用程序并不是“可直接运行”的,因为它需要一个您必须创建的数据库。您应按照章节中给出的说明创建自己的博客应用程序。您可以使用提供的代码作为参考,以检查您创建的文件是否与我创建的文件匹配。

    在您保存 Rails 应用程序的目录中打开一个命令提示符(例如,C:\railsapps),并执行一个创建名为 Blog 的应用程序的命令:

    Rails 3

    rails new blog
    

    Rails 2

    rails blog
    

    创建数据库

    现在让我们创建一个数据库。这里我假设您正在使用 SQLite3 或 MySQL 数据库。如前所述,SQLite3 被认为是 Rails 3 本地开发的标准化数据库系统,它更容易设置和使用。另一方面,MySQL 是一个行业标准数据库,更可能用于在网站上部署。如果您使用 SQLite3,您不需要采取任何特殊行动来创建数据库——Rails 会为您完成。您可以直接跳转到 Scaffolding 在 创建 MySQL 数据库。如果您使用 MySQL,您应遵循下一节中概述的步骤。

    创建 MySQL 数据库

    如果您正在使用 MySQL,通过从 MySQL 程序组中运行 MySQL 命令行客户端来打开一个 MySQL 提示符。当提示输入时,输入您的 MySQL 密码。现在您应该看到以下提示:

    mysql>
    

    在提示符下输入以下内容(确保在末尾放置分号):

    create database blog_development;
    

    MySQL 应该回复“Query OK”以确认数据库已创建。现在请确保您的 Rails 应用程序的数据库配置文件包含开发数据库的适当条目。如果您使用其他数据库(不是 MySQL),您的配置条目必须引用该数据库。

    前往 Rails 创建的新博客应用程序所在的文件夹,并在 \config* 子目录中打开 database.yml 文件。假设您正在使用 MySQL,将 mysql 作为适配器,localhost* 作为主机,您的 MySQL 用户名(例如,root),以及如果您有的话,您的密码。数据库名称应与您刚刚创建的数据库相匹配。以下是一个示例(您将输入实际的密码而不是 mypassword):

    development:
      adapter: mysql
      host: localhost
      username: root
      database: blog_development
      password: *`mypassword`*
    

    注意

    如果在您修改 database.yml 时服务器正在运行,您应该在之后重新启动服务器!

    通常会有多个配置——例如,用于开发、测试和生产。为了简单起见,这里您将只创建一个开发配置;您可以在 database.yml 中注释掉任何其他条目。

    框架

    你将使用一个名为脚手架的功能来一次性创建模型、视图和控制器。脚手架是一种快速将简单应用程序搭建起来的便捷方式。进入新的\blog目录,并在系统提示符下输入以下内容:

    Rails 3

    rails generate scaffold post title:string body:text created_at:datetime
    

    Rails 2

    ruby script/generate scaffold post title:string body:text created_at:datetime
    

    这告诉脚手架生成器创建一个由 Ruby 代码组成的模型,以访问名为post的数据库表,该表有三个列,titlebodycreated_at,每个列后面都指定了数据类型(stringtextdatetime)。为了根据此模型创建数据库结构,你需要运行一个“迁移”来更新数据库表本身。

    迁移

    脚本已经为你创建了一个数据库迁移文件。导航到\db\migrate目录。你会看到这个目录包含一个以_create_posts.rb结尾的编号迁移文件。如果你打开这个文件,你可以看到表结构是如何用 Ruby 代码表示的:

    def self.up
        create_table :posts do |t|
            t.string :title
            t.text :body
            t.datetime :created_at
    
            t.timestamps
        end
    end
    

    一个应用程序可能会随着时间的推移而获得许多迁移,每个迁移都包含有关模型特定迭代的详细信息——对数据库表结构的更改和添加。经验丰富的 Rails 开发者可以使用迁移来选择性地激活模型的不同的版本。然而,在这里,你将使用这个迁移来创建数据库的初始结构。

    在你的应用程序主目录的系统提示符下(例如,/blog),你可以使用rake工具来运行迁移。输入以下命令:

    rake db:migrate
    

    几分钟后,你应该会看到一个消息,说明rake任务已完成,并且 CreatePosts 已迁移。

    部分内容

    现在,让我们创建一个新的部分视图模板。一个部分是网页模板的一个片段,Rails 可以在运行时将其插入一个或多个完整的网页中。例如,如果你计划在你的网站上多个页面上使用相同的数据输入表单,你可以在部分模板中创建该表单。部分模板的名称以下划线开头。

    MySQL 错误?

    如果你使用 MySQL,并且在运行rake时看到错误,首先验证 MySQL 是否已安装,如附录 B 中所述。还要注意任何以类似以下内容开始的错误消息:

    rake aborted!
    !!! Missing the mysql gem. Add it to your Gemfile: gem 'mysql', '2.8.1'
    

    如果你看到这个,你需要将指定的条目添加到一个名为Gemfile的文件中,该文件位于你的应用程序顶级目录中(例如,\blog)。例如,根据前面的消息,你需要将以下文本添加到Gemfile*中:

    gem 'mysql', '2.8.1'
    

    在你的\app\views\posts*目录下创建一个名为_post.html.erb的新文件。打开这个文件,并编辑其内容以匹配以下内容(或者你也可以从源代码存档中的示例项目中复制_post.html.erb*):

    _post.html.erb

    <div>
    <h2><%= link_to post.title, :action => 'show', :id => post %></h2>
    <p><%= post.body %></p>
    <p><small>
    <%= post.created_at.to_s %>
    </small></p>
    </div>
    

    保存你的更改。然后打开名为show.html.erb的文件。此文件是由 scaffold 脚本自动创建的。从文件中删除以下“样板”代码:

    <b>Title:</b>
     <%=h @post.title %>
    </p>
    
    <p>
     <b>Body:</b>
     <%=h @post.body %>
    </p>
    
    <p>
     <b>Created at:</b>
     <%=h @post.created_at %>
    </p>
    

    并将其替换为以下内容:

    <%= render :partial => "post", :object => @post %>
    

    这告诉 Rails 在此处渲染_post部分模板。现在show.html.erb中的代码应该看起来像这样:

    <%= render :partial => "post", :object => @post %>
    
    <%= link_to 'Edit', edit_post_path(@post) %> |
    <%= link_to 'Back', posts_path %>
    

    测试它!

    就这样!现在你已经准备好测试你的应用程序了。首先,运行服务器。在\blog目录的提示符下,输入以下内容:

    Rails 3

    rails server
    

    Rails 2

    ruby script/server
    

    注意

    回想一下,如果你没有使用默认端口 3000,你将需要在-p之后指定实际的端口号,如本章前面所述,例如:rails server -p3003

    进入你的网络浏览器,并输入以下地址(如果这不是 3000 端口,请使用实际的端口号):

    http://localhost:3000/posts
    

    你应该看到你的页面,其索引页面处于激活状态。这应该是这样的:

    无标题图片

    现在点击“新建帖子”链接。在新帖子页面,输入标题和一些正文内容。然后点击创建

    无标题图片

    下一个显示的页面是显示页面。这是由show.html.erb视图和_post.html.erb部分组合定义的。现在继续输入帖子并点击链接在定义的各个视图中导航。

    无标题图片

    注意

    如前所述,本章假设你正在使用 Rails“原生”方式,通过在系统提示符中输入所有必要的命令。一些 IDE 提供了更集成的环境,允许你使用内置的工具和实用程序生成和编写应用程序。你将在附录 D 中找到一些 Ruby 和 Rails IDE 的概述。

    深入挖掘

    “MVC”这三个字母对于理解 Rails 的工作原理至关重要。在这里,我将解释其基本概念。你还将了解 Rails 的目录结构和替代的 Ruby 框架。

    MVC

    如前所述,Rails 采用模型-视图-控制器(MVC)范式。简单来说,这些可以被认为是数据库(模型)、显示(视图)和编程逻辑(控制器)。

    虽然这三个组成部分在理论上可以是独立的实体,但在实践中不可避免地会有一定程度的重叠。例如,一些计算可能在模型中完成,而其他计算可能在控制器中完成;影响数据格式化的操作可能发生在控制器或视图中。没有硬性规定——只有一条基本原则,即尽可能让“接近数据”的操作发生在模型中,让“接近显示”的操作发生在视图中,其余的都放入控制器中。

    这就是理论上的 MVC。现在让我们看看 Rails 是如何实现的。

    模型

    Ruby on Rails 中的模型是数据库中的表(由 MySQL 等数据库服务器处理)和匹配的 Ruby 类的组合,用于操作这些表。例如,在一个博客中,你可能有一个包含名为 Posts 的表的数据库。在这种情况下,Rails 模型也会包含一个名为 Post 的 Ruby 类(注意 Rails 使用复数——Posts 表可以包含多个 Post 对象)。Ruby Post 类通常会包含查找、保存或从 Posts 数据库加载单个 Post 记录的方法。数据库表和相应的 Ruby 类的组合构成了一个 Rails 模型。

    视图

    视图基本上就是它的名字——Ruby on Rails 应用程序的视觉表示。它通常是(但不一定是)以 HTML 模板的形式创建的,其中混合了一些 Ruby 代码。实际上,其他视图类型(例如,使用 Adobe 的 Flex 或 Microsoft 的 Silverlight 制作的图形视图)也是可能的,但 Rails 的默认值是 HTML。这些模板通常具有 .html.erb 扩展名(但也可以使用 Rails 1 的默认扩展名 .rhtml),它们不是直接加载到网页浏览器中的——毕竟,网页浏览器没有运行 Ruby 代码的方法。相反,它们由一个单独的工具预处理,该工具执行 Ruby 代码以与模型交互(根据需要查找或编辑数据);然后,作为最终结果,它创建一个新的 HTML 页面,其基本布局由 ERb 模板定义,但其实际数据(即博客帖子、购物车项目或 whatever)由模型提供。这使得创建高度动态的网页成为可能,这些网页会随着用户与之交互而改变。

    控制器

    控制器以 Ruby 代码文件的形式存在,充当连接模型和视图的中介。例如,在网页(视图)中,用户可能会点击一个按钮来向博客添加一篇新帖子;使用普通的 HTML,这个按钮会提交一个名为 Create 的值。这会导致一个名为 create 的方法在 post “控制器”(Ruby 代码文件)中保存已输入网页(视图)中的新博客条目(一些文本)到数据库(模型的数据库存储库)中。

    Rails 文件夹

    这是对 Rails 生成的顶级文件夹的简化指南,包括它们包含的文件和文件夹的简要描述:

    app 这包含特定于该应用程序的代码。子文件夹可能包括 app\controllersapp\modelsapp\viewsapp\helpersapp\mailers
    config 这包含 Rails 环境的配置文件、路由图、数据库和其他依赖项;数据库配置放入 database.yml 文件中。
    db 这包含 schema.rb 中的数据库模式以及可能包含在数据库中工作的代码。如果已应用迁移,它还将包含在 \migrate 子目录中的迁移文件。
    doc 这可能包含 RDOC 文档(有关 RDOC 的更多信息,请参阅附录 A)。
    lib 这可能包含应用程序的代码库(即不属于\controllers\models\helpers的逻辑代码)。
    log 这可能包含错误日志。
    public 此目录包含可能由 Web 服务器使用的“静态”文件。它包含用于图像样式表JavaScript的子目录。
    script 这包含 Rails 用于执行各种任务的脚本,例如生成特定类型的文件和运行 Web 服务器。
    test 这可能包含 Rails 生成的或用户指定的测试。
    tmp 这包含 Rails 使用的临时文件。
    vendor 这可能包含不属于默认 Rails 安装的第三方库。

    其他 Ruby 框架

    Rails 可能是最著名的 Ruby 框架,但绝对不是唯一的一个。其他如 Ramaze 和 Sinatra 也有专门的追随者。一个名为 Merb 的框架曾被看作是 Rails 的最接近的竞争对手。然而,在 2008 年 12 月,Rails 和 Merb 团队宣布他们将合作开发 Rails 的下一个版本,正是这次合作导致了 Rails 3 的诞生。

    如果你对探索其他 Ruby 框架感兴趣,请遵循以下链接:

    请记住,开源 Ruby 框架有来有去,兴衰取决于核心开发者的热情或其他承诺。Ramaze 团队在其主页维基上维护了一个 Ruby 框架列表:wiki.ramaze.net/Home#other-frameworks

    第二十章:动态规划

    无标题图片

    在过去的 19 章中,我涵盖了 Ruby 语言的大量特性。有一件事我还没有详细讲解,那就是 Ruby 的动态规划能力。

    如果你只使用过非动态语言(比如 C 或 Pascal 家族中的语言),那么编程中的动态性可能需要一点时间来适应。在继续之前,我将明确说明我所说的动态语言的含义。实际上,这个定义有点模糊,并不是所有声称自己是动态语言的语言都共享所有相同的特性。然而,从一般意义上讲,一个提供某种方式在运行时修改程序的语言可以被认为是动态的。动态语言的另一个特点是它能够改变给定变量的类型——你在这本书中的例子中已经无数次这样做过了。

    也可以在 Ruby 这样的动态类型语言和静态类型语言(变量类型在声明时预先声明并固定)之间做出进一步的区分,例如 C、Java 或 Pascal。在本章中,我将专注于 Ruby 的自我修改能力。

    注意

    在形式化的计算机科学中,术语动态规划有时被用来描述解决复杂问题的一种分析方法。但这并不是本章中术语所用的含义。

    自修改程序

    在大多数编译型语言和许多解释型语言中,编写程序和运行程序是两种完全不同的操作:你编写的代码是固定的,在程序运行时已经不可能进行任何进一步的修改。

    但 Ruby 并非如此。一个程序——我指的是Ruby 代码本身——在程序运行时可以被修改。甚至可以在运行时输入新的 Ruby 代码并执行新代码,而无需重新启动程序。

    将数据视为可执行代码的能力被称为元编程。你在这本书中一直在进行元编程,尽管是相当简单的一种。每次你在双引号字符串中嵌入一个表达式时,你就是在进行元编程。毕竟,嵌入的表达式并不是真正的程序代码——它是一个字符串——然而 Ruby 显然必须“将其转换为”程序代码才能对其进行评估。

    大多数时候,你可能会在双引号字符串中的#{}分隔符之间嵌入相当简单的代码片段。你可能会嵌入变量名,比如,或者数学表达式:

    str_eval.rb

    aStr = 'hello world'
    puts( "#{aStr}" )
    puts( "#{2*10}" )
    

    但你并不局限于这样的简单表达式。实际上,你可以将几乎所有内容嵌入到双引号字符串中。你甚至可以在字符串中编写整个程序。你甚至不需要使用printputs来显示最终结果。只需将双引号字符串放入你的程序中,Ruby 就会对其进行评估:

    "#{def x(s)
            puts(s.reverse)
        end;
    (1..3).each{x(aStr)}}"
    

    即使前面的代码片段是一个字符串,Ruby 解释器也会评估其嵌入的代码并显示结果,如下所示:

    dlrow olleh
    dlrow olleh
    dlrow olleh
    

    虽然这很有趣,但在字符串内编写整个程序可能是一个相当无意义的工作。然而,在其他情况下,这种功能和类似的功能可以更加高效地使用。例如,你可能使用元编程来探索人工智能和“机器学习”。实际上,任何需要根据用户交互修改程序行为的程序都是元编程的理想候选。

    注意

    动态(元编程)特性在 Ruby 中无处不在。例如,考虑属性访问器:将符号(如:aValue)传递给attr_accessor方法会导致创建两个方法(aValueaValue=)。

    eval

    eval方法提供了一种简单的方法来评估字符串中的 Ruby 表达式。乍一看,eval可能看起来与双引号字符串中的#{ }定界符做的是同样的工作。这两行代码产生相同的结果:

    eval.rb

    puts( eval("1 + 2" ) )    #=> 3
    puts( "#{1 + 2}" )        #=> 3
    

    然而,有时结果可能并非你所期望的。看看下面的例子:

    eval_string.rb

    exp = gets().chomp()    #<= User enters 2*4
    puts( eval( exp ))      #=> 8
    puts( "#{exp}" )        #=> 2*4
    

    假设你输入2 * 4,并将其分配给exp。当你用eval评估exp时,结果是 8,但当你用双引号字符串评估exp时,结果是"2*4"。这是因为gets()读取的任何内容都是一个字符串,"#{exp}"将其作为字符串评估,而不是作为表达式,而eval( exp )则将字符串作为表达式评估。为了在字符串内强制评估,你可以在字符串中放置eval(尽管这确实可能违背了练习的目的):

    puts( "#{eval(exp)}" )
    

    这里还有一个例子。尝试它,并按照提示操作:

    eval2.rb

    print("Enter a string method name (e.g. reverse or upcase):")
                                       # user enters: upcase
    methodname = gets().chomp()
    exp2 = "'Hello world'."<< methodname
    puts( eval( exp2 ) )               #=> HELLO WORLD
    puts( "#{exp2}" )                  #=> 'Hello world'.upcase
    puts( "#{eval(exp2)}" )            #=> HELLO WORLD
    

    eval方法可以评估跨越多行的字符串,使得在字符串中执行整个程序成为可能:

    eval3.rb

    eval( 'def aMethod( x )
        return( x * 2 )
    end
    
    num = 100
    puts( "This is the result of the calculation:" )
    puts( aMethod( num ))' )
    

    仔细看看前面的代码。它只包含一个可执行的表达式,即对eval()方法的调用。其他所有看起来像代码的东西,实际上是一个单引号字符串,作为参数传递给eval()eval()方法“解包”字符串的内容,将其转换为真正的 Ruby 代码,然后执行。这显示为:

    This is the result of the calculation:
    200
    

    在所有这些eval的巧妙之处,现在让我们看看编写一个可以自己编写程序的程序有多容易。这里就是:

    eval4.rb

    input = ""
    until input == "q"
        input = gets().chomp()
        if input != "q" then eval( input ) end
    end
    

    这可能看起来不多,但这个小程序让你可以从提示符创建和执行 Ruby 代码。试试看。运行程序,一次输入这里显示的两个方法中的一行(但不要按 q 退出——你很快就会写更多的代码):

    def x(aStr); puts(aStr.upcase);end
    def y(aStr); puts(aStr.reverse);end
    

    注意,你必须将每个整个方法输入到单行中,因为程序会逐行评估输入。我稍后会解释如何绕过这个限制。多亏了 eval,每个方法都被转换成了真正的、可工作的 Ruby 代码。你可以通过输入以下内容来证明这一点:

    x("hello world")
    y("hello world")
    

    现在,当你按下回车键在上一段代码的每一行之后,表达式将被评估,并调用你刚才编写的两个方法 x()y(),产生以下输出:

    HELLO WORLD
    dlrow olleh
    

    对于仅仅五行代码来说,这已经很不错了!

    特殊类型的 eval

    eval 主题上存在一些变体,形式为名为 instance_evalmodule_evalclass_eval 的方法。instance_eval 方法可以从特定的对象中调用,并提供对该对象实例变量的访问。它可以带一个代码块或一个字符串来调用:

    instance_eval.rb

    class MyClass
     def initialize
       @aVar = "Hello world"
     end
    end
    
    ob = MyClass.new
    p( ob.instance_eval { @aVar } )         #=> "Hello world"
    p( ob.instance_eval( "@aVar" ) )        #=> "Hello world"
    

    另一方面,eval 方法不能以这种方式从对象中调用,因为它是对象的私有方法(而 instance_eval 是公共的):

    p( ob.eval( "@aVar" )  )    # This won't work!
    

    实际上,你可以通过将 eval 的名称(符号 :eval)发送到 public 方法来显式更改 eval 的可见性。在这里,我将 eval 添加为 Object 类的公共方法:

    class Object
        public :eval
    end
    

    事实上,考虑到当你编写“独立”代码时,你实际上是在 Object 的作用域内工作,简单地输入以下代码(没有 Object 类的“包装”)会产生相同的效果:

    public :eval
    

    现在,你可以将 eval 作为 ob 变量的方法使用:

    p( ob.eval( "@aVar" ) )        #=> "Hello world"
    

    注意

    严格来说,evalKernel 模块的一个方法,它被混合到 Object 类中。实际上,是 Kernel 模块提供了大多数作为 Object 方法可用的函数。

    在运行时修改类定义有时被称为 猴子补丁。这在某些高度专业化的编程类型中可能起到一定的作用,但作为一个一般原则,随意篡改标准 Ruby 类绝对是不推荐的。更改方法的可见性和向基类添加新行为是创建难以理解的代码依赖性的绝佳方式(例如,你的程序之所以能工作,是因为你偶然知道如何更改基类,但你的同事的程序不能工作,因为他们不知道类是如何被更改的)。

    module_evalclass_eval 方法作用于模块和类,而不是对象。例如,下面的代码向 X 模块(在这里 xyz 在一个代码块中定义,并通过 define_method(它是 Module 类的一个方法)添加为接收者的实例方法)添加了 xyz 方法,并将 abc 方法添加到 Y 类:

    module_eval.rb

    module X
    end
    
    class Y
        @@x = 10
        include X
    end
    
    X::module_eval{ define_method(:xyz){ puts("hello" ) } }
    Y::class_eval{ define_method(:abc){ puts("hello, hello" ) } }
    

    注意

    当访问类和模块方法时,你可以使用作用域解析运算符 :: 或单个点。当访问常量时,作用域解析运算符是强制的,当访问方法时是可选的。

    因此,现在一个 Y 类的实例对象将能够访问 Y 类的abc方法和被混合到 Y 类中的X模块的xyz方法:

    ob = Y.new
    ob.xyz        #=> hello
    ob.abc        #=> hello, hello
    

    尽管它们的名称不同,module_evalclass_eval在功能上是相同的,并且每个都可以与模块或类一起使用:

    X::class_eval{ define_method(:xyz2){ puts("hello again" ) } }
    Y::module_eval{ define_method(:abc2){ puts("hello, hello again") }}
    

    你也可以以相同的方式向 Ruby 的标准类中添加方法:

    String::class_eval{ define_method(:bye){ puts("goodbye" ) } }
    "Hello".bye        #=> goodbye
    

    添加变量和方法

    你还可以使用module_evalclass_eval方法来检索类变量的值(但请注意,你这样做得越多,你的代码就越依赖于类的实现细节,从而损害封装):

    Y.class_eval( "@@x" )
    

    实际上,class_eval可以评估任意复杂性的表达式。例如,你可以用它来通过评估一个字符串向一个类添加新方法:

    ob = X.new
    X.class_eval( 'def hi;puts("hello");end' )
    ob.hi        #=> hello
    

    返回到之前关于从外部一个类中添加和检索类变量的例子(使用class_eval),结果发现也有方法可以从内部一个类中做这件事。这些方法被称为class_variable_get(这个方法接受一个表示变量名的符号参数,并返回变量的值)和class_variable_set(这个方法接受一个表示变量名的符号参数和一个第二个参数,即要分配给变量的值)。

    这里是这些方法使用的一个例子:

    classvar_getset.rb

    class X
        def self.addvar( aSymbol, aValue )
            class_variable_set( aSymbol, aValue )
        end
    
        def self.getvar( aSymbol )
            return class_variable_get( aSymbol )
        end
    end
    
    X.addvar( :@@newvar, 2000 )
    puts( X.getvar( :@@newvar ) )    #=> 2000
    

    要获取一个表示类变量名的字符串数组,请使用class_variables方法:

    p( X.class_variables )    #=> ["@@abc", "@@newvar"]
    

    你还可以在创建类和对象之后使用instance_variable_set向类和对象添加实例变量:

    dynamic.rb

    ob = X.new
    ob.instance_variable_set("@aname", "Bert")
    

    通过结合添加方法的能力,大胆(或可能鲁莽?)的程序员可以完全从“外部”改变一个类的内部结构。在这里,我在类 X 中实现了一个名为addMethod的方法,它使用send方法通过define_method和由&block定义的方法体来创建新的方法m

    def addMethod( m, &block )
        self.class.send( :define_method, m , &block )
    end
    

    注意

    send方法调用由第一个参数(一个符号)标识的方法,并将任何指定的参数传递给它。

    现在,一个 X 对象可以调用addMethod来向 X 类中插入一个新的方法:

    ob.addMethod( :xyz ) { puts("My name is #{@aname}") }
    

    虽然这个方法是从类的特定实例(这里ob)调用的,但它影响的是类本身,因此新定义的方法也将对从 X 类创建的任何后续实例(这里ob2)可用:

    ob2 = X.new
    ob2.instance_variable_set("@aname", "Mary")
    ob2.xyz
    

    如果你不在乎你的对象中数据的封装(我对封装的定义假设隐藏内部数据,尽管有些人有更宽松的定义),你也可以使用instance_variable_get方法来检索实例变量的值:

    ob2.instance_variable_get( :@aname )
    

    你也可以同样地设置获取常量:

    X::const_set( :NUM, 500 )
    puts( X::const_get( :NUM ) )
    

    因为 const_get 返回常量的值,所以你可以使用此方法获取类名(它本身也是一个常量)的值,然后附加 new 方法从这个类创建一个新的对象。这甚至可以为你提供一个在运行时通过提示用户输入类名和方法名来创建对象的方法。通过运行此程序来尝试:

    dynamic2.rb

    class X
        def y
            puts( "ymethod" )
       end
    end
    
    print( "Enter a class name: ")                  #<= Enter: X
    cname = gets().chomp
    ob = Object.const_get(cname).new
    p( ob )                                         #=> #<X:0x2bafdc0>
    print( "Enter a method to be called: " )        #<= Enter: y
    mname = gets().chomp
    ob.method(mname).call                           #=> ymethod
    

    在运行时创建类

    到目前为止,你已经修改了类并从现有类创建了新对象。但你是如何创建一个完全新的类呢?嗯,就像你可以使用 const_get 访问现有类一样,你也可以使用 const_set 创建新类。以下是一个示例,说明如何在创建类之前提示用户输入新类的名称,向其中添加一个方法(myname),创建该类的实例(x),并调用其 myname 方法:

    create_class.rb

    puts("What shall we call this class? ")
    className = gets.strip().capitalize()
    Object.const_set(className,Class.new)
    puts("I'll give it a method called 'myname'" )
    className = Object.const_get(className)
    className::module_eval{ define_method(:myname){
            puts("The name of my class is '#{self.class}'" ) }
        }
    
    x = className.new
    x.myname
    

    如果你运行此程序并在提示输入新类名时输入 Xxx,代码将使用 const_set 创建一个名为 Xxx 的新常量作为类;然后对类调用 module_eval,并使用 define_method 创建一个名称与符号 :myname 匹配的方法,其内容由花括号分隔的代码块给出;这里恰好是一个显示类名的 puts 语句。

    运行此代码,当提示输入时请输入 Xxx。从 Xxx 类创建了一个对象 x;调用了其 myname() 方法;果然,它显示了类名:

    The name of my class is 'Xxx'
    

    绑定

    eval 方法可能接受一个可选的“绑定”参数,如果提供,则会在特定的作用域或“上下文”内执行评估。在 Ruby 中,一个绑定是 Binding 类的实例。你可以使用 binding 方法返回一个绑定。Ruby 类库中 eval 的文档提供了以下示例:

    binding.rb

    def getBinding(str)
        return binding()
    end
    str = "hello"
    puts( eval( "str + ' Fred'" ) )                    #=> "hello Fred"
    puts( eval( "str + ' Fred'", getBinding("bye") ) ) #=> "bye Fred"
    

    看起来可能很简单,但为了理解这里发生了什么,可能需要一些思考。本质上,第一次调用 puts 在当前作用域中评估 str,其中它具有“hello”值。第二次调用 putsgetBinding() 方法的范围内评估 str,其中它具有“bye”值。在这个例子中,str 偶然被作为参数传递,但这不是必需的。在这个重写的版本中,我已经将 str 作为 getBinding() 内部的局部变量。效果是相同的:

    binding2.rb

    def getBinding()
        str = "bye"
        return binding()
    end
    str = "hello"
    puts( eval( "str + ' Fred'" )   )                  #=> "hello Fred"
    puts( eval( "str + ' Fred'", getBinding() ) )      #=> "bye Fred"
    puts( eval( "str + ' Fred'" )   )                  #=> "hello Fred"
    

    注意,binding 是 Kernel 的一个私有方法。getBinding 方法能够在当前上下文中调用 binding 并返回 str 的当前值。在第一次调用 eval 时,上下文是 对象,局部变量 str 的值被使用;在第二次调用中,上下文移动到 getBinding 方法内部,局部值 str 现在是方法中的 str 参数或变量的值。上下文也可以由一个类定义。在 binding3.rb 中,你可以看到实例变量 @mystr 的值根据类而变化。那么,当你使用不同的绑定 eval 这些变量时会发生什么?

    binding3.rb

    class MyClass
       @@x = " x"
       def initialize(s)
          @mystr = s
       end
       def getBinding
          return binding()
       end
    end
    
    class MyOtherClass
       @@x = " y"
       def initialize(s)
          @mystr = s
       end
       def getBinding
          return binding()
       end
    end
    
    @mystr = self.inspect
    @@x = " some other value"
    
    ob1 = MyClass.new("ob1 string")
    ob2 = MyClass.new("ob2 string")
    ob3 = MyOtherClass.new("ob3 string")
    
    puts(eval("@mystr << @@x", ob1.getBinding))
    puts(eval("@mystr << @@x", ob2.getBinding))
    puts(eval("@mystr << @@x", ob3.getBinding))
    puts(eval("@mystr << @@x", binding))
    

    在 Ruby 1.8 中,你会看到以下输出,显示实例变量 @mystr 和类变量 @@x 的绑定都被应用:

    ob1 string x
    ob2 string x
    ob3 string y
    main some other value
    

    但在 Ruby 1.9 中,只有实例变量的绑定被应用;当前()上下文中的类变量始终被使用:

    ob1 string some other value
    ob2 string some other value
    ob3 string some other value
    main some other value
    

    这是否意味着给定绑定中的类变量被忽略?让我们做一个实验。只需在主上下文中注释掉对 @@x 的赋值:

    # @@x = " some other value"
    

    现在再次运行程序。这次,Ruby 1.9 显示如下:

    ob1 string x
    ob2 string x
    ob3 string y
    ...uninitialized class variable @@x in Object (NameError)
    

    显然,Ruby 1.9 确实在绑定中评估类变量。然而,如果存在类变量,它会优先考虑 当前 绑定中的类变量。如果你正在将 Ruby 1.8 程序迁移到 Ruby 1.9 或更新的版本,你需要注意这个差异。

    send

    你可以使用 send 方法调用与指定符号具有相同名称的方法:

    send1.rb

    name = "Fred"
    puts( name.send( :reverse ) )    #=> derF
    puts( name.send( :upcase ) )     #=> FRED
    

    虽然文档中说明 send 方法需要符号参数,但你也可以使用字符串参数。或者,为了保持一致性,你可以使用 to_sym 将字符串转换为符号,然后使用与该符号相同的名称调用方法:

    name = MyString.new( gets() )
    methodname = gets().chomp.to_sym #<= to_sym is not strictly necessary
    name.send(methodname)
    

    这里是使用 send 在运行时执行命名方法的示例:

    send2.rb

    class MyString < String
        def initialize( aStr )
            super aStr
        end
    
        def show
            puts self
        end
    
        def rev
            puts self.reverse
        end
    end
    
    print("Enter your name: ")          #<= Enter: Fred
    name = MyString.new( gets() )
    print("Enter a method name: " )     #<= Enter: rev
    methodname = gets().chomp.to_sym
    puts( name.send(methodname) )       #=> derF
    

    移除方法

    回想一下,你之前(dynamic.rb)使用 send 调用 define_method 并传递给它要创建的方法的名称,m,以及包含新方法代码的块,&block

    dynamic.rb

    def addMethod( m, &block )
        self.class.send( :define_method, m , &block )
    end
    

    除了创建新方法外,有时你可能想移除现有方法。你可以在给定类的范围内使用 remove_method 来做这件事。这将从一个特定的类中移除由符号指定的方法:

    rem_methods1.rb

    puts( "hello".reverse )  #=> olleh
    class String
        remove_method( :reverse )
    end
    puts( "hello".reverse )  #=> undefined method error!
    

    如果为该类的祖先定义了具有相同名称的方法,则不会移除祖先类的方法:

    rem_methods2.rb

    class Y
        def somemethod
            puts("Y's somemethod")
        end
    end
    
    class Z < Y
        def somemethod
            puts("Z's somemethod")
        end
    end
    
    zob = Z.new
    zob.somemethod                     #=> Z's somemethod
    class Z
         remove_method( :somemethod )  # Remove somemethod from Z class
    end
    
    zob.somemethod                     #=> Y's somemethod
    

    在这个例子中,somemethod 从 Z 类中被移除,因此当在 Z 对象上随后调用 zob.somemethod 时,Ruby 会执行 Z 的 祖先 类中第一个具有该名称的方法。在这里,Y 是 Z 的祖先,所以它的 somemethod 方法被使用。

    与之相反,undef_method 阻止指定的类响应方法调用,即使在其祖先中定义了具有相同名称的方法。以下示例使用了之前示例中使用的相同的 Y 和 Z 类。唯一的区别是这次使用 undef_method 而不是使用 remove_method 仅从当前类中移除 somemethod

    undef_methods.rb

    zob = Z.new
    zob.somemethod                       #=> Z's somemethod
    
    class Z
       undef_method( :somemethod )       #=> undefine somemethod
    end
    
    zob.somemethod                       #=> undefined method error
    

    处理缺失的方法

    当 Ruby 尝试执行一个未定义的方法(或者,用面向对象的话说,当一个对象收到它无法处理的消息时),错误会导致程序退出。你可能希望你的程序能够从这样的错误中恢复。你可以通过编写一个名为 method_missing 的方法来实现,该方法将未找到的方法名称分配给一个参数。这将在一个不存在的方法被调用时执行:

    nomethod1.rb

    def method_missing( methodname )
       puts( "Sorry, #{methodname} does not exist" )
    end
    xxx        #=> Sorry, xxx does not exist
    

    method_missing 方法还可以在缺失的方法名称之后接受一个传入参数的列表 (*args):

    nomethod2.rb

    def method_missing( methodname, *args )
          puts( "Class #{self.class} does not understand:
                       #{methodname}( #{args.inspect} )" )
    end
    

    假设之前的 method_missing 方法被写入一个名为 X 的类中,你现在可以尝试在 X 对象上调用任何方法,无论该方法是否存在以及是否传递了任何参数。例如,如果你尝试调用一个不存在的名为 aaa 的方法,首先不带任何参数,然后带三个整数参数,method_missing 方法将响应无效的方法调用并显示适当的错误信息:

    ob = X.new
    ob.aaa            #=> Class X does not understand: aaa( [] )
    ob.aaa( 1,2,3 )   #=> Class X does not understand: aaa( [1, 2, 3] )
    

    method_missing 方法甚至可以动态地创建一个未定义的方法,以便对不存在的方法的调用自动使其存在:

    def method_missing( methodname, *args )
           self.class.send( :define_method, methodname,
                lambda{ |*args| puts( args.inspect) } )
    end
    

    记住,lambda 方法将一个块(这里是大括号之间的代码)转换成一个 Proc 对象。这已在 第十章 中解释。然后代码能够将此对象作为参数传递给 send,定义一个与传递给 method_missingmethodname 参数具有相同名称的新方法。结果是,当在 Z 对象上调用未知方法时,会创建一个具有该名称的方法。运行包含此代码的 nomethod2.rb 程序:

    ob3 = Z.new
    ob3.ddd( 1,2,3)
    ob3.ddd( 4,5,6 )
    

    这会产生以下输出:

    Class Z does not understand: ddd( [1, 2, 3] )
    Now creating method ddd( )
    [4, 5, 6]
    

    在运行时编写程序

    最后,让我们回到你之前查看的程序:eval4.rb。你可能还记得,这个程序提示用户输入字符串以在运行时定义代码,评估这些字符串,并从它们中创建新的可运行方法。

    那个程序的一个缺点是它坚持要求每个方法都在单行上输入。实际上,编写一个允许用户输入跨越多行的方法的程序相当简单。例如,以下是一个评估直到输入空白行为止的所有输入代码的程序:

    writeprog.rb

    program = ""
    input = ""
    line = ""
    until line.strip() == "q"
        print( "?- " )
        line = gets()
        case( line.strip() )
        when ''
            puts( "Evaluating..." )
            eval( input )
            program += input
            input = ""
        when '1'
            puts( "Program Listing..." )
            puts( program )
       else
            input += line
        end
    end
    

    你可以通过输入完整的方法后跟空白行来尝试这个方法(当然,只输入代码,不要输入注释):

    def a(s)             # <= press Enter after each line
    return s.reverse     # <= press enter (and so on...)
    end
                         # <- Enter a blank line here to eval these two methods
    def b(s)
    return a(s).upcase
    end
                         # <- Enter a blank line here to eval these two methods
    puts( a("hello" ) )
    
                         # <- Enter a blank line to eval
                         #=> olleh
    puts( b("goodbye" ) )
                         # <- Enter a blank line to eval
                         #=> EYBDOOG
    

    输入每一行后,都会出现一个提示(?-),除非程序正在评估代码的过程中,此时会显示“Evaluating”,或者当它显示评估结果时,例如 olleh

    如果您按照之前指示的文本输入,您应该看到以下内容:

    Write a program interactively.
    Enter a blank line to evaluate.
    Enter 'q' to quit.
    ?- def a(s)
    ?- return s.reverse
    ?- end
    ?-
    Evaluating...
    ?- def b(s)
    ?- return a(s).upcase
    ?- end
    ?-
    Evaluating...
    ?- puts(a("hello"))
    ?-
    Evaluating...
    olleh
    ?- b("goodbye")
    ?-
    Evaluating...
    EYBDOOG
    

    这个程序仍然非常简单。它甚至没有基本的错误恢复功能,更不用说文件保存和加载等花哨的功能。即便如此,这个小例子展示了在 Ruby 中编写自修改程序是多么容易。

    进一步探索

    使用本章概述的技术,您可以创建从可以教授语法规则的天然语言解析器到可以学习新谜题的冒险游戏等任何东西。

    在这本书中,我涵盖了大量的内容——从“hello world”到动态规划。您已经探索了 Ruby 语言的大部分重要和强大的功能。其余的取决于您。

    这就是冒险真正开始的地方。

    深入挖掘

    有时候,您可能想确保您的 Ruby 对象不能以本章中描述的方式被修改。在这里,您将学习如何做到这一点。

    冻结对象

    在您拥有所有这些修改对象的方法之后,您可能会担心对象可能会被无意中修改。实际上,您可以使用 freeze 方法“冻结”对象来具体固定对象的状态,这是您在第十二章中首次遇到的。一旦被冻结,对象包含的数据就不能被修改,如果尝试修改,将会引发 TypeError 异常。然而,在冻结对象时要小心,因为一旦被冻结,它就不能被“解冻”。

    freeze.rb

    s = "Hello"
    s << " world"
    s.freeze
    s << " !!!"   # Error: "can't modify frozen string"
    

    您可以使用 frozen? 方法具体检查一个对象是否被冻结:

    a = [1,2,3]
    a.freeze
    if !(a.frozen?) then
        a << [4,5,6]
    end
    

    请注意,尽管冻结对象的数据不能被修改,但定义它的类可以被修改。假设您有一个包含 addMethod 方法的类 X,该方法可以使用由符号 m 给定的名称创建新方法:

    cant_freeze.rb

    def addMethod( m, &block )
        self.class.send( :define_method, m , &block )
    end
    

    现在,如果您有一个由 M 类创建的对象,ob,那么调用 addMethod 向类 M 添加新方法是完全合法的:

    ob.freeze
    ob.addMethod( :abc ) { puts("This is the abc method") }
    

    如果您想防止冻结对象修改其类,当然可以使用 frozen? 方法测试其状态:

    if not( ob.frozen? ) then
      ob.addMethod(:def){puts("'def' is not a good name for a method")}
    end
    

    您还可以冻结类本身(记住,类也是一个对象):

    freeze_class.rb

    X.freeze
    if not( X.frozen? ) then
      ob.addMethod(:def){puts("'def' is not a good name for a method")}
    end
    

    附录 A. 使用 RDoc 记录 Ruby

    无标题图片

    RDoc 是一种 Ruby 源代码文档格式和工具的名称。Ruby 标准版中包含的 RDoc 工具可以处理 Ruby 代码文件和 Ruby 的 C 代码类库,以便提取文档并将其格式化,以便在例如网页浏览器中显示。您可以通过源代码注释的形式显式地将 RDoc 文档添加到自己的代码中。RDoc 工具还可以提取源代码的元素,以提供类、模块和方法的名称,以及方法所需的任何参数名称。

    以一种可被 RDoc 处理器访问的方式记录自己的代码很容易。您要么在要记录的代码(如类或方法)之前编写一个普通单行注释块,要么编写一个由 =begin rdoc=end 分隔的嵌入式多行注释。请注意,rdoc 必须跟在 =begin 之后;否则,RDoc 处理器将忽略注释块:

    =begin rdoc
    This is an RDoc comment
    =end
    

    此示例使用单行注释,取自 RDoc 文档:

    # Determine the letters in a word or phrase
    #
    # * all letters are converted to lowercase
    # * anything not a letter is stripped out
    # * the letters are converted into an array
    # * the array is sorted
    # * the letters are joined back into a string
    def letters_of(text)
        text.downcase.delete('^a-z').split('').sort.join
    end
    

    这里,* 字符指示 RDoc 将项目格式化为项目符号列表,生成的输出类似于以下内容:

    letters_of(text)
    Determine the letters in a word or phrase
        • all letters are converted to lowercase
        • anything not a letter is stripped out
        • the letters are converted into an array
        • the array is sorted
        • the letters are joined back into a string
    

    当您准备好生成文档时,只需从命令提示符运行 RDoc 处理器即可。要为单个文件生成文档,请输入 rdoc 后跟文件名:

    rdoc rdoc1.rb
    

    要为多个文件生成文档,请在 rdoc 命令后输入文件名,文件名之间用空格分隔:

    rdoc rdoc1.rb rdoc2.rb rdoc3.rb
    

    RDoc 工具将创建一个格式良好的 HTML 文件 (index.html),顶部有三个面板和一个底部较大的面板。三个顶部面板显示文件、类和方法名称,而底部面板显示文档。

    HTML 包含超链接,以便您可以点击类和方法名称导航到相关的文档。文档放置在其自己的子目录 \doc 中,包括一些必需的 HTML 文件和用于应用格式的样式表。

    您可以通过在单个单词周围放置格式化字符或在多个单词周围放置标签来为您的 RDoc 注释添加额外的格式。使用 ** 来表示粗体,__ 来表示斜体,以及 ++ 来表示等宽的“打字机”字体。较长文本的等效标签是 <b></b> 用于粗体,<em></em> 用于斜体,以及 <tt></tt> 用于打字机字体。

    如果您想排除注释或注释的一部分,使其不包含在 RDoc 文档中,您可以在 #—#++ 注释标记之间放置它,如下所示:

    #--
    # This comment won't appear
    # in the documentation
    #++
    # But this one will
    

    还有一些特殊说明,位于成对的冒号之间。例如,如果您想在浏览器栏中显示标题,可以使用 :title: 如此使用:

    #:title: My Fabulous RDoc Document
    

    使用 RDoc 可以提供更多选项,让您能够以多种方式格式化文档,并以不同的格式输出,除了 HTML。如果您真的想精通 RDoc,请务必阅读完整的文档,可在rdoc.sourceforge.net/doc/index.html在线找到。

    附录 B. 为 Ruby on Rails 安装 MySQL

    无标题图片

    如果你正在使用 Rails,你需要安装一个数据库。尽管有许多选择可供你选择,但最广泛使用的是 MySQL。如果你以前从未使用过 MySQL,你可能会发现一些设置选项令人困惑。在这里,我将尝试引导你通过这个过程,以避免潜在的问题。MySQL 主网站是www.mysql.com/,你可以导航到各种版本的下载页面。

    注意

    本附录基于在 Windows 下安装 MySQL 5.0。在其他操作系统上安装其他版本时可能会有所不同。请参考 MySQL 网站以获取更多指导。

    下载 MySQL

    我将假设你将使用 MySQL 的免费版。这可以从dev.mysql.com/downloads/下载。在撰写本文时,当前版本是 MySQL 5 Community Server。名称和版本号当然会随时间而变化。下载当前(不是即将发布的、alpha 或 beta)版本。选择为你操作系统推荐的特定版本(例如,Win32 和 Win64 可能有不同的版本)。

    你需要找到适合你操作系统的安装程序。对于 Windows,你可以下载完整的 MySQL 包,或者如果可用,较小的 Windows Essentials 包。完整的包包含数据库开发人员的一些额外工具,但这些对于简单的 Rails 开发不是必需的。因此,对于大多数人来说,较小的 Windows Essentials 下载文件是首选。你可能需要选择镜像站点,也可能会有一个问卷调查,如果你想填写的话。

    安装 MySQL

    下载完成后,通过选择下载对话框中的打开运行(如果此对话框仍然可见)或通过例如 Windows 资源管理器双击安装文件来运行程序。

    注意

    在安装 MySQL 期间,可能会出现一些广告屏幕。点击按钮以浏览屏幕。一些安全警告也可能提示你验证安装软件的意图。当提示时,你应该点击必要的选项以继续安装。

    设置向导的第一页现在将出现。点击下一步按钮。如果你愿意将软件安装到默认的 MySQL 目录中——在 Windows 上是在 *C:\Program Files* 之下,你可以选择典型设置选项。但是,如果你想安装到其他目录,请选择自定义。然后点击下一步。点击更改以更改目录。

    当你准备好继续时,点击下一步

    你将看到显示“准备安装程序”的屏幕。请确认目标文件夹是否正确,然后点击安装按钮。

    根据 MySQL 的版本,你现在可能会看到一些促销屏幕,或者可能会提示你创建一个新的 MySQL 账户,这将让你能够接收有关更改和更新的消息。这些不是软件安装的必要部分,你可以点击“下一步”或“跳过”按钮继续安装过程。

    现在会显示向导完成对话框。

    点击完成按钮。

    配置 MySQL

    事实上,这并不是安装的结束。在一些安装程序中,会出现一个新窗口,欢迎你进入 MySQL 服务器实例配置向导。如果这种情况没有发生,你需要自己加载它。在 Windows 上,点击开始菜单,然后通过你的程序组导航到MySQL ▸ MySQL 服务器 5.0(或你正在使用的任何版本号)▸ MySQL 服务器实例配置向导。点击下一步

    假设这是你第一次在这台机器上安装 MySQL,你可以选择标准配置(如果你是从较旧版本的 MySQL 升级,你需要选择详细配置,但这超出了本简单设置指南的范围)。点击下一步。在下一个对话框中,保留默认选项(即作为 Windows 服务安装,服务名称为“MySQL”,并自动启动 MySQL 服务器)。然后点击下一步

    在下一屏,保留修改安全设置复选框选中,并将你选择的相同密码输入到前两个文本框中。你稍后需要这个密码,所以请记住它或将其写在安全的地方。如果你需要从另一台计算机访问 MySQL,可以勾选“启用从远程机器的 root 访问。”然后点击下一步

    注意

    默认的 MySQL 用户名是“root”。密码是你刚刚输入的那个。你稍后创建 Rails 应用程序时需要这两项信息。

    下一屏只是提供了即将执行的任务的一些信息。点击执行按钮。

    如果你之前已安装或配置过 MySQL,你可能会看到一个错误消息,告诉你跳过安装。你可以点击重试以查看是否可以绕过此问题。如果不能,点击跳过,然后重新启动 MySQL 配置过程,当提示时选择重新配置实例和标准实例。

    当一切安装完成后,点击完成

    就这样!

    找不到数据库?

    当使用 Rails 与 MySQL 一起使用时,即使 MySQL 安装成功,当你尝试运行应用程序时,Rails 也可能会显示类似于以下错误消息:

    no such file to load -- mysql
    

    一些版本的 Rails(Rails 2.2 及更高版本)要求将 MySQL 晶石(gem)作为单独的操作安装。为此,请在系统提示符下输入以下内容:

    gem install mysql
    

    在 Windows 上还可能出现另一个问题。当你运行应用程序时,你可能会看到类似于以下错误消息:

    The specified module could not be found.
    c:/ruby/lib/ruby/gems/1.8/gems/mysql-2.7.3-x86-mswin32/ext/mysql.so
    

    如果遇到这个问题,你应该能够通过从 MySQL 二进制目录(例如,C:\Program Files\MySQL\MySQL Server 5.0\bin)复制一个名为 libmySQL.dll 的文件到 Ruby 二进制目录(例如,C:\ruby\bin)来修复它。重启你的应用程序(关闭并重新启动服务器),然后再次尝试运行它。

    附录 C. 进一步阅读

    无标题图片

    本附录包含了一些关于 Ruby 和 Rails 的最有用的阅读材料。

    书籍

    关于 Ruby 和 Rails 的书籍有很多。在我看来,以下是一些最有用的书籍。

    《Ruby 编程:实用主义程序员指南》

    作者:Dave Thomas,Chad Fowler 和 Andy Hunt ($49.95)

    ISBN: 978-0-9745-1405-5 (第二版)

    ISBN: 978-1-9343-5608-1 (第三版)

    实用主义:www.pragmaticprogrammer.com/titles/ruby/index.html

    这是一本关于 Ruby 语言和库的全面指南,所谓的 Pickaxe 书通常被认为是 Ruby 的必备参考书。然而,它并非易读之书,而且(在我看来)也不是 Ruby 的最佳入门书籍。尽管如此,你迟早可能需要它。第二版涵盖了 Ruby 1.8;第三版涵盖了 Ruby 1.9。

    《Ruby 入门:从新手到专家》

    作者:Peter Cooper ($39.99)

    ISBN: 978-1-5905-9766-8 (第一版)

    ISBN: 978-1-4302-2363-4 (第二版)

    Apress:www.apress.com/

    本书以温和的方式介绍了 Ruby 编程。解释清晰,代码示例有用。第二版涵盖了 Ruby 1.9 的某些方面,但并不详细。如果你已经有一些编程经验,并希望有一个易于理解的 Ruby 世界入门,这本书会是个不错的选择。

    《Ruby 的方式》

    作者:Hal Fulton ($39.99)

    ISBN: 978-0-6723-2884-8

    Addison-Wesley:www.awprofessional.com/ruby/

    这是一本关于 Ruby 编程方面的扎实、深入的书。在介绍部分,作者表示,由于相对缺乏教程材料,“你很可能不会从这本书中学到 Ruby。”他将其描述为一种“倒置的参考。”而不是查找方法或类的名称,你将根据功能或目的查找内容。”我个人认为他低估了《Ruby 的方式》的教程价值。然而,作者确实假设你已经相当擅长编程。

    《扎实的 Ruby 编程者》

    作者:David A. Black ($44.99)

    ISBN: 978-1-9339-8865-8 (软封面印刷版,包含免费电子书)

    Manning:www.manning.com/black2/

    这本书在很大程度上是对 David Black 先前著作《Ruby for Rails》的改编,但这次作者专注于 Ruby 语言而不是 Rails 框架。它涵盖了 Ruby 1.8 和 1.9,但对于两个版本之间的确切差异描述得相当模糊。对于相当有经验的程序员来说,这是一本不错的入门书籍。

    《使用 Rails 进行敏捷 Web 开发》

    作者:Sam Ruby, Dave Thomas, 和 David Heinemeier Hansson ($43.95)

    ISBN: 978-1-93435-616-6 (第三版)

    ISBN: 978-1-93435-654-8 (第四版)

    实用主义:pragprog.com/titles/rails4/agile-web-development-with-rails/

    这是“必备”的 Rails 书。几本 Ruby 编程书籍可能争夺成为必备书籍的称号,但我所知没有其他 Rails 书籍能像《Agile Web Development with Rails》那样在全面覆盖其主题方面与之匹敌。第三版涵盖了 Rails 2。第四版涵盖了 Rails 3。

    电子书

    如果您喜欢在电脑屏幕上阅读书籍,这里有一些不错的选择——而且它们都是免费的。

    学习编程

    克里斯·派恩(Chris Pine)书的第一个版本为 Ruby 提供了一个温和的入门介绍。

    www.pine.fm/LearnToProgram/

    《Ruby 编程:实用程序员指南》

    这是著名的“拾金者”书的第一个版本。

    www.ruby-doc.org/docs/ProgrammingRuby/

    《Ruby 小书》

    这是您目前正在阅读的书的弟弟版本。

    www.sapphiresteel.com/The-Little-Book-Of-Ruby/

    网站

    有无数网站致力于 Ruby、Rails 以及相关技术。以下是一些开始探索的网站。

    Ruby 语言网站

    www.ruby-lang.org/

    Ruby 文档网站

    www.ruby-doc.org/

    Ruby 1.8 类库参考(在线)

    www.ruby-doc.org/ruby-1.8/index.html

    Ruby 1.9 类库参考(在线)

    www.ruby-doc.org/ruby-1.9/index.html

    Ruby 类库参考(下载)

    www.ruby-doc.org/downloads/

    Ruby on Rails

    www.rubyonrails.org/

    Ruby Inside(信息/博客)

    www.rubyinside.com/

    我的博客

    www.sapphiresteel.com/Blog/

    附录 D. Ruby 和 Rails 开发软件

    无标题图片

    要编程 Ruby,你需要一个 Ruby 解释器和编辑器或 IDE。本附录列出了 Ruby 和 Rails 开发工具的主要来源。

    IDEs 和编辑器

    一些 Ruby 程序员喜欢使用简单的文本编辑器和从命令行运行程序;其他人则喜欢一个完全集成的 IDE,具有内置的调试功能。这里有一些可能性。

    3rdRail

    3rdRail 官网

    这是一个针对 Eclipse 的商用 Rails-centric IDE。它目前仅支持 Rails 1.x和 2.x

    Aptana RadRails

    Aptana RadRails 官网

    这是一个针对 Eclipse 的免费 Rails-centric IDE。

    NetBeans

    NetBeans Ruby 插件官网

    这是一个针对 NetBeans 的免费 Ruby IDE。现在,NetBeans 中 Ruby 支持的未来发展已经停止。

    Ruby In Steel

    SapphireSteel 官网

    这是一个针对 Visual Studio 的商用 Ruby 和 Rails IDE。

    RubyMine

    JetBrains Ruby 插件官网

    这是一个强调 Rails 的商用 Ruby IDE。

    TextMate

    Macromates 官网

    这是一个针对 Mac OS X 的 Ruby 编辑器。

    Web 服务器

    这里有一些用于 Ruby on Rails 的流行 Web 服务器。

    WEBrick

    WEBrick 官网

    LightTPD

    LightTPD 官网

    Mongrel

    Mongrel 官网

    Nginx

    Nginx 官网

    Apache

    Apache 官网

    数据库

    如果你使用 Rails,你需要一个数据库。通常 SQLite 3 可能用于本地开发,而其他数据库之一可能用于部署。

    MySQL

    MySQL 官网

    SQLite

    SQLite 官网

    PostgreSQL

    PostgreSQL 官网

    SQL Server Express

    SQL Server Express 官网

    Ruby 实现

    在撰写本文时,Ruby 1.8 和 1.9 的版本都可用,并且承诺在未来某个日期推出 2.0 版本。目前,Ruby 1.8.6、1.8.7 和 1.9.2 可能是最广泛使用的 Ruby 版本。JRuby 也有相当强的用户基础。还有其他几个 Ruby 解释器、编译器和虚拟机可用或正在开发中。以下是一些提供更多关于(和下载)Ruby 实现信息的网站列表。

    Ruby

    “标准”Ruby 实现

    Ruby 语言官网

    JRuby

    Ruby for Java

    JRuby 官网

    IronRuby

    一个.NET 的 Ruby 实现

    www.ironruby.net/

    Rubinius

    Ruby 的编译器/虚拟机(主要用 Ruby 编写)

    www.rubini.us/

    MagLev

    快速的 Ruby 实现(开发中)

    maglev.gemstone.com/


    1. a-z ↩︎

    2. a-z ↩︎

    3. a-z 0-9 ↩︎

    4. a-z 0-9 ↩︎

    5. a-z 0-9 ↩︎

    6. a-z 0-9 ↩︎

    7. a-z 0-9 ↩︎

    posted @ 2025-11-27 09:18  绝不原创的飞龙  阅读(19)  评论(0)    收藏  举报