存疑,知行RUBY文档中心
提供RUBY中文教程、文章、翻译作品

 

返回 :: 返回目录
 
4、Ruby的新特性

翻译:夏克  发表时间:2005-6-5 15:33:57  点击次数:3798

   
刚开始写这本书的时候,我们有一个宏伟的计划(那时我们还年轻),我们希望从头到尾地给出这门语言的文档,从类和对象开始,到语法细节的深入研讨。那时候,这看上去是一个好主意,毕竟,Ruby中的任何东西都是对象,所以从对象开始似乎更有意义。

我们大概就是这样想的。

不幸的是,用这种方式来描述一门语言是很困难的。如果你不介绍字符串、if语句、赋值和其它的细节,很难写出有关类的例子,贯穿我们的从头至尾的描述,我们要附加一些我们所需的低级的细节才能使我们的例子代码有意义。

所以,我们提出了另一个宏伟计划(他们为什么叫我们pragmatic而不是别的),我们仍然从头描述Ruby,但是在此之前,我们加入一篇短章来描述例子中使用到的所有共同的语言特性,连同在Ruby中使用到的特殊词汇,这就像是一个迷你教程,引导我们走进后面的内容。

Ruby是面向对象的语言

我们再重申一遍,Ruby是一个真正的面向对象的语言,你所操作的任何东西都是对象,这些操作本身返回的也是对象。虽然,很多语言都这样宣称,但他们却对面向对象的意思有着不同的解释,或者,对他们所使用的概念套上不同的术语。

所以,在我们深入到细节之前,让我们先简短地看一下我们即将要用到的术语和符号。

当你写面向对象的代码时,通常是把真实世界的物体抽象为模型,然后写入到你的代码中。在这个建模的过程中,你要找出需要用代码来实现的各种物体的种类。比方说在自动唱片点唱机中,“歌曲”也许就是一个种类。在Ruby中,定义一个类来表述每一个实体。一个类是一系列属性的集合(比如,歌曲的名称),方法使用这些属性(可能某个方法是用来播放歌曲的)。

一旦你有了这些类,自然会想到要创建它们的实例。点唱机系统包含一个类叫Song,你要用点唱次数来区分这些实例比如“Ruby Tuesday”、“Enveloped in Python”、“String of Pearls”、“Small talk”等,Object这个单词可以用来表示类实例的意思(作为一个懒惰的打字员,我们可能会经常使用object这个单词)。

在Ruby中,这些对象是调用构造器来创建的,构造器是类的一个特殊方法。标准的构造器叫做new。

song1 = Song.new("Ruby Tuesday")
song2 = Song.new("Enveloped in Python")
# and so on


这些实例都源自同一个类,但它们又都有唯一的特性。第一,每个对象都有一个唯一的object identifier(简写作object id),就是对象标识符。第二,你可以定义实例变量,变量的值对每个实例来说都是唯一的,这些实例变量记录着对象的属性。比如说我们的每一首歌曲,都可能有一个实例变量记录了歌曲的标题。

每一个类你都可以定义实例方法。方法是功能的集合体,可以在类中或者类以外(取决于访问级别)被调用。这些实例方法可以访问对象的实例变量,当然也就可以访问对象的属性。

调用方法是通过发送信息给对象的,这种信息中包含了方法的名字和所需的参数。[这种传递信息来调用方法的方式来自Smalltalk]。如果对象收到了信息,就在自己的类中寻找匹配的方法,若找到了就执行它,若找不到... ...呵呵,我们到后面再说。

这种方法和信息之间的交流看上去很复杂,实际操作一下就会发现很自然。我们来看一些方法调用。(记住例子代码中的箭头表示相应的表达式的返回值。)

"gin joint".length  >>  9  
"Rick".index("c")   >>  2  
-1942.abs           >>  1942  
sam.play(aSong)     >>  "duh dum, da dum de dum ..."  


在这里,点号前的被称为接收者,点号后面的是被调用的方法的名字。第一个例子显示一个字符串的长度,第二个寻找一个字符串中字母C的序号,第三个计算一个数的绝对值,最后一个,我们让sam为我们放一首歌。

这里值得注意的是Ruby和其它语言之间的一个主要的区别,在Java中(比方说),你会看到计算一个数的绝对值需要调用一个单独的函数,把这个数作为参数传递给这个函数,可能你会这样写:

number = Math.abs(number)     // Java code


在Ruby中,计算绝对值的功能内置在数字中----细节是由它们内部自己实现的。你简单地把abs这个信息发送给数字对象,就可以让它完成工作。

number = number.abs


这种特性Ruby的所有对象都具有:在C中你要写strlen(name)而在Ruby中可以是name.length等等,这就是我们为什么要说Ruby是一个真正的面向对象语言的部分原因。

