2006-07-13

第八章 异常,catch和throw

第八章 异常,catch和throw

迄今为止我们已在Pleasantville内开发了代码,这是个没有错误的地方。每个库都可成功调用,用户从不会输入错误数据,资源是丰富和廉价的。当然这并不真实。欢迎回到现实中来。


在现实生活中,会有错误发生。好的程序(和程序员)可预见到错误并优雅地安排对错误的处理。但是这并不总是像说起来这么容易。通常,用于侦测错误的代码并不真的知道它们在做什么。例如,在有些情况下,试图打开一个不存在文件的做法可能是合理的,而这在其它时候可是个致命错误。你的文件处理模块用来做什么?

传统方式是使用返回代码。open方法返回一些特定值来说明它失败了。然后这个值在调用例程层中向外传播,直到有人出于责任而捕获它。

这个方式的问题是管理这么多错误代码可能是件痛苦的事。如果一个函数调用open,然后是read,最后是close,而这每一步都可能返回一个错误代码,这个函数如何能够区别调用返回的这些错误代码。

在很大范围内用异常来解决这个问题。异常让你包装有关的错误信息到一个对象内。然后这个异常对象被自动从调用堆栈向外传播,直到运行时系统找到,明确地声明了它知道如何处理这个异常类型的代码。

Exception

包含有关一个异常信息的包是个Exception类或它的子类的对象。Ruby预定义了一个简洁的异常层次,像下一页中表8.1显示的。稍后我们会看到,用这个层次处理异常相当地容易。

当你需要引发一个异常时,你可使用内建的Exception类,或你自己创建的异常类中的一个。如果你自己创建,你可能需要让它成为StandardError或它的子类的一个子类。如果你不这么做,你的异常在缺省情况下不会被捕获。

每个异常都有相关的消息字符串一个堆栈轨迹。如果你定义了自己的异常,你可以添加额外的信息。

处理异常

我们自动点唱机使用一个TCP套接字在互联网上下载歌曲。基本代码很简单(假设filenamesocket已经设置完了)

op_file = File.open(opfile_name, "w")

while data = socket.read(512)

op_file.write(data)

end

如果我们在下载到一半时出现了一个致命的错误,这会怎样?当然,我们不会将一个完整的歌曲存储到歌曲列表中。 “I Did It My *click*.”

让我们添加一些异常处理代码,看看这会有什么帮助。为了对异常进行处理,我们封装引起异常的代码在一个begin/end块中,并且使用一或多个rescue子句来告诉Ruby我们想处理的异常类型。在这个特定的例子中,我们只感兴趣SystemCallError异常(当然也包括它的子类),所以它会出现在rescue行中。在错误处理块内,我们报告错误,关闭和删除输出文件,然后重新引发这个异常。

op_file = File.open(opfile_name, "w")

begin

# Exceptions raised by this code will

# be caught by the following rescue clause

while data = socket.read(512)

op_file.write(data)

end

rescue SystemCallError

$stderr.print "IO failed: " + $!

op_file.close

File.delete(opfile_name)

raise

end

当一个异常出现时,它有单独的异常处理,Ruby放置相关的异常对象的引用在全局变量$!(异常指针令人惊异地会镜像我们可能引起错误的任何代码)。在前面例子中,我们使用$!变量来格式化我们的错误消息。

在关闭和删除文件后,我们调用无参数的raise,它重新引发$!内的异常。这是个很有用的技术,它允许你编写这样的代码,过滤异常,传递你没有处理的那些异常到上层。它几乎就像是实现的一个对错误处理的继承层次。

你可在一个begin块内使用多个rescue子句,而每个resuce子句可以指定多个要捕获的异常。在每个rescue子句末端时你可以给出一个Ruby的局部变量来接收匹配的异常。很多人认为在下述位置上使用$!更易于阅读。

begin

eval string

rescue SyntaxError, NameError => boom

print "String doesn't compile: " + boom

rescue StandardError => bang

print "Error running script: " + bang

end

