Programming Ruby

实用程序员指南

上一章 < 目录 ^
下一章 >

Ruby语言



这一章让我们来对Ruby语言做一个从头到脚地审视。和前面的章节不同,在这里我们集中精力阐述事实,而不是揭示语言设计的细节问题。然后我们也尽量避免对内建的类和模块的介绍,这些内容在第275页开始有详细地介绍。

如果这章的内容看上去很眼熟,那是因为它们本来就应该是我们熟悉的;我们在前面介绍性的章节中对它们都有描述。应该把这一章看成自包含的对核心Ruby语法描述的语言参考。

代码编排(Source Layout)

Ruby程序是用7字节ACSII码(7-bit ASCII)来表示的。[Ruby也有完整的语言扩展来支持Kanji编码,就是使用EUC, SJIS或UTF-8编码系统。如果一段代码使用了7字节ACSII码以外的编码格式,那么KCODE选项就应该被适当地设置,如137页所示。]

Ruby是一种面向行的(line-oriented)程序设计语言。Ruby的表达式和声明语句都是在一行的最后结束,除非一个语句很明显是没有结束的---比如一行的最后一个字符是一个操作符或者逗号。一个分号可以用来分割一行中的多个语句。你也可以在一行最后加一个反斜杠来使这一行延续到下一行。注释以'#'开始,到物理行结束为止。在编译(执行)的时候,注释将被忽略。

a = 1

b = 2; c = 3

d = 4 + 5 +      #不需要'\'     6 + 7

e = 8 + 9   \     + 10         #需要'\'

在以=begin开始的一行和以=end结束的一行之间的语句会被编译器忽略,不过它们可以作为内嵌文档来使用(请参见第511页开始的附录A)。

Ruby只读一遍源程序,所以你可以向Ruby编译器的stdin发送程序代码。

echo 'print "Hello\n"' | ruby

在任何地方,只要编译器遇到一行仅包含“__END__”的代码,没有前行和后随的空格,它就把这一行当成程序的结束---后面的所有代码都不会被编译了。然而,后面的代码可以用一个全局IO对象DATA读入正在执行的当前程序,这在217页有描述。

BEGIN和END模块

每个Ruby源文件都可以声明一些代码模块用来在文件被载入前(BEGIN模块)或者在程序执行结束后(END模块)运行。

BEGIN {
  begin code
}

END { end code }

一个程序可以有多个BEGINEND模块。BEGIN模块以它们出现的顺序执行,END模块以相反的顺序执行。

常规定界输入(General Delimited Input)

还有另外一种用来表示字符串常量,数组,正则表达式和shell命令的格式,就是用常规定界语法。所有这些常量都是以一个百分号开始,后面跟一个用来指定常量类型的字符。对这些字符的描述请参见200页的表18.1;常用的几个字符在这章的后面会有描述。

常规定界输入
类型 涵义 参见
%q 单引号字符串 202
%Q, % 双引号字符串 202
%w 常量数组 204
%r 正则表达式 205
%x Shell命令 218