Ruby的一些基本知识

很多人在拿起一门新的语言的时候,都不乐意去看那些枯燥的语法规则,所以我们只能投机取巧,在这一节中,我们要介绍一些精粹,你要写Ruby程序就必须要知道的内容。以后在18章,也就是第199页开始,我们会深入到所有的让人寒颤的细节中。

我们开始于一个简单的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返回给调用者。注意我们不需要声明变量result,当我们赋值给它的时候,它就存在了。

定义了方法后,我们调用了它两次,每次我们都把返回值传递给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的string对象,有很多种方法来创建字符串对象,不过最通用的莫过于使用字符串的字面值(译者注:literal不好翻,参考其它翻译作品,这里采用字面值的翻译,大概意思就是字符串本身):在单引号或者双引号之间的字符序列。两者之间的不同是Ruby在转换字面值到字符串对象时所处理的数量,单引号的情况下会少。除了一些例外,字符串字面值就是字符串的值。

双引号的情况下,Ruby要做更多的处理,首先,它要寻找替代序列就是反斜杠开头的字符,把它替换成二进制值。最常见的是"\n",它被替换成换行符,一个字符串如果包含换行符,那么"\n"强制换行。

puts "And Goodnight,\nGrandma"

 
produces: 
And Goodnight,
Grandma


第二件事就是Ruby要修改双引号括住的字符串,字符串中的#{表达式}序列要用表达式的值来替换,我们用这个来重写前面的方法。

def sayGoodnight(name)
  result = "Goodnight, #{name}"
  return result
end


当Ruby构造这个字符串对象时,它找到name的当前值,并在字符串中替换它。#{...}结构中可以放入任意复杂的表达式,一个简洁的写法是,如果表达式是一个简单的全局变量、实例或者类变量,那么就不必写出大括号。关于字符串的更多内容,和其它的Ruby标准类型,参看第5章,在47页。

最后,我们可以让这个方法更加简化,一个Ruby方法的返回值默认是最后被求的表达式的值,所以我们可以省略掉return语句。

def sayGoodnight(name)
  "Goodnight, #{name}"
end


我们知道这一节很简单,我们讨论了至少一个话题:Ruby名称,为了简单,我们在定义的时候使用了一些术语(像类变量),但是,现在我们要谈到规则,赶在你打游戏之前让我们看看实例变量和类似的一些东西。

Ruby使用一个约定来帮助它区别一个名字的用法:名字前面的第一个字符表明这个名字的用法,局部变量、方法参数和方法名称应该用一个小写字母开头或者一个下划线;全局变量用美元符作为前缀($),而实例变量用@开头,类变量用两个@开头;最后,类名、模块名和常量应该大写字母开头。第10页的表2.1显示了这些不同的名字的例子用法。

词首字母后面可以是字母、数字和下划线的任意组合(规则规定,@后面不可以直接跟数字)。

Example variable and class names
Variables                                         Constants and  
Local         Global     Instance     Class       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 ]   # 三个元素的数组  
# 访问第一个元素
a[0]         >>   1  
# 设置第三个元素的值
a[2] = nil  
# 输出数组的值 
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 = {
  '大提琴'     => '弦乐器',
  '单簧管'     => '木管乐器',
  '鼓'         => '打击乐器',
  '双簧管'     => '木管乐器',
  '小号'       => '铜管乐器',
  '小提琴'     => '弦乐器'
}


索引哈希使用和数组一样的方括号。

instSection['oboe']     >>   "woodwind"  
instSection['cello']    >>   "string"  
instSection['bassoon']  >>   nil  


最后这个例子显示出,如果使用一个不存在的键来索引哈希,默认返回nil。正常情况下这是很方便的,因为nil用在条件表达式中就是false。有时你希望改变这种默认值,例如,如果你使用哈希来计算每一个键出现的次数,那么比较方便的情况是默认值为0,在你创建一个新的空的哈希时改变默认值是很容易的。

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之所以为Ruby的原因,正则表达式当然的也是对象,并且能被处理。

举例,你要写一个模式来匹配一个包含"Perl"或者"Python"的字符串,就用下面的正则表达式:

/Perl|Python/


斜线界定了模式,它包括我们要匹配的两个字符串,用管道符("|")分隔开,你可以在模式中使用圆括号,就像在算术表达中那样,你可以把模式写成这样:

/P(erl|ython)/


也可以在模式中重复声明,/ab+c/匹配一个字符串,它包含一个"a",然后是一个或者多个"b",然后是一个"c"。如果把加号改成星号,就是/ab*c/那么创建的正则表达式是匹配一个"a",0或者更多的"b",和一个"c"。