Ruby是如何确定哪个rescue子句被执行的?它将它们处理成类似于使用case语句。对begin块内的每个rescue子句,Ruby将被引发异常与每个参数依次比较。如果被引发的异常匹配一个参数,Ruby执行这个rescue子句体并停止查找。这个匹配使用parameter===$!操作完成。对于大多数异常,这意味着如果rescue子句内的异常名若与当前被抛出的异常的类型相同,或是那个异常的一个子类,则匹配将会成功。(注:这种比较这所以会发生,是因为异常是类,类是种模块。而模块内定义的===方法,在做为操作数的类与被调的祖先相同时返回true)如果你写了无参数rescue子句,则它的缺省参数是StandardError

如果没有找到匹配的rescue子句,如果一个异常在begin/end块的外部被引发,Ruby在堆栈内移出调用者并从调用者内查找一个异常处理程序,然后是调用者内调用者,依次类推。

尽管rescue子句的参数通常是Exception类的名字,但实际上它们可以是能返回一个异常类的任意表达式(包括方法调用)

系统错误

但调用操作系统并返回一个错误代码时系统错误被引发。在POSIX系统上,这些错误有如EAGAINEPERM等样的名字。(如果你使用Unix系统,你可以输入manerrno来获得这些错误的列表。)

Ruby接受这些错误并将它们包装一个特定的异常对象内。每个对象都是SystemCallError的子类,每个对象都被定义在Errno模块内。这意味着将可使用类名称如Errno::EAGAIN,Errno::EIO,Errno::EPERM来寻找异常。如果你想获取操作系统的错误代码,每个Errno异常对象都有个类常量叫(有点乱)Errno,它就包含这个值。

Errno::EAGAIN::Errno => 35

Errno::EPERM::Errno => 1

Errno::EIO::Errno => 5

Errno::EWOULDBLOCK::Errno => 35

注意EWOULDBLOCKEAGAIN有同样的错误号码。这是用于产生个块的计算机操作系统的一个特征两个常量映射到同一错误号码。要处理它,Ruby会排列它们以便Errno::EAGAINErrno::EWOULDBLOCKrescue子句中被同等对待。你在一个rescue中请求,你就会处理这两者。通过重新定义SystemCallError#===,以便If you ask to rescue one, you’ll rescue either. It does this by redefining SystemCallError#=== so that if two subclasses of SystemCallError are compared, the comparison is done on their error number and not on their position in the hierarchy.

清理

有时候你需要保证代码块的最后部分也会被处理,而不管是否有异常被引发。例如,你可能在进入块时打开一个文件,并且需要确保在块退出时文件会被关闭。

使用ensure子句会做到这点。ensure出现在最后的rescue子句后面,它所包含的代码块在块中止时,总是会得到执行的。它不考虑块是正常退出,还是引发了异常及处理这个异常,或由一个未捕获异常而引起的中止,ensure块都将被执行。

f = File.open("testfile")

begin

# .. process

rescue

# .. handle error

ensure

f.close unless f.nil?

end

else子句也是一样的构造,尽管它不是很有用。如果出现的话,它在rescue子句的后面和任何ensure子句的前面。else子句体只有在主代码体没引发异常时才会被执行。

f = File.open("testfile")

begin

# .. process

rescue

# .. handle error

else

puts "Congratulationsno errors!"

ensure

f.close unless f.nil?

end

再次恢复执行

有时候你可能要纠正异常的错误。在这些情况下,你可以在一个rescue子句中使用retry语句来重新进入到begin/end块中。明显地,这是个无穷尽的循环,所以使用这个特征要小心。

这个例子的代码会再次审理一个异常,可以查看Minero Aokinet/smtp.rb库。

@esmtp = true

begin

# First try an extended login. If it fails because the

# server doesn't support it, fall back to a normal login

if @esmtp then

@command.ehlo(helodom)

else

@command.helo(helodom)

end

rescue ProtocolError

if @esmtp then

@esmtp = false

retry

else

raise

end

end

这个代码首先试图使用EHLO命令来连接一个SMTP服务,这个命令没有得到广泛的支持。如果连接失败,代码设置@esmtp变量为false,并且会试图重新连接。如果第二次又失败了,则引发异常给调用者。

主动引发异常

到现在我们一直是在防御,处理由它人引发的异常。你也可反过来。

