当我们开始写这本书时,我们有一个宏大的计划(那时我们还比较年轻)。我们希望将 Ruby 这门语言从头至尾地描述一遍,从类和对象开始,直到语法中的种种细节为止。那时这的确是个好主意。因为在 Ruby 看来万物皆对象,因此我们打算从对象开始谈起。
或者,这只是我们的一厢情愿。
不幸的是,以这种方式描述一门语言是困难的。如果你不曾了解字符串,if 声明,赋值以及其他细节,要写出一个类的例子是难以成立的。尽管我们要从头至尾地描述这门语言,但我们也需要从低级别的细节开始了解,这样才能知晓样例代码的含义。
因此,我们制定了新的宏大计划(这样我们就不会再被说一点也不务实了)。尽管我们想要从头开始讲述 Ruby,但在此之前,我们会添加一个简短的章节,通过 Ruby 的专有关键字描述一些语言特性,这些语言特性会在后面样例中使用到,算是一种为书籍正式内容制作的一个迷你指引。
让我们开始讨论 Ruby 吧。Ruby 是一门真正的面向对象语言。你操作的每个事物都是对象,这些事物操作的结果也是对象。然而,许多语言都自称是面向对象的,并且对于面向对象也都有着不同的解释,以及对自己所采用思想的不同术语。
因此,在我们更多地了解细节之前,让我们简单地认识一下即将用到的对术语和符号。
当你写面向对象代码时,通常你会从真实世界提取模型到并表现在你的代码中。一般情况下,在你抽象模型的过程,你会发现你需要表现到代码中事物的类型。例如一个自动点唱机,「歌曲」这个概念一定会是一种事物类型。在 Ruby 中,你需要定义一个 class(类) 来表现这个实体。一个类是状态(例如这个歌曲的名字)和使用状态的方法(或许是一个播放歌曲的方法)的结合体。
一旦你拥有了一些类,通常就会想为每个类创建许多实例。在点唱机系统中就包含一个叫做「Song」的类,你需要为其的一般使用区分出不同的实例,比如 Ruby Tuesday, Enveloped in Python, String of Pearls, Small talk 等等。object(对象) 这个词和类实例是相同的含义(作为一个懒惰的打字员,我使用对象这个词会更加频繁)。
在 Ruby 中,这些对象被构造器创建,它是类中的一个特殊方法。一个标准的构造被叫做 new(创建)。
song1 = Song.new("Ruby Tuesday")
song2 = Song.new("Enveloped in Python")
# and so on
这些实例都是从相同的类派生出来的,但它们都有自己唯一的特质。首先,每个对象都有唯一的 object identifier(对象标识) (简称 object id)。其次,你可以定义实例变量,每个实例的变量值都是不同的。这些实例变量保存着对象的状态。例如,每个 Song 中,都会有实例变量保存歌曲标题的值。
在每个类中,我们可以定义实例方法。每个方法就是一块函数定义,这些函数可以在类中调用,也可以在外部调用(在外部调用时依赖访问限制)。这些方法可以访问对象实例的变量,从而访问到对象的状态。
方法通过发送信息给对象的方式调用。这些信息包括方法名字,也包含方法需要的参数。当对象获取到信息后,它会查找类中相应的方法。如果查找到就会执行此方法,如果没有找到,这种情况我们后面再讨论。
方法的业务和信息听起来可能比较复杂,但是在练习中方法的表现会非常自然。让我们看看一些方法的调用(请记住,下面代码中的箭头符号表示对应表达式返回的结果值)。
"gin joint".length » 9
"Rick".index("c") » 2
-1942.abs » 1942
sam.play(aSong) » "duh dum, da dum de dum ..."
上面代码中,在句点符号之前的部分被 receiver(接收器),在句点后的是方法的调用。第一个例子询问了一个字符串的长度,第二个例子在询问另一个字符串中字符 c
的下标。第三行代码是计算一个数字的绝对值。最后,我们让 Sam 给我们播放了一首歌。
这里值得注意的是,在许多其他语言和 Ruby 之间有一个主要的差异。就以 Java 而言,你会发现对过调用一个分离的方法和传递数字的方式计算此数字的绝对值。你可能会这样写
number = Math.abs(number) // Java code
在 Ruby 中,计算一个绝对值的能力建立在数字的基础上——它们在方法内部关心细节。你发送一个 abs
的信息给数字对象,并且让它做相应的工作。
number = number.abs
Ruby 的对象都是类似的应用: 在 C 语言中你需要写 strlen(name)
,而在 Ruby 中可以写为 name.length
等等。这就是我们说 Ruby 是一个真正的面向对象语言的原因。
当开始学习一门新的语言时,不是所有人都喜欢阅读一堆无聊的语法规则。因此我们将走一下捷径。在这个部分我们会添加一些高亮,用以提醒这些知识是写 Ruby 代码必须知道的。稍后,在 18 章我们会着重介绍这些细节。
让我们开始一段基本的 Ruby 程序。我们将会写一个方法用以返回一个字符串,这个字符串会被添加上一个人的名字。稍后我们会调用这个方法好几次。
def sayGoodnight(name)
result = "Goodnight, " + name
return result
end
# Time for bed...
puts sayGoodnight("John-Boy")
puts sayGoodnight("Mary-Ellen")
首先,进行一些基本的观察。Ruby 方法是比较清晰的。你不需要在语句末尾使用分号,只需要换行就可以。Ruby 的注释是以一个 #
号开头,直到此行的结尾都是处于注释的状态。代码的布局取决于你,缩进也是不重要的。
方法是由关键字 def 定义,在关键字之后跟方法名(在这个例子中是 「sayGoodnight」),而方法参数是在小括号之间。Ruby 不会使用花括号划定复合语句和定义体的边界。取而代之的是,你可以使用关键字 end 完成一段内容的结束。我们的方法体很简单。第一行我们将「Goodnight, ]与参数 name
进行了拼接,并将结果赋值给临时变量 result
。下一行将结果返回给了调用者。需要注意的是,我们并还是必须声明 result
变量,在我们赋值的时候它已经存在准备被返回给方法调用的。
定义完方法后我们调用了此方法两次。在所有的例子中,我们把调用方法返回的结果传值给了 puts
方法,puts
方法会将其入参输出并在末尾添加换行。
Goodnight, John-Boy
Goodnight, Mary-Ellen
puts sayGoodnight("John-Boy")
这行包含了两个方法的调用,一个是 sayGoodnight
,另一个是 puts
。为什么一个方法的调用入参需要放在小括号内,而另一个方法的调用入参不需要如此?在这个案例中,完全是个人的习惯问题。下面几行代码是完全等价的。
puts sayGoodnight "John-Boy"
puts sayGoodnight("John-Boy")
puts(sayGoodnight "John-Boy")
puts(sayGoodnight("John-Boy"))
然而生活并不是如此简单,很多时候优先级问题会使清晰知道哪个参数是传入哪个方法调用变得困难,因此我们建议,除了最简单的情况外都使用小括号。
这些例子也展示了一些 Ruby 的字符串对象。其实有许多方式可以创建字符串对象,但是使用字符串创建字符串对象是较常用的方式,也就是由单引号或者双引号包含的字符序列。这两种方式的区别在于 Ruby 对于字符串的处理为字符串对象的工作量上。如果使用单引号,Ruby 的工作量非常少。除了少数情况,你输入的字符串都会转换成字符串对象值。
如果使用双引号,Ruby 需要做更多的工作。首先,需要查找替换点,如字符串中以反斜杠开关的字符,会被一些二进制值替换。最常见的有 \n
,会被替换为换行字符。当一个包含换行符的字符串被输出时,\n
会强制换行。
puts "And Goodnight,\nGrandma"
结果是:
And Goodnight,
Grandma
Ruby 中使用双引号创建字符串对象第二个功能是可以插入表达式。在字符串中,字符串 #{ expression }
会被表达式的值替换。我们可以使用这个功能重写之前的方法。
def sayGoodnight(name)
result = "Goodnight, #{name}"
return result
end
当 Ruby 构造字符串对象时,它找到当前 name
的值在字符串中替代他。任何复杂的表达式都允许通过 #{...}
的方式构造。其实还可以使用便捷的写法,当表达式是全局变量,实例或者类变量时可以不使用花括号。字符串作为 Ruby 中的基本类型,关于它的更多知识我们会在第 5 章讲述。
最后,我们还可以将刚才的方法更加简化。一个 Ruby 方法返回值是最后一个表达式的结果,因此我们可以去除 return
表达式。
def sayGoodnight(name)
"Goodnight, #{name}"
end
我们保证这个部分会十分简短。我们还有个主题需要讲:Ruby 命名。为了简洁,我们将会使用一些使用还没有讲过的知识点(例如 class varable(类变量))。然而在我们开始真正介绍 instance variables(实例变量) 及稍后讨论的知识点之前,我们需要开始谈论这个规则。
Ruby 用约定对命名的使用进行区分:名字的首字母表示这个命名是应该怎样使用的。临时变量,方法参数和方法名称应该以小写字母或者下划线开头。全局变量需要以 $
作为前缀,而使用实例变量时需要以 @
符号开头。类变量需要以 @@
作为开头。最后,类名,模块名和常量应该以一个大写字母开头。命名的不同样例在下表中给出。
下面的这些初始化字符可以与字符,数字和下划线拼接(另外,@
符号后的字符不能是数字)。
Variables Local | Global | Instance | Class | Constants and Class Names |
---|---|---|---|---|
name | $debug | @name | @@total | PI |
fishAndChips | $CUSTOMER | @point_1 | @@symtab | FeetPerMile |
x_axis | $_ | @X | @@N | String |
thx1138 | $plan9 | @_ | @@x_pos | MyClass |
_26 | $Global | @plan9 | @@SINGLE | Jazz_Song |
Ruby 的数组和哈希表是基于下标的收集器。都可以存储对象,并且通过键访问存储的对象。对于数组来说,键是一个整型,但是哈希表支持任何对象作为键。数组和哈希表都可以根据元素的增加而变长。相比而言,数组的元素访问效率会更高,而哈希表的元素访问更具有灵活性。任何数组和哈希表都可以包含不同类型的对象,你可以在一个数组中同一时间包含整型,字符串,以及一个浮点数。
你可以用数组字符定义和初始化一个数组,只需要将数组元素置于方括号之间。当有一个数组时,你可以通过将下标放置在方括号之间访问指定元素,就如同下面的例子一样。
a = [ 1, 'cat', 3.14 ] # array with three elements
# access the first element
a[0] » 1
# set the third element
a[2] = nil
# dump out the array
a » [1, "cat", nil]
你可以通过不在方括号之间放置任何元素的方式创建空数组,或者通过数组对象的构造器 Array.new
也可以达到一样的效果。
empty1 = []
empty2 = Array.new
有时,创建一个单词数组是比较麻烦的,你需要在元素间添加引号和逗号。幸运地是,%w
这个简写可以帮助我们方便地达到上述效果。
a = %w{ ant bee cat dog elk }
a[0] » "ant"
a[3] » "dog"
Ruby 的哈希表和数组相似。不过哈希表是使用花括号而不是使用方括号。其中的每个条目必须使用两个对象,一个是键,另一个是值。
例如,你可能想将乐器分配至管弦乐器模块。你可以用哈希表完成此事。
instSection = {
'cello' => 'string',
'clarinet' => 'woodwind',
'drum' => 'percussion',
'oboe' => 'woodwind',
'trumpet' => 'brass',
'violin' => 'string'
}
哈希表与数组一样使用方括号取值。
instSection['oboe'] » "woodwind"
instSection['cello'] » "string"
instSection['bassoon'] » nil
最后一个示例表明了,在哈希表中当键的取值不存在时,默认返回 nil
。一般情况下没有不便,毕竟当作为条件表达式使用时,nil
和 false
是一个意思。但有时你会想修改默认值。例如,如果你用哈希表统计每个键出现的次数时,为了方便,默认值应该修改为零。这时当你创建一个新的空哈希表时,可以通过指定默认值的方式轻易做到上述需求。
histogram = Hash.new(0)
histogram['key1'] » 0
histogram['key1'] = histogram['key1'] + 1
histogram['key1'] » 1
数组和哈希表对象中有许多有用的方法,这些会从 33 页开始的部分进行讨论,关于其中的更多详细内容位于 278 至 317 页的部分。
Ruby 有所有常用的逻辑控制结构,比如 if
判断和 while
循环。在 Java,C 和 Perl 中,程序员可能会在这些逻辑结构体周围不使用花括号。但是,Ruby 使用 end
关键字结束逻辑结构体。
if count > 10
puts "Try again"
elsif tries == 3
puts "You lose"
else
puts "Enter a number"
end
类似地,while
结构也是以 end
结束。
while weight < 100 and numPallets <= 30
pallet = nextPallet()
weight += pallet.weight
numPallets += 1
end
在你使用 if
或者 while
结构并且只有单一表达时,Ruby 结构修改器是一个有用的快捷方式。可以简洁地书写表达式,然后再跟 if
或者 while
条件。例如,下面是一个简单的 if
结构。
if radiation > 3000
puts "Danger, Will Robinson"
end
这里可以用结构修改器重新书写如下。
puts "Danger, Will Robinson" if radiation > 3000
类似地,while
循环也可以这样修改
while square < 1000
square = square*square
end
可以简写如下
square = square*square while square < 1000
这些结构修改器在 Perl 程序员看来应该比较熟悉。
许多 Ruby 的内置类型对于所有不同语言的程序员来说都是相似的。主流的语言中都会包含字符串,整型,浮点型,数组等等。然而,在 Ruby 出现之前,正则表达式作为内置类型只会出现在所谓的脚本语言中,例如 Perl,Python 和 awk。这是一种羞耻,尽管正则表达式比较神秘,但不失为一种强有力的文本处理工具。
已经有书籍完整地讲述了关于正则表达式的种种(比如 Mastering Regular Expressions),因此我们不打算在这个简短的章节里面将这些知识复述一遍。取而代之的是,我们会实际展示几个关于正则表达式的小例子。你也可以从 56 页开始完全了解正则表达式。
正则表达式通过简单的方式描述字符模式,以完成在字符串中匹配目标的工作。在 Ruby 中,你通常可以通过在斜杠之间编写模式的方式创建一个正则表达式(/pattern/)。而且,对于 Ruby 来说,正则表达式也是对象,本身也可以进行操作。
例如,你可以像下面的例子一样编写一个模式,用来匹配字符串中的「Perl」或者「Python」文本。
/Perl|Python/
斜杠标识出了模式,模式中包含了我们需要匹配的两个文本,使用管道符号「|」分隔。你也可以在表达式中使用小括号,正如在算术表达式中那样使用,于是上面的表达式也可以写成如下这样。
/P(erl|ython)/
你也可以在模式中指定重复的方式。/ab+c/
匹配一个包含 a
并跟随一个或多个 b
再紧接着一个 c
的字符串模式。如果把加号替换成星号,/ab*c/
就匹配一个包含 a
并跟随零个或多个 b
再紧接着一个 c
的字符串模式。
你也可以通过一个模式匹配一组字符。一些公共的例子是字符类,比如 \s
,会匹配一个空白字符(空格,tab,换行等等),\d
可以匹配任何数字,以及 \w
可以匹配一般单词中的任意字符。一个句点字符 .
可以匹配任意字符。
我们可以把这些知识组合成一些有用的正则表达式。
/\d\d:\d\d:\d\d/ # 匹配时间,例如 12:34:56
/Perl.*Python/ # Perl,跟零个或多个其他字符,再是 Python
/Perl\s+Python/ # Perl,跟一个或多个空白字符,再是 Python
/Ruby (Perl|Python)/ # Ruby,然后一个空格,再是 Perl 或者 Python
一旦你已经创建一个模式,如果不使用它显示很不够意思。匹配操作符 =~
可以通过正则表达式匹配一个字符串。如果这个模式在字符串中匹配上,=~
会返回其起始位置,否则会返回 nil
。这意味着你可以使用正则表达式作为 if
和 while
语句的条件。如下,如果字符串包含 ‘Perl’ 或者 ‘Python’ 就会将这段字符串输出。
if line =~ /Perl|Python/
puts "Scripting language mentioned: #{line}"
end
如果想通过正则表达式替换字符串中的目标字符,可以使用 Ruby 的替换方法。
line.sub(/Perl/, 'Ruby') # 替换第一个 'Perl' 为 'Ruby'
line.gsub(/Python/, 'Ruby') # 替换所有的 'Python' 为 'Ruby'
整本书我们将会在许多地方谈论到正则表达式。
这一章节会简短介绍 Ruby 中的一个特点,也是它的优点。我们先看看代码块,你能够结合方法的调用使用代码块,此时代码块就像参数一样。这是一个非常有用的特性。你可以使用代码块实现回调函数(但这种实现会比 Java 中的匿名内部类更加简单),实现回调函数的方式可以是传递代码块(同样它也比 C 语言的指针函数更加灵活),并且也可以实现迭代器。
代码块就是在花括号或者 do...end
间的一块代码。
{ puts "Hello" } # 这是一个代码块
do #
club.enroll(person) # 这也是一个代码块
person.socialize #
end #
一旦你已经创建了一个代码块,你就可以通过调用方法的方式访问代码块。然后你可以使用 Ruby 的 yield
语句调用方法块一次或多次。下面的例子展示了这种方式。我们定义一个方法调用两次 yield
。然后我们调用它,把一个代码块放在方法调用之后,并且同一行的位置(并且是在调用方法的所有参数之后)。(一些人喜欢认为通过一个方法的调用使用代码块,就像把代码块作为一个参数传递。这只是其中的一方面,但并不完整。更好的认知应该是在代码块与方法之间有一个协同程序,它可以在代码块和方法之间来回穿梭)。
def callBlock
yield
yield
end
callBlock { puts "In the block" }
结果是:
In the block
In the block
看看代码块是怎样被执行了两次的,其实每一次都是调用的 yield
。
你可能会提供参数来调用 yield
,希望这些参数能够通过代码块使用。在代码块中,你通过在两个竖线「|」之间列举参数名字来获取这些参数。
def callBlock
yield ,
end
callBlock { |, | ... }
代码块也在 Ruby 库中用来实现迭代器,比如一些从容器(例如数组)中返回连续元素的方法。
a = %w( ant bee cat dog elk ) # 创建数组
a.each { |animal| puts animal } # 迭代所有内容
结果是:
ant
bee
cat
dog
elk
让我们看看如何实现一个像前一个例子中 Array
类的 each
方法。each
方法通过在循环到每个元素时都使用 yield
调用。在伪代码中看起来可能像下面这样:
# within class Array
def each
for each element
yield(element)
end
end
你可以调用 each
方法遍历数组中的元素同时也将代码块应用上。代码块在每个元素被遍历到时都会调用。
[ 'cat', 'dog', 'horse' ].each do |animal|
print animal, " -- "
end
结果是:
cat -- dog -- horse --
类似的,许多像 C 和 Java 中的循环结构在 Ruby 中被简化为了方法调用,然后通过代码块零次或多次的调用实现。
5.times { print "*" }
3.upto(6) {|i| print i }
('a'..'e').each {|char| print char }
结果是:
*****3456abcde
我们使用了数字 5 调用了代码块 5 次,然后通过数字 3 调用代码块,传入代码块中连续值,一直到 6 为止。最后,通过 each
方法将 ‘a’ 至 ‘e’ 之间的字符调用了代码块。
Ruby 有一个全面的 I/O 库。在本书的许多例子中都会和 I/O 的简单方法有所关联。我们在输出时已经使用过两个方法。puts
输出它的每个参数,并且在结尾添加一个换行。print
也会输出它的参数,不过没有换行。这些都可以用来输出任何 I/O 对象,但在默认情况下是输出到控制台。
其它我们使用比较多的输出方法是 printf
,它会根据格式定义输出它的参数(就像 C 或 Perl 中的 printf
方法一样)。
printf "Number: %5.2f, String: %s", 1.23, "hello"
结果是:
Number: 1.23, String: hello
在这个例子中,格式化字符串 “Number: %5.2f, String: %s” 使用一个浮点数(允许总共 5 个字符,其中小数位有 2 个字符)和字符串替换。
当然也有很多方式可以读取程序中的输入。一般大多数传统方式是使用 gets
例程,它会从你的程序标准输入流中返回下一行。
line = gets
print line
不过 gets
例程有一个副作用。尽管可以返回读取到的行,但它也会把读取的行存储到全局变量 $_
中。这是一个特殊变量,在许多情况下会被作为默认参数使用。如果你调用 print
方法而不传入参数,它就会打印 $_
中的内容。如果使用一个正则表达式作为 if
或者 while
语句的条件,正则表达式会对 $_
进行匹配。从一些纯粹主义者的角度来说, 以上这些简写都可以帮助你简化代码。例如,下面的程序会输出所有包含 ‘Ruby’ 的输入的行。
while gets # 读取行并赋值给 $_
if /Ruby/ # 与 $_ 进行匹配
print # 输出 $_
end
end
不过以 Ruby 的方式一般会使用遍历器。
ARGF.each { |line| print line if line =~ /Ruby/ }
这里使用了一个预定义对象 ARGF
,它代表程序能够读取的标准输入。
这章就到这里。我们完成了 Ruby 基础部分的快速学习。我们简单地介绍了对象,方法,字符串,容器和正则表达式,也了解了一些基本的逻辑控制结构,以及一些优雅的迭代器。希望这章能够给予你足够的知识储备以完成本书接下来的内容。
时间要继续,我们也要继续到下一个更高的层级进行学习。接下来,我们要学习一下类和对象,同时也会了解 Ruby 中最高级的结构以及这门完整语言的重要基础。
本文翻译自《Programming Ruby》,主要目的是自己学习使用,文中翻译不到位之处烦请指正,如需转载请注明出处
本章原文为 Ruby.new