Expressions

一直以来我们使用 Ruby
的表达式时都表现得十分傲慢。毕竟,a=b+c
是标准事物。你完全可以不阅读本章也可以完成大量的 Ruby 代码工作。

但你的代码就不会写得那么愉快。

Ruby
中的第一个不同点在于任何操作都会返回合理的值,就像所有的事物都是表达式一样。实际上这是什么意思呢?

非常明显的就是声明链的功能。

a = b = c = 0                »0
[ 3, 1, 7, 0 ].sort.reverse  »[7, 3, 1, 0]

或许还没有那么明显,一般情况下在 C 和 Java 中的声明都是 Ruby
中的表达式。例如,ifcase
表达式都会返回最后一个表达式的结果。

songType = if song.mp3Type == MP3::Jazz
             if song.written < Date.new(1935, 1, 1)
               Song::TradJazz
             else
               Song::Jazz
             end
           else
             Song::Other
           end

 rating = case votesCast
          when 0...10    then Rating::SkipThisOne
          when 10...50   then Rating::CouldDoBetter
          else                Rating::Rave
          end

我们会从 70 页开始讨论 ifcase 的更多内容。

操作符表达式

Ruby
跟平常的语言一样有一些基本操作符(+,-,*,/等)。完整的操作符列表及它们的优先级在
219 页的表格 18.4 列出。

在 Ruby 中,许多操作符其实是方法的调用。当编写 a*b+c
的时候,你实际上是需要对象引用 a 执行方法 *,并且将 b
作为参数传递。然后你需要将结果对象与 c 参数进行 +
的计算运行。这与下面的写法是等价的

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

因为万物皆对象,并且你也可以定义实例方法,所以你可以通过重新定义基础数学算法获得自己期望的答案。

class Fixnum
  alias oldPlus +
  def +(other)
    oldPlus(other).succ
  end
end
1 + 2  »4
a = 3
a += 4 »8

实际中更有意义的点在于你可以如同构建对象一样参与到操作符表达式的编写中。例如,你也许需要从一首歌曲的中提取指定一段时间的音乐。我们可以通过下标操作符
[] 指定被提取的音乐。

class Song
  def [](fromTime, toTime)
    result = Song.new(self.title + " [extract]",
                      self.artist,
                      toTime - fromTime)
    result.setStartTime(fromTime)
    result
  end
end

这段代码继承于 Song[]
方法,并且定义了两个参数(开始时间和结束时间)。最后会返回一段位于传递的时间间隔间的音乐。接着我们就可以播放一首歌曲的简介了,如同下面代码写的一样:

aSong[0, 0.15].play

混杂表达式

除了操作符表达式和方法调用,以及一些声明表达式(比如 ifcase)之外,Ruby 中还有很多表达式的使用方式。

命令扩张

如果你用反引号或者 %x
前缀包围字符串,它将会以操作系统的命令方式执行。这种表达式的值是命令的标准输出。不过换行不会被影响,就如同有一个尾部返回或者换行符让你从操作系统回来一样。

`date`                  »"Sun Jun  9 00:08:26 CDT 2002\n"
`dir`.split[34]         »"lib_singleton.tip"
%x{echo "Hello there"}  »"Hello there\n"

你也可以使用扩张表达式,以及在命令字符串中使用常用的反义序列。

for i in 0..3
  status = `dbmanager status id=#{i}`
  # ...
end

命令的退出状态值可以存储于全局变量 $?

反引号是灵活的