你可以在你的代码中使用Kernel.raise方法(同义词是Kernel.fail)来引发异常。

raise

raise "bad mp3 encoding"

raise InterfaceException, "Keyboard failure", caller

第一种形式只是简单地重新引发当前异常(或者不是当前异常的RuntimeError)。这可以被用在需要在异常被传递前中途截住它的异常处理程序中。

第二种形式创建一个新的RuntimeError异常,用指定字符串设定它的消息。然后在调用堆栈引这个异常。

第三种形式的第一个参数创建一个异常,然后用第二个参数设置相关消息和堆栈轨迹给第三个参数。典型地,第一个参数即可以是异常层次内的类名字(注:技术上,这个参数可以是任何对象,该对象通过返回object.kind_of?(Exception)true的对象来表示异常消息。),也可以是这些类的一个对象实例的引用。堆栈轨迹通常使用Kernel.caller方法来产生。

这儿是些raise的典型例子。

raise

raise "Missing name" if name.nil?

if i >= names.size

raise IndexError, "#{i} >= size (#{names.size})"

end

raise ArgumentError, "Name too big", caller

在最后例子中,我们从堆栈轨迹中移除了当前例程,它通常在库模块中很有用。我们可使用这个特征。下面代码通过传递调用堆栈的一个子集给新的异常来从堆栈轨迹中移除两例程。

raise ArgumentError, "Name too big", caller[1..1]

给异常添加信息

你可以定义你自己的异常,它持有你需要传递给错误的任何信息。例如,某些类型的网络错误所依赖的环境是短暂的。如果这样一个错误发生,则环境是主要的,你可以在异常中设置个标志来告诉处理者,它是否值得去重新操作。

class RetryException <>

attr :ok_to_retry

def initialize(ok_to_retry)

@ok_to_retry = ok_to_retry

end

end

一个短暂的错误在代码的某处发生。

def read_data(socket)

data = socket.read(512)

if data.nil?

raise RetryException.new(true), "transient read error"

end

# .. normal processing

end

我们调用上一层堆栈来处理异常。

begin

stuff = read_data(socket)

# .. process stuff

rescue RetryException => detail

retry if detail.ok_to_retry

raise

end

Catch Throw

raiserescue的异常机制中,当出现错误时,它们会放弃异常。有时候这很好,可让你在通常的处理期间从很深的嵌套结构中跳出来。此处的catchthrow也很方便。

catch (:done) do

while line = gets

throw :done unless fields = line.split(/t/)

songlist.add(Song.new(*fields))

end

songlist.play

end

catch定义带有给定名字(可以SymbolString)标签的块。块通常会被执行直到它遇见throw

Ruby遇到一个throw时,它展开调用堆栈来查找带有匹配符号的catch块。当找到时,Ruby在此点上展开堆栈并中止块。所以前面例子中,如果输入中不包含当前被格式化的行,throw将跳到相应的catch的尾部,不只是中止while循环也跳过歌曲列表的播放。如果在调用throw时使用了可选的第二个参数,则catch的值会被做为返回值。

下面例子如果用户对提示的回答是!时,使用throw来中止与用户的会话。

def prompt_and_get(prompt)

print prompt

res = readline.chomp

throw :quit_requested if res == "!"

res

end

catch :quit_requested do

name = prompt_and_get("Name: ")

age = prompt_and_get("Age: ")

sex = prompt_and_get("Sex: ")

# ..

# process information

end

就像这个例子显示的,throw没有必要出现在catch的静态作用域中。

第七章 表达式(4-2)

第七章 表达式(4-2)

Defined?, And, Or, Not

Ruby支持所有标准的布尔操作,另外,还引入了新的操作符defined