你也可以用一个模式来匹配一组字符,常见的如"\s"匹配空白字符(空格、Tab、换行符等),"\d"匹配所有的数字,"\w"匹配所有的可打印字符,简单的一个字符"."(一个点号)匹配任意字符。

我们把它们放在一起就可以组合成非常有用的正则表达式。

/\d\d:\d\d:\d\d/    #  类似12:34:56这样的时间
/Perl.*Python/            #  Perl,0或者更多的字符,然后是Python
/Perl\s+Python/           #  Perl,1个或者更多空格,然后是Python
/Ruby (Perl|Python)/      #  Ruby,1个空格,然后是Perl或者Python


匹配算符"=~"用在正则表达式匹配一个字符串的时候。当模式在字符串中被匹配到后,=~返回它开始的位置,否则返回nil。这意味着你可以在if语句或者while语句中使用正则表达式。例如,下面的代码片断显示当一个字符串中包含"Perl"或者"Python"时的情况。

if line =~ /Perl|Python/
  puts "Scripting language mentioned: #{line}"
end


字符串中与正则表达式匹配的部分也可以被替换成不同的文本,这要使用Ruby的替代方法。

line.sub(/Perl/, 'Ruby')    # 用"Ruby"替换第一个"Perl"
line.gsub(/Python/, 'Ruby')       # 用"Ruby"替换所有的"Python"


随着本书的进行,我们还要讨论正则表达式的更多内容。


代码块和迭代器

(译者注:代码块就是blocks的直译)

这一节简短介绍一下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


看看代码块中的代码(puts "In the block") 是如何被执行两次的,就是对yield的每一次调用。

你可以在调用yield时给出参数,这些参数传递给代码块。在代码块中,列举变量的名字来接受参数,这些参数被用"|"括着。

  def callBlock
    yield , 
  end

  callBlock { |, | ... }


代码块贯穿在实现迭代器的Ruby库中,迭代器就是一种方法,用来连续返回某种集合的元素,比如一个数组。

a = %w( ant bee cat dog elk )    # 创建一个数组
a.each { |animal| puts animal }  # 迭代所有的内容
 
produces: 
ant
bee
cat
dog
elk


我们来看看实现Array类的each迭代器的可能的方法,我们要用到前面的例子。each迭代器遍历数组的每个元素,每次都调用yield,类似的代码可能会是下面这样:

# 在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中就是简单的方法调用,这个方法调用所关联的代码块0次或者更多次。

5.times {  print "*" }
3.upto(6) {|i|  print i }
('a'..'e').each {|char| print char }

结果: 
*****3456abcde


在这里,我们让数字5调用一个代码块5次,然后让数字3调用一个代码块,传递给它连续的数值直到6,最后,字符"a"到"e"的区间使用each方法调用一个块。


读写

Ruby带着很完善的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" 告诉printf用一个浮点数(总共允许5位,小数点后两位)和一个字符串来代替。

有很多种方式来把输入读取到你的程序中,也许,最传统的就是使用gets例程,它从你的程序的标准输入流中返回下一行。

line = gets
print line


gets例程有一个附带效果,它除了返回读取的行,还把它储存到全局变量$_中,这个变量很特殊,在很多环境中它作为默认变量使。如果你调用print而没有带参数,它显示$_的内容;如果你写一个if或者while语句时仅仅使用一个正则表达式作为条件,那么这个表达式匹配的对象是$_。尽管一些纯粹主义者把这看作是令人讨厌的野蛮行径,但是这些简写确实又能帮助我们写出简洁的程序来。例如,下面的程序现实输入流中的所有行中包含"Ruby"单词的行。

while gets           # assigns line to $_
  if /Ruby/          # matches against $_
    print            # prints $_
  end
end


Ruby方式的写法是使用迭代器:

ARGF.each { |line|  print line  if line =~ /Ruby/ }


这里使用了预定义对象ARGF,它描述可以被程序读取的输入流。


前进前进前进进

就是这些,我们已经完成了这个闪电般的Ruby基础特性的旅行,我们简单地看了一下对象、方法、字符串、容器和正则表达式,看了一些简单的控制结构,和迭代器,有前途的是,这章给了你充足的弹药去冲锋本书剩下的部分。

随着时间的流逝,你一步步走向更高的水平。下面,我们要看看类和对象,它同时既是Ruby中最高水平的结构也是整个语言最基本的柱石。
版权声明:RUBY文档中心的所有文章标明[原创]的均为本站作品,版权属RUBY中文化计划,若转载请注明;标明[翻译]的其外文版权归原作者,译文版权属RUBY中文化计划;标明[转贴]的,若原作者感到侵犯了他的著作权,那么请及时跟主持人联系,我们会尽快更正。
 

 

 

版权所有(C) RUBY中文化计划