在命令的转义表达式描述中,我们讲过被反引号包围着的字符串默认以命令的方式执行。实际上,字符串是被传参至
「Kernel::`」方法。你甚至可以重写它。

alias oldBackquote `
def `(cmd)
  result = oldBackquote(cmd)
  if $? != 0
    raise "Command #{cmd} failed"
  end
  result
end
print `date`
print `data`

结果是:

Sun Jun  9 00:08:26 CDT 2002
prog.rb:3: command not found: data
prog.rb:5:in ``': Command data failed (RuntimeError)
from prog.rb:10'

赋值

如同之前所有例子中展示的一样,我们使用了许多赋值的特性。或许也是到我们谈论这些知识的时候了。

赋值声明就是将左边的值或者属性指向右边的值。并且会返回值作为赋值表达式的结果。这意味着你可以使用链式调用赋值,也可以在意想不到的地方使用赋值。

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

赋值的另一种形式是左侧已经包含了对象属性或元素的引用。

aSong.duration = 234
instrument["ano"] = "ccolo"

这些形式都比较特别,因为都是通过左侧值调用方法实现的,这意味着你可以重写它们。

我们已经了解了如何定义可写的对象属性。一般定义一个以等号结尾的名字的方法即可。方法会接收赋值的右侧值为参数。

class Song
  def duration=(newDuration)
    @duration = newDuration
  end
end

自然而然地,属性的设置方法必须与内部实例参数一致,否则就必须为每个可写的属性创建相应的属性读取方法(反之亦然)。

class Amplifier
  def volume=(newVolume)
    self.leftChannel = self.rightChannel = newVolume
  end
  # ...
end

提示:在类中使用访问器

为什么我们在上面的例子中会写为
self.leftChannel?这是因为可写属性有一个隐藏问题。一般情况下,类中的方法都可以调用相同类中或超类中的其它方法(这种情况下隐含了调用者是
self 的条件)。然而,对于属性设置器并不会照常运转。Ruby
解析到赋值操作,并会将左侧作为局部变量处理,而不是作为属性设置器调用。

class BrokenAmplifier
  attr_accessor :leftChannel, :rightChannel
  def volume=(vol)
    leftChannel = self.rightChannel = vol
  end
end
ba = BrokenAmplifier.new
ba.leftChannel = ba.rightChannel = 99
ba.volume = 5
ba.leftChannel   »99
ba.rightChannel  »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
的赋值可以进行有效的平行操作,所以被赋予的值不会被赋值本身影响。右边的值在进行赋值前已经按照匹配顺序评估给了左边的变量或属性。一些特意制作的例子可以说明这种方式。第二行分别将
xx+=1x+=1 的值分别赋值给了 abc 变量。

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

当赋值超过一个左侧值时,右侧的表达式将会返回一个右侧值数组。如果赋值时左侧比右侧值多,多余的左侧值会设置为
nil。如果赋值时右侧比左侧多,额外的值会被忽略。如同 Ruby 1.6.2
描述的,如果赋值时是一个左侧值和多个右侧值,右侧值会被转换为数组并分配给左侧值。

你还可以用平行赋值收缩和展开数组。如果左侧值是以星号起始,所有剩下的右侧值都会被收集并作为数组赋值给左侧值。相似情况下,如果右侧值是数组并且以星号开头,它将会把所有元素展开。(如果右侧只有数组值,它将会自动展开)。

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
将这些条目视作嵌套赋值声明。在继续更高级别的赋值前,会将与嵌套提取一致的右侧值赋值给小括号中的条目。

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(aString)
    @value = aString.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
有几种不同的机制实现条件表达式,多数是常见的,但有一些使用方式会比较不一样。在我们深入了解之前,我们需要先学习布尔表达式。

布尔表达式

Ruby 对「真」有一个简单的定义。只要不是 nil 或者 false
值就是真。你会发现 Ruby 的库将个观念贯彻始终。比如, IO#gets
会将文件内容的下一行返回,并且如果是文件的结尾会返回
nil,因此你便可以循环执行它如下:

while line = gets
  # process line
end

然而,如果是 C,C++,Perl 程序员在此处会遇到一个陷阱。数字
0 并不会如同 false
值一样将循环打断。所以长度为零的字符串也不会作为 false
值使用。这个特点需要大家努力去适应。

defined?,与,或,非

Ruby 也支持标准布尔值运算符,并且还有个新运算符 defined? 需要介绍。

如果运算值都是 true,那 and&& 运算结果都会是
true。只有在第一个值是 true
时才会运算第二个值(这种情况被称为「短路计算」)。两个操作符的不同之处在于优先级(and&& 级别要低)。

类似地,or|| 只要任意运算符为 true 时计算结果就为
ture。只有第一个值为 false
它们才会运算第二个值。在两个运算符间的区别也只是优先级,就如同 and
&& 一样。

生活总是充满惊喜,andor 的优先级是一致的,但 &&
的优先级高于 ||

not! 返回与计算值相反的结果(如果计算值是 true,就返回
false;如果计算值是 flase,就返回 true)。并且 not!
也只是优先级不同而已。

所有的优先级规则都在 219 页的表 18.4 做了总结。

如果参数(可以是任意表达式)没有定义,defined? 会返回
nil,否则它会返回参数的描述。如果参数是
yielddefined? 会返回字符串
「yield」及当前上下文关联的代码块。

defined? 1                »"expression"
defined? dummy            »nil
defined? printf           »"method"
defined? String           »"constant"
defined? $&               »nil
defined? $_               »"global-variable"
defined? Math::PI         »"constant"
defined? ( c,d = 1,2 )    »"assignment"
defined? 42.abs           »"method"

关于布尔操作符另说一句,Ruby 对象也支持比较方法,比如
=====<=>=~eql?equal?(可以参见 79 页的表
7.1)。除了 <=> 之外,其它的比较方法都在 Object
对象中有过定义,但通常都被继承类按照自己合适的操作重新定义了。例如,类
Array
为了比较两个数组,当它们的元素数量和相应元素都相等时就为相等,重写了
== 方法。

Common comparison operators

OperatorMeaning
==Test for equal value.
===Used to test equality within a when clause of a case statement.
<=>General comparison operator. Returns -1, 0, or +1, depending on whether its receiver is less than, equal to, or greater than its argument.
<, <=, >=, >Comparison operators for less than, less than or equal, greater than or equal, and greater than.
=~Regular expression pattern match.
eql?True if the receiver and argument have both the same type and equal values. 1 == 1.0 returns true, but 1.eql?(1.0) is false.
equal?True if the receiver and argument have the same object id.

===~ 都有相反的运算 !=
!~。但是当程序被读取的时候 Ruby 将对它们进行转换。a!=b 等于
!(a==b)a!~b!(a=~b)
一致。这意味着如果你编写的类重写了 ===~
你就会自然影响到 !=
!~。另一方面来说,你就不能分别将 !=!~===~
独立定义。

你也可以使用 Ruby 范围作为布尔表达式使用。比如 exp1..exp2
这样的表达式如果 exp1 不为 true,那结果就为 false。直到
exp2 也为 true 时范围才为
true。一旦如此,范围将会被重置,直到下次触发。我们还在 82
页准备了一些例子。

最后,你还可以使用纯粹的正则表达式作为布尔表达式。Ruby 会将其展开为
$_=~/re/

If 和 Unless 表达式

Ruby 中的 if 表达式和其他语言中的 if 声明是相似的。

if aSong.artist == "Gillespie" then
  handle = "Dizzy"
elsif aSong.artist == "Parker" then
  handle = "Bird"
else
  handle = "unknown"
end

如果是使用多行 if 声明可以不使用 then 关键词。

if aSong.artist == "Gillespie"
  handle = "Dizzy"
elsif aSong.artist == "Parker"
  handle = "Bird"
else
  handle = "unknown"
end

不过如果你希望你的代码更加紧凑,你可以使用 then
关键词将布尔表达式与后面的声明分离。

if aSong.artist == "Gillespie" then  handle = "Dizzy"
elsif aSong.artist == "Parker" then  handle = "Bird"
else  handle = "unknown"
end

你可以使用零个或更多 elsifelse 子句。

正如我们所说,if
是表达式不是声明,所以它会返回值。虽然没有强制要求使用 if
的返回值,但它迟早有用。


handle = if aSong.artist == "Gillespie" then
           "Dizzy"
         elsif aSong.artist == "Parker" then
           "Bird"
         else
           "unknown"
         end

Ruby 也有 if 声明的反向类型。

unless aSong.duration > 180 then
  cost = .25
else
  cost = .35
end

最后对于 C 语言粉丝而言,Ruby 也支持 C
语言风格的条件表达式:

cost = aSong.duration > 180 ? .35 : .25

这个条件表达式会将冒号前或后的值返回,返回哪个值取决于问号前的表达式是
true 还是 false。这个例子中,如果歌曲长度大于 3 分钟表达式返回
.35,如果是小于或等于 3 分钟的歌曲返回
.25。无论返回结果是什么都将赋值给 cost

If 和 Unless 修改器

Ruby 吸取了 Perl
语言中一个优雅的特性。声明修改器允许你在一般声明之后使用条件表达式。

mon, day, year = $1, $2, $3 if /(\d\d)-(\d\d)-(\d\d)/
puts "a = #{a}" if fDebug
print total unless total == 0

对于 if 修改器,只有条件是 true
时才会执行之前的表达式。而 unless 是相反的。

while gets
  next if /^#/            # Skip comments
  parseLine unless /^$/   # Don't parse empty lines
end'

因为 if
不是个表达式而是声明,你会觉得声明修改器将变得不清晰,例如:

if artist == "John Coltrane"
  artist = "'Trane"
end unless nicknames == "no""'"

这会将你导向疯狂。

Case 表达式

Ruby 的 case 表达式是种有用的方法,如同多个 if

case inputLine

  when "debug"
    dumpDebugInfo
    dumpSymbols

  when /p\s+(\w+)/
    dumpVariable($1)

  when "quit", "exit"
    exit

  else
    print "Illegal command: #{inputLine}"
end

就像 if 一样,case
表达式返回最后一个表达式的执行结果,如果表达式和条件需要编写为一行的话需要使用
then 关键词。


kind = case year
         when 1850..1889 then "Blues"
         when 1890..1909 then "Ragtime"
         when 1910..1929 then "New Orleans Jazz"
         when 1930..1939 then "Swing"
         when 1940..1950 then "Bebop"
         else                 "Jazz"
       end

case 是通过将目标(在 case 后的表达式)与每个 when 后的表达式进行比较的运算。比较是通过 === 进行。只要类对 === 进行定义,在 case 表达式中类的对象就会使用自定义的 ===

例如,正则表达式将 === 定义为模式匹配。

case line
  when /title=(.*)/
    puts "Title is #$1"
  when /track=(.*)/
    puts "Track is #$1"
  when /artist=(.*)/
    puts "Artist is #$1"
end

Ruby 类是 Class
的实例,如果匹配参数是类的实例或其中一个超类就将使用 Class
自定义的
===。因此你可以比较对象的类(这其实放弃了多态的特性,也是在不断提醒你进行重构)。

case shape
  when Square, Rectangle
    # ...
  when Circle
    # ...
  when Triangle
    # ...
  else
    # ...
end

循环

不要告诉任何人,尽管 Ruby 有很基础的循环构造。

只要条件表达式是 true,while
就会执行循环体零次或多次。例如下面读取输入,直到输入停止。

while gets
  # ...
end

不过循环也有反向形式,除非条件表达式为 true,否则不会停止。

until playList.duration > 60
  playList.add(songList.pop)
end

就像 ifunless 一样,循环也可以用作声明修改器。

a *= 2 while a < 100
a -= 10 until a < 100

在第 78 页的布尔表达式小节中,我们有谈及 range
可以作为一种触发器,当第一件事情发生并且为 true
而第二件事情暂未发生时返回
true。这个功能一般在循环中使用。下面的例子中,我们会读取一个包含序数(first,second 等等)的文件,但只打印在 「third」到「fifth」间的内容。

file = File.open("ordinal")
while file.gets
  print  if /third/ .. /fifth/
end

结果是:

third
fourth
fifth

range
中的元素被使用在布尔表达式中时本身也可以作为表达式。这些值在每次布尔表达式计算后进行求值。例如,下面的代码实际上使用了
$.
变量,它包含了输入的行号,并且可以将行号与这些匹配条件匹配成功的输出。

file = File.open("ordinal")
while file.gets
  print if ($. == 1) || /eig/ .. ($. == 3) || /nin/
end

结果是:

first
second
third
eighth
ninth

当把 whileuntil
用作声明修改符时有个问题。如果被修改的声明是 begin/end
代码块,块中的代码会至少执行一次,与布尔表达式的值无关。

print "Hello\n" while false
begin
  print "Goodbye\n"
end while false

结果是:

Goodbye

迭代器

如果你阅读了前一部分的开头,你可能会感到灰心。那里说「Ruby
有很简单的循环构造方法」。不要灰心,绅士的阅读者,这有一些好消息。Ruby
不需要复杂的循环构造,因为我们还可以使用 Ruby 迭代器。

例如,Ruby 没有如同 C,C++ 和 Java 的 for
循环。不同的是,Ruby
使用基于不同类的方法定义完成类似的功能,不过这样更不容易出错,也更加函数化。

让我们看一些例子。

3.times do
  print "Ho! "
end

结果是:

Ho! Ho! Ho!

它可以避免越界和下标偏离 1 的问题,上面的循环将被执行 3 次。除了
times,整型数可以在指定范围内进行循环,比如
downtouptostep。例如,传统的 for
循环可以像接下来这样写。

0.upto(9) do |x|
  print x, " "
end

结果是:

0 1 2 3 4 5 6 7 8 9

0 到 12 每次间隔 3 的循环可以像下面这样写

0.step(12, 3) {|x| print x, " " }

结果是:

0 3 6 9 12

与之相似的是,通过数组和容器的 each
方法可以使它们的遍历很简单。

[ 1, 1, 2, 3, 5 ].each {|val| print val, " " }

结果是

1 1 2 3 5

而且一旦一个类支持 each 方法时,Enumerable
模块中的方法对其也是可用的。例如,File 类提供了 each
方法用于遍历文件返回的每行文字。同时我们也可以对每行文字使用带指定条件的
grep 方法。

File.open("ordinal").grep /d$/ do |line|
  print line
end

结果是:

second
third

最后还有一种比较活用的基础循环写法。Ruby 提供了 loop 迭代器。

loop {
  # block ...
}

loop
迭代器永远调用关联的代码块(至少在你未跳出循环前是如此,不过你必须阅读一下后面的章节,才知道如何跳出当前循环)。

For…In

更早之前我们只提到了 Ruby 中的基本循环是 whileuntil。那么
for 呢? for 只是一种语法糖。当你这样写时

for aSong in songList
  aSong.play
end

Ruby 会将它作如下翻译:

songList.each do |aSong|
  aSong.play
end

for 循环和 each
之间唯一的不同之处是循环体中定义的局部变量。我们要在 87 页再进行讨论。

你可以用 for 迭代对 each 方法有响应的对象,比如 Array
Range

for i in ['fee', 'fi', 'fo', 'fum']
  print i, " "
end
for i in 1..3
  print i, " "
end
for i in File.open("ordinal").find_all { |l| l =~ /d$/}
  print i.chomp, " "
end

结果是:

fee fi fo fum 1 2 3 second third

只要是定义了 each 方法的类,你就可以用 for 循环遍历。


class Periods
  def each
    yield "Classical"
    yield "Jazz"
    yield "Rock"
  end
end

periods = Periods.new
for genre in periods
  print genre, " "
end

结果是:

Classical Jazz Rock

Break, Redo 及 Next

循环控制构造 breakredonext
可以让你改变循环和迭代的正常流向。

break 会立即打断循环,控制在下一次代码块中恢复。redo
会从开始重复整个循环,但不会再评估条件或者获取下一元素。next
会跳过循环的结尾,然后再开始下一迭代。


le gets
  next if /^\s*#/   # skip comments
  break if /^END/   # stop at end
                    # substitute stuff in backticks and try again
  redo if gsub!(/`(.*?)`/) { eval($1) }
  # process line ...
end

这几个关键词也可以在以迭代为基础的循环体系中

i=0
loop do
  i += 1
  next if i < 3
  print i
  break if i > 4
end

结果是:

345

Retry

redo
声明会引起一个循环重复当前的迭代。尽管有时你需要使循环从头回到开始的地方。retry
也可以实现类似功能。retry
可以使任何类型的迭代循环重新开始。

for i in 1..100
  print "Now at #{i}. Restart? "
  retry if gets =~ /^y/i
end

当你运行程序并进行交互时你应该会看到


Now at 1. Restart? n
Now at 2. Restart? y
Now at 1. Restart? n
 . . .

retry 会再次评估重启的参数。线上的 Ruby
文档使用下面的样例,你可以使用 until 改造它。

def doUntil(cond)
  yield
  retry unless cond
end

i = 0
doUntil(i > 3) {
  print i, " "
  i += 1
}

结果是:

0 1 2 3 4

变量域和循环

whileuntilfor
循环都是基于语言构建的,并且也没有介绍新的领域。之前已经存在的局部变量可以在循环中使用,任何新的局部变量也可以在循环后面创建。

被迭代使用的代码块有一些不同。一般情况下,在代码块中创建的变量在代码块外面并不能访问。

[ 1, 2, 3 ].each do |x|
  y = x + 1
end
[ x, y ]

结果是:

prog.rb:4: undefined local variable or method `x'
for #<Object:0x401c2ce0> (NameError)'

不过,如果代码块执行的局部变量已经有同名的变量存在,已经存在的局部变量将会被代码块使用。变量的值会因此在代码块结束后依然可用。下面的例子显示,这同时适用于代码块中的一般变量,也适用于代码块参数。

x = nil
y = nil
[ 1, 2, 3 ].each do |x|
  y = x + 1
end
[ x, y ]                 »[3, 4]

本文翻译自《Programming Ruby》,主要目的是自己学习使用,文中翻译不到位之处烦请指正,如需转载请注明出处

本章原文为
Expressions

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值