在类型字符后面是一个分隔符,它可以是任何字符。如果分隔符是“(”,“[”,“{”,“<”中的一个,那么常量由分隔符到相应的后随结束符(译者注:比如左括号'('对右括号')')之间的字符组成,注意嵌套的分隔符。对于其他的分隔符,常量由两个分隔符之间的字符组成。

%q/this is a string/
%q-string-
%q(a (nested) string)

定界字符串可以跨越多行文本。

%q{def fred(a)
     a.each { |i| puts i }
   end}

基本数据类型

Ruby的基本数据类型有数字,字符串,数组,哈希表(hashes),范围(ranges),符号(symbols)和正则表达式。

整数和浮点数类型

Ruby的整数是类Fixnum或者Bignum的对象。Fixnum可以容纳物理机器一个字长少一位大小的整数。当一个Fixnum超过它的范围时,它会自动转换成Bignum类型,而Bignum类型的范围只能被物理内存所限制。如果一个Bignum上的操作使得它的最终值适合一个Fixnum保存,那么结果就会以一个Fixnum对象返回。

整数前面可以带一个可选的符号标记,一个可选的进制指示符(0代表八进制,0x代表十六进制,0b代表二进制),然后跟一串相应进制的数字串。在数字串中的下划线将被忽略。

你可以在一个ASCII字符前面加一个问号来取得它相应的整数值。Ctrl和Meta(译者注:Meta就是Alt键)的键组合可以由?\C-x, ?\M-x, 和?\M-\C-x来产生。一个字符ch的Ctrl版本是(就是Ctrl + ch组合)ch&0x9f,然后它的Meta版本是ch | 0x80。你可以用字符序列?\\来取得反斜杠的整数值。

123456                    # Fixnum
123_456                   # Fixnum (下划线被忽略)
-543                      # Negative Fixnum
123_456_789_123_345_789   # Bignum
0xaabb                    # 十六进制
0377                      # 八进制
-0b1010                   # 二进制 (负的)
0b001_001                 # 二进制
?a                        # character code
?A                        # upper case
?\C-a                     # control a = A - 0x40
?\C-A                     # case ignored for control chars
?\M-a                     # meta sets bit 7
?\M-\C-a                  # meta and control a

一个带有小数点的数字串(也可以带指数)被认为是Float对象,大小和物理机器上double类型一样。你必须在小数点后面跟一个数字,因为像1.e3意味着去调用类Fixnum的方法e3

12.34 12.34
-.1234e2 -12.34
1234e-2 12.34

字符串

Ruby提供了好几种方法来产生字符串常量。每中方法都产生String对象。每种方法在字符串是如何定界和要产生多少替换上是不一样的。

单引号字符串('stuff'和%q/stuff/)进行最少限度的替换。两种格式的表示方法都把“\\”转换成一个反斜杠,然后把“\'”转换成一个单引号。

'hello' hello
'a backslash \'\\\'' a backslash '\'
%q/simple string/ simple string
%q(nesting (really) works) nesting (really) works
%q no_blanks_here ; no_blanks_here

双引号字符串("stuff",%Q/stuff/,和%/stuff/)还会进行额外的替换,详见203页表18.2。

双引号字符串中的替换

\a Bell/alert (0x07) \nnn Octal nnn
\b Backspace (0x08) \xnn Hex nn
\e Escape (0x1b) \cx Control-x
\f Formfeed (0x0c) \C-x Control-x
\n Newline (0x0a) \M-x Meta-x
\r Return (0x0d) \M-\C-x Meta-control-x
\s Space (0x20) \x x
\t Tab (0x09) #{expr} Value of expr
\v Vertical tab (0x0b)

a  = 123
"\123mile" Smile
"Say \"Hello\"" Say "Hello"
%Q!"I said 'nuts'," I said! "I said 'nuts'," I said
%Q{Try #{a + 1}, not #{a - 1}} Try 124, not 122
%<Try #{a + 1}, not #{a - 1}> Try 124, not 122
"Try #{a + 1}, not #{a - 1}" Try 124, not 122

字符串可以跨越多个文本行,在这种情况下它们可以包含换行符。我们也可以使用here documents来表达很长的字符串常量。每当Ruby遇到字符序列<<标识符 或者 <<括号字符串的时候,它就把后随的所有输入行变成一个字符串常量。它一直接收字符到字符串中,直到它遇到一行以前面的标识符或者括号字符串开始的程序行。你可以在<< 后面跟一个负号,使得结束符可以向右边缩进。如果一个括号字符串被用来作为结束符,那么引号规则就会应用到here document中;另外,双引号规则也起作用。

a = 123
print <<HERE
Double quoted \
here document.
Sum = #{a + 1}
HERE

print <<-'THERE'     This is single quoted.     The above used #{a + 1}     THERE
产生结果:
Double quoted here document.
Sum = 124
    This is single quoted.
    The above used #{a + 1}

相邻的单引号和双引号字符串会连接在一起成为一个 String 对象。

'Con' "cat" 'en' "ate" "Concatenate"

字符串保存在8比特字节长的序列中[为了在日本使用,jcode库支持一系列对EUC,SJIS,或者UTF-8编码字符串的操作。不过底层的字符串还是以字节形式被访问。],然后每个字节都保存一个256以内的8比特值,包括空值null和换行。203页表18.2所示的替换规则允许不可打印字符可以便利并且可移植地嵌入到普通字符串中。

每一次一个字符串被用在一个赋值或者用做一个参数的时候,一个新的String对象会被创建。

for i in 1..3
  print 'hello'.id, " "
end
产生结果:
537767360 537767070 537767040

String类的详细资料请参见第363页开始的文档。

范围(Ranges)

在一个条件表达式外面,表达式expr..exprexpr...expr会创建范围对象(Range)。中间两点的格式不包括两边的边界值;中间有三点的格式还包括最后那个边界值。要看Range类的详细信息,请参见第359页。第222页有在条件语句中使用范围的其他方法。

数组

Array的对象由一系列在方括号中由逗号分隔的对象引用组成。最后一个逗号会被忽略。

arr = [ fred, 10, 3.14, "This is a string", barney("pebbles"), ]

字符串数组可以由一个简捷符号组成,%w,它可以把由逗号分隔的一个连续字符串中的子字符提取出来组成一个字符串数组。空格可以由反斜杠转义。这是一种定界输入的格式,请参见200--201页。

arr = %w( fred wilma barney betty great\ gazoo )
arr ["fred", "wilma", "barney", "betty", "great gazoo"]

哈希值(Hashes)

一个Ruby的Hash对象是由一系列在花括号里面的键/值匹配对组成的。由逗号分隔键/值对,键和值之间由=>连接。最后一个逗号会被忽略。

colors = { "red"   => 0xf00,
           "green" => 0x0f0,
           "blue"  => 0x00f
         }

在一个特定的哈希表中的键和值的类型不一定要一致。

哈希键值的要求

作为一个哈希键值的唯一要求是它必须响应消息hash,然后返回一个哈希值,这个哈希值不能改变。这意味着一些类(比如ArrayHash,像这种类型的)不能很好的适合哈希键值的要求,因为它们的值会随着它们的内容而变化。

如果你保持了一个对象的外部引用,然后这个对象又被用来做为一个哈希键。那么你通过这个引用来改变这个对象然后改变哈希值,这个哈希键对应的哈希值将不会起作用。

因为字符串经常被用来当作键值,然后字符串值又会经常改变,Ruby对字符串键值做了特殊处理。当你把一个String对象当做哈希键时,哈希表会复制一个字符串,然后把这个字符串当作键值。后随的所有对原来字符串的改变不会影响哈希表。

如果你写了自己的类然后把它们的实例当作哈希键值,你必须确保哈希表的键值在创建以后不再改变或者在键值改变的时候记得调用Hash#rehash方法来重新索引哈希表。

符号对象(Symbols)

一个Ruby的符号对象是一个对象名的内部表示。你可以在对象名前面加一个冒号来产生相应的符号对象。一个特定的对象名总是产生相同的符号对象,不论对象名在程序中是如何被使用的。

:Object
:myVariable

其他编程语言称这个替换过程为“interning”,然后把符号对象叫做“atoms”。

正则表达式

正则表达式是类型Regexp的对象。它们可以通过显式地调用Regexp.new来创建,或者使用文字常量方式,/pattern/和%r{ pattern}%r创建方法是一种普通定界输入(详见200--201页)。

/pattern/
/pattern/选项
%r{pattern}
%r{pattern}选项
Regexp.new( 'pattern' [, 选项
            ] )

正则式的选项

一个正则表达式可以包含一个或多个选项来修改默认的匹配规则。如果你使用文字常量来创建正则式,那么一个或多哥选项就紧跟在结束符后面。如果你使用Regexp.new方法,那么选项就是构造函数的第二个参数。

i 忽略大小写. 这个匹配将会忽略模式和字符串中的字符的大小写。如果全局变量$=被设置,那么匹配也会大小写不敏感。
o 替换一次。任何在正则式中的#{...}替换都仅进行一次,就是第一次遇到它的时候。其他情况中,替换会在每次一个Regexp对象产生的时候进行。
m 多行模式。通常的,'.'匹配除了换行以外的任何字符。有了/m 选项以后,'.'匹配任何字符了。
x 扩展模式。复杂的正则表达式会很难阅读。'x'选项允许我们可以在模式中加入空格,换行和注释来使它更容易被阅读。

正则式的模式

规则字符
除了.,|, (, ), [, \, ^, {, +, $, *,和?,任何字符都匹配它自己。如果要匹配上面的字符,那么应该在它们前面加一个反斜杠。

^
匹配一行的开始

$
匹配一行的末尾

\A
匹配字符串的开始

\z
匹配字符串的末尾

\Z
匹配字符串的末尾,如果字符串以"\n"结束,那么它从"\n"前面开始匹配。

\b, \B
相应匹配单词边界和非单词的边界

[ 字符 ]
一个字符串类型匹配任何出现在方括号里面的单个字符。 字符|, (, ), [, ^, $, *,?,在其他地方有特殊含义,但是在方括号里面就失去了它们的特殊含义。序列 \ nnn, \x nn, \c x, \C- x, \M- x, and \M-\C- x有如203页表18.2所示的特殊含义。序列\d, \D, \s, \S, \w,和\W是一组字符串的缩写,如59页表5.1所示。序列c1-c2表示在c1和c2之间的所有字符(包括c1,c2)。字符]-必须仅跟在左括号后面。一个(^)紧跟在左括号后使匹配反义---模式匹配任何不在方括号中出现的字符。

\d, \s, \w
分别匹配数字,空格和单词字符(word character)。\D, \S, 和\W匹配不是数字,空格和单词字符的字符。这些缩写在59页表5.1有详细描述。

. (句点)
出现在方括号外面,它代表除了换行以外的任何字符(如果/m选项被设置了,那么它也匹配换行符)。

re *

匹配零次或者多次re的出现。

re +
匹配一次或者多次re的出现。

re {m,n}
匹配最少"m"次然后最多"n"次re的出现。

re ?
匹配零次或一次re的出现。 *, +, 和{m,n}都是最大匹配的,加上一个问号使得它们进行最小匹配。

re1 | re2
匹配re1或者re2|的优先级很低。

(...)
圆括号用来集合正则表达式。比如,模式 /abc+/匹配一个含有一个'a', 一个'b'然后一个或多个'c'的字符串。/(abc)+/就匹配一次或多次"abc"的出现。圆括号还用来收集模式匹配的结果。Ruby为每一个左圆括号保存括号中的部分匹配结果,直到相应的右括号为止。在同一个匹配中,\1代表第一组匹配的结果,\2代表第二组结果,依次类推。在匹配外面,特殊变量$1, $2等起到相同的作用。

替换作用

#{...}
表示一个字符串中的表达式的替换。默认情况,替换会在每次匹配成功时发生。当/o选项被设置时,它仅仅匹配一次。

\0, \1, \2, ... \9, \&, \`, \', \+
替换第n组匹配,或者整个,前面或后面,或者最高匹配的那一组。

语法扩展

和Perl和Python类试,Ruby的正则表达式提供了一些对传统Unix正则表达式的扩展。这些扩展都写在(?/code> and )里面。这些圆括号括着的扩展是组,但是它们不会产生后引用(backreferences):它们不会设置\1$1等变量的值。

(?# 注释)
在模式串中插入注释。注释在模式匹配的时候被忽略。

(?:re)
re加入组,然而不产生后引用。这在当你希望创建一个组但是又不想让它设置$1等值的时候特别有用。在下面的例子中,两个模式都保存了一个包括月份,日子和年份的数据,用冒号或空格分隔。第一种格式的在$2$4中保存了数据,但是第二种格式没有保存内部数据。

date = "12/25/01"
date =~ %r{(\d+)(/|:)(\d+)(/|:)(\d+)}
[$1,$2,$3,$4,$5] ["12", "/", "25", "/", "01"]
date =~ %r{(\d+)(?:/|:)(\d+)(?:/|:)(\d+)}
[$1,$2,$3] ["12", "25", "01"]

(?=re)
在此处匹配re,但是不要销毁它(也可以惬意地叫做零距离回顾(``zero-width positive lookahead''))。它可以让我们方便地引用前面的内容而不会影响$&。在这个例子中,scan方法匹配后面跟一个句点的单词,但是句点不包括在匹配结果里面。

str = "red, white, and blue"
str.scan(/[a-z]+(?=,)/) ?/td> ["red", "white"]

(?!re)
看看在此处是否不匹配re。不要销毁这个匹配(否定的零距离回顾)。比如,/hot(?!dog)(\w+)/匹配任何包含“hot”然后后面不跟“dog”的单词,返回$1中的最后一个单词。

(?>re)
把一个独立的正则式嵌入到第一个正则式中。这个表达式在当前匹配点被确定。如果它销毁了一些字符,这些字符就不会被上级的正则式访问到。因此这个结构是有后回溯机制的(backtracking),这可以是一个执行效果的增强。比如,模式/a.*b.*a/的基本意思是匹配一个含有一个'a',后面跟数个'b',然后没有后随'a'的字符串。然而,这可以用嵌套的正则式/a(?>.*b).*a/来避免。在这种格式中,内嵌的正则式消耗掉了所有字符串,直到有字符'b'的出现。当一个对后随'a'的查找失败以后,就没有回溯的必要了,这个模式匹配迅速结束。

require "benchmark"
include Benchmark
str = "a" + ("b" * 5000)
bm(8) do |test|
  test.report("Normal:") { str =~ /a.*b.*a/ }
  test.report("Nested:") { str =~ /a(?>.*b).*a/ }
end
产生结果:
              user     system      total        real
Normal:   0.420000   0.000000   0.420000 (  0.414843)
Nested:   0.000000   0.000000   0.000000 (  0.001205)

(?imx)
打开"i","m"或"x"选项。如果用在一个组里面,那么效果就限制在组里面。

(?-imx)
关闭"i","m"或"x"选项。

(?imx:re)
打开re的"i","m"或"x"选项。

(?-imx:re)
关闭re的"i","m"或"x"选项。

标识名

Ruby的标识名用来指向常量,变量,方法,类和模块。标识名的首字符用来帮助我们确定标识所指向内容的类型。一些标识名,就是210页表18.3所示的都是保留字,不能用来当作变量,方法,类或模块的名字。

保留字

__FILE__ and def end in or self unless
__LINE__ begin defined? ensure module redo super until
BEGIN break do false next rescue then when
END case else for nil retry true while
alias class elsif if not return undef yield

在下面的描述中,小写字母表示'a'到'z'的字母和'_',下划线。大写字母表示'A'到'Z'的字母,还有数字表示'0'到'9'的数字字符。标识符字符表示大小写字母和数字的组合。

一个局部变量由一个小写字母后面跟标识符组成。

fred  anObject  _x  three_two_one

类的实例变量由一个‘@’开始,后面跟一个大写或小写字母,然后后面还可以有一些标识符字符。

@name  @_  @Size

一个类变量由两个‘@@’开始,后面跟一个大写或小写字母,然后后面还可以有一些标识符字符。

@@name  @@_  @@Size

一个常量名由一个大写字母开始,后面跟标识符字符。类名和模块名是常量,所以也遵循常量的命名规则。常量名一般是由大写字母组成,然后可以带下划线。

module Math
  PI = 3.1415926
end
class BigBlob

全局变量,以及一些特殊的系统变量,由一个美圆符号'$'开始,然后后面跟标识符字符。另外,还有一些有两个字符组成的变量名,它们第二个字符是标点符号。这些预设的变量名在213页有描述。最后,全局变量名也可以用"$-"开头,后面跟任意一个字符。

$params  $PROGRAM  $!  $_  $-a  $-.

关于方法的命名规则,在从第225页的开始的章节有描述。

变量/方法的疑惑

每当Ruby在一个表达式中遇到一个比如像'a'这样的字符,他必须确定这个'a'到底是一个局部变量的引用还是一个无参数的函数方法调用。为了确定这个,ruby用了一种启发式(heuristic)的方法。当Ruby在读取源代码的时候,它记下了那些被赋值的标识符。它假定这些标识符是变量名。然后如果它在随后遇到一个标识符可能是变量,也可能是方法名的时候,它就查看这个标识服是否在以前被赋过值。如果被赋过值,它就把标识符当成变量名;否则它就把标识符当成一个方法调用。作为一个比较诡异的例子,请参见以下的代码片段,由Clemens Hintze递交。

def a
  print "Function 'a' called\n"
  99
end

for i in 1..2   if i == 2     print "a=", a, "\n"   else     a = 1     print "a=", a, "\n"   end end
produces:
a=1
Function 'a' called
a=99

在语法解析的时候,Ruby遇到第一个'a',因为此时'a'没有被赋值,就认为它是一个方法调用。然后到第二个打印语句的时候,Ruby已经遇到了一个赋值,所以此时它把'a'当成一个变量。

注意赋值语句不一定要被执行---Ruby只要见到它就可以了。这个程序不会出现错误。

a = 1 if false; a

变量和常量

Ruby变量和常量都是指向对象的引用。变量自身并没有内在的类型。一个变量的类型仅仅由它指向的对象的信息决定。 [我们说一个变量没有类型,我们的意思就是任何给定的变量可以在不同时间指向许多不同的类型。]

一个Ruby的常量也是对一个对象的引用。常量在Ruby首次对它赋值时被创建(一般在一个类或模块的定义中)。和一些通融性差的语言不同,允许我们修改变量的值,尽管这样会得到一个警告信息。

MY_CONST = 1
MY_CONST = 2   # generates a warning
产生结果:
prog.rb:2: warning: already initialized constant MY_CONST

注意虽然常量不应该被修改,但是我们可以修改它指向的对象的内部状态。

MY_CONST = "Tim"
MY_CONST[0] = "J"   #修改常量的内部状态
MY_CONST "Jim"

赋值潜在地给一个对象别名,给同一个对象不同的名字。

常量和变量的作用域

在一个类或模块中定义的常量可以方便地在类或模块任何地方访问。在模块或类外面,它们可以使用域作用符“::”来访问,在一个表达式前加一个“::”可以返回类或模块相应的对象。在任何类外面定义的常量,可以通过在它前面加"::"访问到。我们不能在方法中定义常量。

OUTER_CONST = 99
class Const
  def getConst
    CONST
  end
  CONST = OUTER_CONST + 1
end
Const.new.getConst 100
Const::CONST 100
::OUTER_CONST 99

全局变量在整个程序中都可以使用。每个指向特定全局对象名的引用都返回相同的对象。引用一个未初始化的全局变量返回一个

类变量在整个类或模块体中都可以被使用。类变量在使用之前必须被初始化。一个类变量被这个类的所有对象所共有,然后只在类内部使用。

class Song
  @@count = 0
  def initialize
    @@count += 1
  end
  def Song.getCount
    @@count
  end
end

类变量属于最内层嵌套类或模块。最顶层的类变量是定义在Object中的,然后就像全局变量一样。如果接收者是一个类或者模块,那么定义在单例类里面的类变量属于接收者;否则,它们属于接收者的类。

class Holder
  @@var = 99
  def Holder.var=(val)
    @@var = val
  end
end

a = Holder.new def a.var   @@var end


上一章 < 目录 ^
下一章 >

Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide"
Copyright © 2001 by Addison Wesley Longman, Inc. This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at http://www.opencontent.org/openpub/)).

Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder.

Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.