操作符and && 两者的值都为true才会返回true。第一个值为真,才会对第二个操作数求值(有时这也被称为短路求值)。这两个操作符的区别是优先级不同(and低于 &&

类似的 or || 有一方操作数为true就会返回true。如果第一个为false,它们就会对第二个操作数求值。类似and,这两个操作符只有优先级的不同。

有趣的是,andor有相同的优先级,而&&的优先级高于||

not! 返回操作数的相反的值(如果操作数为true,则这个操作符返回false,如果操作数为false,则返回true)。并且nod和!也只是优先级不同。

所有的这些操作符和优先级的总结在324页表22.4中。

操作符defined?将返回nil,如果参数(可以是任意表达式)没有定义的话。否则,将返回后面参数的描述信息。 如果参数是yielddefined?(在代码块与当前上下文环境关联)返回字符串”yield”

defined? 1 => "expression"

defined? dummy => nil

defined? printf => "method"

defined? String => "constant"

defined? $_ => "globalvariable"

defined? Math::PI => "constant"

defined? a = 1 => "assignment"

defined? 42.abs => "method"

除了这些布尔表达式,Ruby对象还支持使用方法 ==, ===, <=>, =~, eql?, equal?进行对象之间的比较(见表7.1)。除了<=>之外这些操作符都在Object类中定义,但是经常被子类重载。比如,类Array重定义了==方法,判断两个数组相同的条件事它们元素的个数相同,同一位置的元素也相同。

Table 7.1. 通用比较操作符

操作符

意义

==

测试值是否相等

===

case语句的when子句中用于每个条目与目标比较

<=>

通用比较操作符。返回 −1, 0, +1, 这依赖于被调小于,等于,大于它的参数。

<, <=, >=, >

小于,小于等于,大于等于和大于的比较操作符。

=~

正则表达式模式匹配

eql?

如果被调和参数两者类型相同并且值相等返回true1 == 1.0 返回true,但是1.eql?(1.0) false

equal?

如果被调和参数有同样的ObjectID,返回true

== =~ 两者有否定形式, != and !~. 但是,当你的程序被读入时这些会由Ruby转换。a != b !(a == b)是相等的,而 a !~ b !(a =~ b)也是一样的。 这意味着如果你的类覆写了== =~ ,你会自动得到!= !~ 。同时,你也不能离开了===~而孤立的定义!=!~两个方法。

你可以使用Ruby range 作为一个布尔表达式,一个类似 exp1..exp2 range只有在遇到exp1true,然后exp2又为true之后,才会返回true。我们将在94页显示这个例子。

先前的Ruby1.8中,你可以用正则表达式来当作一个布尔表达式。现在不赞成这样做。你仍可以使用~操作符(580页描述的)来与一个模式匹配$_

逻辑表达式的值

在书中,我们这样说如果两个操作符是true,则求值为true。但实际上要比这复杂些。操作符andor,&&||实际上返回它们的第一参数,该参数决定条件的truefalse。重要的是,这是什么意思?

拿表达式”val1 and val2”来说,如果val1falsenil,那么我们知道这个表达式不会为true。在这种情况下,val1的值决定了表达式的整个值,所以它是被返回的值。如果val1有其它值,那么整个表达式值则依赖于val2,因此它值是返回值。

nil and true => nil

false and true => false

99 and false => false

99 and nil => nil

99 and "cat" => "cat"

Note that despite all this magic, the overall truth value of the expression is correct.

The same evaluation takes place for or (except an or expression’s value is known early if val1 is not false).

false or nil => nil

nil or false => false

99 or false => 99

A common Ruby idiom makes use of this.

words[key] ||= []

words[key] <<>

The first line is equivalent to words[key] = words[key] || []. If the entry in the hash words for key is unset (nil), the value of || will be the second operand, a new, empty array. Thus, this line of code will assign an array to a hash element that doesn’t already have a value, leaving it untouched otherwise. 有时你也会看到将它们写一行上:

(words[key] ||= []) <<>

If Unless表达式

Ruby中的if语句跟其他语言类似。

if song.artist == "Gillespie" then

handle = "Dizzy"

elsif song.artist == "Parker" then

handle = "Bird"

else

handle = "unknown"

end

如果你的if语句写在多行上,可以省略then关键字。

if song.artist == "Gillespie"

handle = "Dizzy"

elsif song.artist == "Parker"

handle = "Bird"

else

handle = "unknown"

end

但是,如果你的语句都写在一行上,then关键字应该写上来分开布尔表达式和后面的语句。

if song.artist == "Gillespie" then handle = "Dizzy"

elsif song.artist == "Parker" then handle = "Bird"

else handle = "unknown"

end

1.8中你也可使用冒号:来替换then,使代码更简洁些。

if song.artist == "Gillespie": handle = "Dizzy"

elsif song.artist == "Parker": handle = "Bird"

else handle = "unknown"

end

你可以使用0个或多个elsif子句,和一个可选的else子句。

就像我们前面说道的,if是一个表达式,不是一个语句,它可以返回一个值,你不必使用if表达式的返回值,但是它可能有些用处。

handle = if song.artist == "Gillespie" then

"Dizzy"

elsif song.artist == "Parker" then

"Bird"

else

"unknown"

end

Ruby也未if提供了一个否定的形式,unless

unless song.duration > 180

cost = 0.25

else

cost = 0.35

end

最后,也为使用C语言的程序员准备了C风格的条件表达式:

cost = song.duration > 180 ? 0.35 : 0.25

这个条件表达式在根据?前面的布尔值为truefalse返回冒号前面或后面的值。在这个例子中,如果歌曲的时长大于3分钟,将返回.35,否则返回.25,然后,将这个值赋给cost

If Unless 修饰句

Ruby也借鉴了Perl的一些特点,语句修饰句使我们可以在普通语句的末尾加上条件语句。

mon, day, year = $1, $2, $3 if date =~ /(dd)(dd)(dd)/

puts "a = #{a}" if debug

print total unless total.zero?

对于if修饰句来说,只有当if后面的条件为true,前面的语句才会执行,unless正好和if相反。

File.foreach("/etc/fstab") do |line|

next if line =~ /^#/ # Skip comments

parse(line) unless line =~ /^$/ # Don't parse empty lines

end

因为if本身也是表达式,所以下面的写法将会使代码变得难懂。

if artist == "John Coltrane"

artist = "'Trane"

end unless use_nicknames == "no"

This path leads to the gates of madness.

第七章 表达式(4-1)

第七章 表达式(4-1)

到目前为止我们已经用了一些基本的表达式,毕竟,abc是最基本的了,你即使不看本章,也能写出一大堆的Ruby代码来。但是那样做不是什么有趣的事情;-)


Ruby和其它语言的一个不同之处就是任何东西都能返回一个值:几乎所有的东西都是个表达式。在实际中,这有什么意义呢?

一些明显得作用是可以实现链式语句:

a = b = c = 0 => 0

[ 3, 1, 7, 0 ].sort.reverse => [7, 3, 1, 0]

或许不是很明显,CJAVA中通常的语句,在Ruby中都是表达式。例如,ifcase两者都返回最后的表达式执行后返回的值。

song_type = if song.mp3_type == MP3::Jazz

if song.written <>

Song::TradJazz

else

Song::Jazz

end

else

Song::Other

end

rating = case votes_cast

when 0...10 then Rating::SkipThisOne

when 10...50 then Rating::CouldDoBetter

else Rating::Rave

end

我们会在90页祥细地讨论ifcase语句。

操作符表达式

Ruby提供的基本操作符集()让人有些惊讶。完整的操作符列表和优先级在324页表22.4中给出。

Ruby中,很多操作符实际上是做为方法调用实现的。例如,当你执行a*b+c时,实际上是对a的对象引用执行方法*,传递参数b给它。然后你请求从做为计算结果的对象上再执行+方法,传递参数c给它。这实际上等于写:

(a.*(b)).+(c)

因为万物皆对象,所以你可以重定义实例方法,如果你不喜欢现有基本操作符的工作方式,你总是可以重新定义它。

class Fixnum

alias old_plus +

# Redefine addition of Fixnums. This is a BAD IDEA!

def +(other)

old_plus(other).succ

end

end

1 + 2 => 4

a = 3

a += 4 => 8

a + a + a => 26

很有用的一个技巧是你自己写的的类可以像内建对象一样参与操作符的操作,比如,我们想从一首歌中间某处开始提取一部分音乐,我们可以用索引操作符"[ ]"来完成对指定音乐的抽取:

class Song

def [](from_time, to_time)

result = Song.new(self.title + " [extract]",self.artist,to_time from_time)

result.set_start_time(from_time)

result

end

end

这段代码扩展了类Song,增加了[ ]方法,这个方法接收两个参数(开始时间和结束时间)。这个方法返回一个新的Song对象,这个对象是歌曲的一部分。然后,我们就可以这样播放这段音乐:

song[0, 15].play

混合表达式(Miscellaneous Expressions

除了明显的操作符表达式和方法调用,和不是很显眼的语句表达式(比如ifcase),Ruby还支持在表达式中使用更多的东西。

命令展开 Command Expansion

如果你用反引号(`)来括起来一个字符串,或者用定界形式的前缀%x{ }括起来,那么这个表达式中的字符串默认得会作为底层的操组系统命令来执行。表达式的值是那个命令的标准输出。换行符将不会从结果中去掉,所以返回结果一般都会包含一个回车符。

`date` => "Thu Aug 26 22:36:31 CDT 2004n"

`ls`.split[34] => "book.out"

%x{echo "Hello there"} => "Hello theren"

你也可以在命令字符串中使用表达式展开和所有通常的转义序列。

for i in 0..3

status = `dbmanager status id=#{i}`

# ...

end

执行的命令的返回状态存放在全局变量$?中。

重定义反引号方法

在命令输出表达式中描述中,我们说,反引号之中的字符串将"默认"被做为一个命令来执行。实际上,这个字符串是传递给了Kernel::` 这个方法(一个反引号)来执行。如果你愿意,可以重写这个方法。

alias old_backquote `

def `(cmd)

result = old_backquote(cmd)

if $? != 0

fail "Command #{cmd} failed: #$?"

end

result

end

print `date`

print `data`

produces:

Thu Aug 26 22:36:31 CDT 2004

prog.rb:10: command not found: data

prog.rb:5:in ``': Command data failed: 32512 (RuntimeError)

from prog.rb:10

赋值

我们前面的例子中都涉及到了赋值这一基本表达式,下面,我们来讨论一些关于赋值语句的东西。

一个赋值语句给在左侧的一个变量或属性(左值lvalue)设定一个在它右侧指定的值(右值)。然后这个值作为赋值表达式的返回值返回。也就是说,我们可以用链式赋值来给一些变量赋值:

a = b = 1 + 2 + 3

a => 6

b => 6

a = (b = 1 + 2) + 3

a => 6

b => 3

File.open(name = gets.chomp)

Ruby中有两种基本的赋值格式,第一种是将对象引用赋值给变量或常量。这种形式被紧密连接到语言中的。

instrument = "piano"

MIDDLE_A = 440

另一种是在赋值语句左边使用对象的属性或者元素的引用。

song.duration = 234

instrument["ano"] = "ccolo"

这些形式比较特殊,因为它们是通过调用左值的方法来实现的,它意味着你可覆写它们。

我们已经看过如何定义一个可修改的对象属性。只需要简单的在方法后面以等号结尾即可。这个方法把接收的参数作为赋值语句的右值。

class Song

def duration=(new_duration)

@duration = new_duration

end

end

这些属性设置方法没有必须与内部的实例变量一致,你也不需要为每个属性提供读取方法(反过来也是一样)

class Amplifier

def volume=(new_volume)

self.left_channel = self.right_channel = new_volume

end

end

在老Ruby版本中,赋值语句的结果是由属性设置方法返回的值。在1.8中,赋值的值总是参数的值;方法的返回值被丢弃。

class Test

def val=(val)

@val = val

return 99

end

end

t = Test.new

a = t.val = 2

a => 2

在老版本中,a被赋值语句设置为99,在1.8中被设置为2

在类中使用存取器方法

84页中的例子中为什么我们必须要写 self.leftChannel 而不能省掉self呢?一般的,可修改属性都有一个隐藏的gotcha。通常,一个类中的方法可以直接调用同类或者父类中的其他方法(也就是说,暗中带有self被调)。但是,对于属性修改方法来说这就不管用了,Ruby将把左面的名字作为一个局部变量,而不是一个对写属性方法的调用。 

class BrokenAmplifier

attr_accessor :left_channel, :right_channel

def volume=(vol)

left_channel = self.right_channel = vol

end

end

ba = BrokenAmplifier.new

ba.left_channel = ba.right_channel = 99

ba.volume = 5

ba.left_channel => 99

ba.right_channel => 5

我们在leftChannel前面忘了写self.了,所以ruby把这个新值赋给了一个方法volume=的一个局部变量,而这个对象的属性没有任何变化。这可能会经常产生问题。

并行赋值

在学习了一段时间程序设计之后,我们可能会遇到要求将两个变量的值互换:

int a = 1;

int b = 2;

int temp;

temp = a;

a = b;

b = temp;

Ruby中很简单,只需要:

a, b = b, a

Ruby可以有效的实现并行赋值,所以被分派的值不会影响到赋值本身。在右边的值在被赋给左面的变量或属性之前按照它们的顺讯进行求值,然后对应的赋给左面的属性或变量。一个例子如下,第二行给abc的值分别是xx+=1x+=1计算之后的值。

x = 0 => 0

a, b, c = x, (x += 1), (x += 1) => [0, 1, 2]

当一个赋值语句有多于一个的左值时,这个赋值表达式返回值是个右值的数组。如果一个赋值语句的左值多余右值,多余的左值被设为nil,反过来如果右值多余左值,那么多余的右值将被忽略。如果左值只有一个,右值有多个,那么这些右值将作为一个数组赋给左值。

你也可以在并行赋值语句中分解和扩展数组。如果最后的左值以星号作为前缀,那么所有对应这个得值和以后的值将会组成一个数组,赋给这个左值(如下面第三行的c);类似的,如果最后一个右值是一个数组,你可以加一个星号作为前缀,Ruby将会把这个数组拆开按相应的位置赋给左值(如下面第六行的c,而且,如果这个数组是唯一的右值,这个星号是可以省略的,作为右值得数组自动拆开,如第二行所示)。

a = [1, 2, 3, 4]

b, c = a => b == 1, c == 2

b, *c = a => b == 1, c == [2, 3, 4]

b, c = 99, a => b == 99, c == [1, 2, 3, 4]

b, *c = 99, a => b == 99, c == [[1, 2, 3, 4]]

b, c = 99, *a => b == 99, c == 1

b, *c = 99, *a => b == 99, c == [1, 2, 3, 4]

嵌套赋值

并行赋值还有一个值得一提的特性,赋值语句左边还可以包括用括号括起来的变量列表,Ruby中叫做嵌套赋值语句。Ruby首先摘出右值中相应的项进行赋值,然后在进行高层的赋值操作。

b, (c, d), e = 1,2,3,4 => b == 1, c == 2, d == nil, e == 3

b, (c, d), e = [1,2,3,4] => b == 1, c == 2, d == nil, e == 3

b, (c, d), e = 1,[2,3],4 => b == 1, c == 2, d == 3, e == 4

b, (c, d), e = 1,[2,3,4],5 => b == 1, c == 2, d == 3, e == 5

b, (c,*d), e = 1,[2,3,4],5 => b == 1, c == 2, d == [3, 4], e == 5

其它赋值形式

像其它语言一样,ruby也为a=a+2提供了类似a+=2的快捷方式。

第二种方式是第一种的深入,这意味着你已定义的那个操作符在你的类内可做为方法来工作。

class Bowdlerize

def initialize(string)

@value = string.gsub(/[aeiou]/, '*')

end

def +(other)

Bowdlerize.new(self.to_s + other.to_s)

end

def to_s

@value

end

end

a = Bowdlerize.new("damn ") => d*mn

a += "shame" => d*mn sh*m*

你不会在Ruby中找到CJava中的增量(++)和减量(--)操作符。使用+==形式来代替。

条件执行(Conditional Execution

Ruby有几种不同的机制来实现代码的条件执行;大多数都感觉很类似,也有一些很灵巧。在深入讨论之前,我们先来花点时间看看布尔表达式。

Boolean 表达式

Ruby中的true定义很简单。任何不是nil和常量false的东西都是true。你会发现系统的实现库中很多这种用法。比如,IO#gets ,用来返回一个文件的下一行,如果到了文件末尾,返回nil,所以,我们才可以这样通过while来循环读取数据:

while line = gets

# process line

end

但是,这里对于CC++perl程序员来说有一个误区,数字0和长度为0的字符串都不会被解释成false值,需要注意。