2005年06月16日

转载自[Ruby中文化计划]  翻译:夏克 

一个只有一首歌曲的点唱机很少见(除非在非常非常特殊的场合),所以不久我们就开始考虑制作一个歌单和播放列表。它们两个都是容器:用来保存对一个或者多个对象的引用的对象。

歌单和播放列表有一些相似的方法:添加一首歌、删除一首歌,返回一个歌曲列表等等。播放列表还应该增加一些其它的功能,比如每次播放时插入广告,记录累计播放时间等。不过我们先不去考虑这些问题,现在最好先创建一个通用的SongList类供歌单和播放列表使用。


容器

在实现以前,我们需要决定怎样在SongList对象中储存歌曲列表,有三种方法,使用Ruby的Array类型或者Hash类型,要不然就自己做一个列表结构。偷懒的结果就是先来看看数组和哈希,从中选择一个用在我们的类中。

数组

Array类保存着对象引用的集合。每一个对象的引用在数组中都有一个位置,通过一个非负整数来索引。

你可以使用字面值或者直接生成一个Array对象,一个字面上的数组是被方括号括住的一个对象序列。

a = [ 3.14159, "pie", 99 ]
a.type         >>      Array  
a.length       >>      3  
a[0]           >>      3.14159  
a[1]           >>      "pie"  
a[2]           >>      99  
a[3]           >>      nil  
 
b = Array.new  
b.type         >>      Array  
b.length       >>      0  
b[0] = "second"  
b[1] = "array"  
b              >>      ["second", "array"] 



数组通过[]操作符来索引,就像大多数的Ruby操作符,这实际上也是一个方法(在Array类中)所以也可以在子类中重载,如例中所示数组索引从0开始。使用一个single整数来索引数组,返回该位置的对象,如果在那个位置没有对象则返回nil。如果使用一个负整数索引数组,那么它从数组尾端返回,参看35页的表4.1。


a = [ 1, 3, 5, 7, 9 ]  
a[-1]          >>      9  
a[-2]          >>      7  
a[-99]         >>      nil  



也可以使用一对数字来索引数组,[start,count]。这会返回一个新数组,它由从start开始的count个对象的引用组成。


a = [ 1, 3, 5, 7, 9 ]  
a[1, 3]          >>      [3, 5, 7]  
a[3, 1]          >>      [7]  
a[-3, 2]         >>      [5, 7]  



(译者注:注意负整数索引的方向依然是从前向后)

最后你也可以使用区间来索引数组,开始和结束位置之间插入两个或者三个点,两个点的形式表示包含结束位置,三个点不包含。


a = [ 1, 3, 5, 7, 9 ]  
a[1..3]         >>      [3, 5, 7]  
a[1...3]        >>      [3, 5]  
a[3..3]         >>      [7]  
a[-3..-1]       >>      [5, 7, 9]  



[]操作符对应的有[]=操作符,通过它可以设置数组元素的值。用一个single整数索引,把操作符右边的值赋给该位置的元素。中间所产生的空隙用nil来填补。


a = [ 1, 3, 5, 7, 9 ]           >>      [1, 3, 5, 7, 9] 
a[1] = ’bat’                    >>      [1, "bat", 5, 7, 9] 
a[-3] = ’cat’                   >>      [1, "bat", "cat", 7, 9] 
a[3] = [ 9, 8 ]                 >>      [1, "bat", "cat", [9, 8], 9] 
a[6] = 99                       >>      [1, "bat", "cat", [9, 8], 9, nil, 99] 



如果[]=的索引有两个数(开始和长度)或者是一个区间,那么在原始数组中的对应元素就会被操作符右边的值代替;如果索引的长度为0,那么右边的值就插入到开始位置前面,不删除元素;如果右边也是一个数组,它的元素代替原始数组的元素;如果索引所选择的元素数目和右边的不一样,那么就自动调整数组的大小来适应。

(译者注:值得一提的是如果出现了空隙,依旧用nil来填补)


a = [ 1, 3, 5, 7, 9 ]         >>      [1, 3, 5, 7, 9] 
a[2, 2] = ’cat’               >>      [1, 3, "cat", 9] 
a[2, 0] = ’dog’               >>      [1, 3, "dog", "cat", 9] 
a[1, 1] = [ 9, 8, 7 ]         >>      [1, 9, 8, 7, "dog", "cat", 9] 
a[0..3] = []                  >>      ["dog", "cat", 9] 
a[5] = 99                     >>      ["dog", "cat", 9, nil, nil, 99] 



数组有大量有用的方法,通过它们你可以把数阻当成堆、栈、集、队列、双列、先入先出列等。278页有完整的数组方法列表。

哈希

哈希(有时被认为是数组和字典的结合)和数组一样是用来储存对象引用的集合。

不过,区别于通过整数来索引数组,你可以通过任意类型的对象来索引哈希:字符、正则表达式等。在哈希中保存元素实际上是保存了两个对象—-键和值。用键可以索引到对应的值。哈希中的值可以是任意类型的对象,下面的例子使用了哈希字面值:括号括起来的键值对。


h = { ’dog’ => ’canine’, ’cat’ => ’feline’, ’donkey’ => ’asinine’ }  
 
h.length         >>      3  
h['dog']         >>      "canine"  
h['cow'] = ’bovine’  
h[12]    = ’dodecine’  
h['cat'] = 99  
h                >>      {"donkey"=>"asinine", "cow"=>"bovine", "dog"=>"canine", 12=>"dodecine", "cat"=>99}  



对比于数组,哈希有一个显见的好处就是可以使用任意对象做索引,但同时一个显见的不好处就是它的元素是无序的,所以你不能简单地把哈希用作堆栈或者队列。

你很快会发现哈希是ruby中使用最广泛的一类数据结构。317页有完整的哈希类中实现的方法列表。

实现一个SongList容器

上面简单讨论了一下数组和哈希,下面我们来实现点唱机的SongList类。我们先列出在SongList中所需的基本的方法的一个列表,我们希望随着我们的进度不断丰富它,现在先把它做出来。

append( aSong ) >> list 
      向列表中添加指定的歌曲

deleteFirst() >> aSong 
      从列表中删除第一首歌曲并返回该歌曲

deleteLast() >> aSong 
      从列表中删除最后一首歌并返回该歌曲

[anIndex] >> aSong 
      从列表中返回anIndex所索引的歌曲,可以是整数索引或者歌曲的标题。
(译者注:这里要实现一个操作符方法即[]方法)

这个列表给我们一些实现方法的提示。在列表尾端添加歌曲的功能,在最前和最后位置删除歌曲的功能。建议使用双列—-一个两端队列—-这样我们可以使用Array来实现,同样,数组也支持用一个整数来索引歌曲。

但是我们也需要使用歌曲标题来索引歌曲,可能会想到使用哈希,那样用标题做键歌曲做值。那么可以使用哈希吗?也许可以,不过这样有问题。首先哈希是无序的,所以我们不得不使用一个辅助的数组来跟踪列表。一个更大的麻烦是哈希不支持多个键对应一个值,这会给我们的播放列表带来麻烦,因为一首歌可能会被播放许多次。我们可以在一个歌曲的数组中搜索需要的歌曲标题,如果这会成为执行上的瓶颈,那么我们会在后面加入一些基于哈希的查找特性。

我们从一个基本的initialize方法开始我们的类,创建一个数组用来存放歌曲和一个引用它的实例变量@songs。


class SongList
    def initialize
        @songs=Array.new
    end
end



SongList#append方法在@songs数组末尾添加歌曲,返回它自己也就是当前的SongList对象。这是一个有用的特性,可以让我们把多个append调用联接在一起,后面会看到这个例子。


class SongList
    def append(aSong)
        @songs.push(aSong)
        self
    end
end



然后添加deleteFirst和deleteLast方法,简单地用Array#shift和Array#pop来分别实现。


class SongList
    def deleteFirst
        @songs.shift
    end
    def deleteLast
        @songs.pop
    end
end



让我们来快速地测试一下,在列表中添加四首歌曲。炫耀一下,我们用append返回的SongList对象来联接这些方法调用。


list = SongList.new
list.
  append(Song.new(‘title1′, ’artist1′, 1)).
  append(Song.new(‘title2′, ’artist2′, 2)).
  append(Song.new(‘title3′, ’artist3′, 3)).
  append(Song.new(‘title4′, ’artist4′, 4))



然后检查一下列表的开始和结束位置是否正确,当列表空的时候返回nil。


list.deleteFirst        >>      Song: title1–artist1 (1)  
list.deleteFirst        >>      Song: title2–artist2 (2)  
list.deleteLast         >>      Song: title4–artist4 (4)  
list.deleteLast         >>      Song: title3–artist3 (3)  
list.deleteLast         >>      nil



很好,下一个方法是[],通过索引来访问元素。如果索引是整数(在这里我们用Object#kind of?来检查),那么返回该位置的元素。


class SongList
  def [](key)
    if key.kind_of?(Integer)
      @songs[key]
    else
      # …
    end
  end
end



再来测试一下


list[0]         >>      Song: title1–artist1 (1)  
list[2]               >>      Song: title3–artist3 (3)  
list[9]               >>      nil  



现在需要添加通过歌曲标题来索引的功能,这要求扫描整个歌曲列表,检查每一首歌曲标题。在这之前,我们需要先来熟悉一下Ruby最简洁的一个特性:迭代器。


代码块和迭代器

(译者注:关于代码块,Ruby的作者在2003年9月的访谈中提及到,参看注3)

下一个问题是实现SongList的[]方法,它用一个字符串来搜索一首歌曲的标题,看起来很简单:我们有一个歌曲的列表,遍历整个列表依次匹配每一首歌曲的标题即可。


class SongList
  def [](key)
    if key.kind_of?(Integer)
      return @songs[key]
    else
      for i in 0…@songs.length
        return @songs[i] if key == @songs[i].name
      end
    end
    return nil
  end
end



它能工作,并且看上去也很熟悉,一个for循环遍历整个数组,能不能做的更自然些呢?

事实上有更自然的方法。这里我们的for循环要求数组的一些私有信息,它要求数组的长度,然后按序匹配每一个值。为什么不要求数组仅提供一个对其每个元素的检测呢?这正是Array的find方法所做的。


class SongList
  def [](key)
    if key.kind_of?(Integer)
      result = @songs[key]
    else
      result = @songs.find { |aSong| key == aSong.name }
    end
    return result
  end
end



我们还可以把if用作语句修饰符来缩短句子。


class SongList
  def [](key)
    return @songs[key] if key.kind_of?(Integer)
    return @songs.find { |aSong| aSong.name == key }
  end
end



find方法是一个迭代器,一个重复调用代码块的方法。迭代器和代码块是Ruby最有趣的特性中的两个,所以我们花些时间来研究一下它们(这个过程中我们也可以看到我们的[]方法是如何真正工作的)。

实现迭代器

一个Ruby迭代器不过是一个简单的方法,它可以调用代码块。初看Ruby的代码块很像是C、Java或者Perl中的代码块,不幸的是,不是这样—-Ruby的代码块是一种组合语句的途径但不是一种方便的途径。

首先,一个代码块出现在一个方法调用的代码附近,代码块和方法的最后一个参数处在同一行;第二,代码块中的代码并不被执行,而是Ruby保存代码块出现时的上下文关系(局部变量、当前对象等等),然后进入到方法中。这正是魅力所在。

在方法中,代码块通过yield语句被调用,这使得代码块就好像是一个方法一样。当yield执行时,它调用代码块中的代码。代码块退出时,控制就马上被返回给yield后面的语句。[程序设计语言的粉丝会很高兴看到yield关键字被采用在这里,它模拟了Liskov的CLU语言中的yield功能,这是一个有着20年历史的语言,仍然保留着远没有被非CLU语言所广泛采用的许多特性。(注2)]我们看一个简单的例子:


def threeTimes
  yield
  yield
  yield
end
threeTimes { puts "Hello" }


 
结果: 

Hello
Hello
Hello



(译者注:这里的代码块是puts "Hello"这条语句,而方法是threeTimes,当语句执行到threeTimes { puts "Hello"}这行时,puts "Hello"不是马上被执行,而是由Ruby先保存puts "Hello"这条语句和threeTimes的关系,然后进入到threeTimes中,遇到第一条yield语句的时候调用并执行puts "Hello"语句,执行完毕后返回到第一条yield语句的后面也就是第二条yield语句,直到三条yield语句都被执行了,才返回到threeTimes{puts "Hello"}这条语句后)

代码块(大括号括住的部分)和threeTimes方法的调用相关联,在这个方法中,yield被调用了三次,每次它都调用代码块中的代码,然后打出一个愉快的问候。不过,代码块最有趣的地方是你可以给它传递参数还可以取得它的返回值。比如我们写一个简单的函数来返回一个特定值的菲波纳奇数列。[菲波纳奇数列是一个整数序列,以两个1开始,随后的每一项都是它前面两项之和,这个序列常常用在排序算法和分析自然现象当中。]


def fibUpTo(max)
  i1, i2 = 1, 1        # 并行赋值
  while i1 <= max
    yield i1
    i1, i2 = i2, i1+i2
  end
end
fibUpTo(1000) { |f| print f, " " }


 
结果:

1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987



在这个例子中,yield有一个参数,这个值被传递给关联的代码块。代码块的定义中,参数出现在两个竖条之间。这个例子中,变量f接受了yield传递来的值,所以代码块就连续地显示出数列的数了。(这个例子也展示了并行赋值,75页还有更多介绍。)尽管经常给代码块传递一个参数,但不是必须的,代码块可以有很多参数。如果代码块的参数数目和yield传递来的不一样时该怎么办呢?这和并行赋值下的规则是一致的(些小的差别是:如果代码块只有一个参数那么yield传递来的多个参数会转换成一个数组)。

代码块的参数也可能是已存在的局部变量,如果是这样的话,代码块执行完毕后变量的新值会被保留下来。这可能会导致不可预测的结果,但是她也带来一个性能上的优势,变量已经存在了。(关于这方面的更多信息和其它的“gotchas”参看127页,更多的性能信息在128页。)

代码块也可以给方法返回值,在代码块中最后被计算的表达式的值被作为yield的值回传给方法。Array类中的find方法就是如此。[find方法实际上在Enumerable模块中被定义,该模块混合在Array中。]它的实现就像下面这样。


class Array  
  def find  
    for i in 0…size  
      value = self[i]  
      return value if yield(value)  
    end  
    return nil  
  end  
end

[1, 3, 5, 7, 9].find {|v| v*v > 30 }        >>      7  



数组中的元素连续地传递给关联的代码块,如果代码块返回true方法就返回对应的元素,如果没有匹配的元素方法就返回nil。例子显示了使用这种迭代器的好处。Array类完成了它所能完成的,访问数组元素,撇开程序代码而专注于自身特定的需求(在这个例子中,找到符合数学标准的元素)。

许多迭代器通用于大多数类型的ruby集合,我们已经见识过find,另外两个是each和collect,each可能是最简单的迭代器—-它所做的就是连续返回集合中的元素。


[ 1, 3, 5 ].each { |i| puts i }


 
结果:

1
3
5



each迭代器在ruby中有着特殊的地位,在85页我们描述了它怎样用在最基本的for循环中,在102页还会看到怎样定义一个each方法以便在你的类中自由地添加更多的功能。

另一个通用的迭代器是collect,它从集合中取得每一个元素然后传递给代码块。代码块返回的结果用来生成一个新的数组,例如:


["H", "A", "L"].collect { |x| x.succ }      >>      ["I", "B", "M"]  



Ruby与C++和Java的比较

有必要用一个段落比较一下Ruby在迭代器方面和C++与Java的不同。迭代器就是一个简单的方法,和其他方法一样,当它产生一个新值的时候就调用yield。使用迭代器是很简单的,把一个代码块和方法相关联即可,不需要Java和C++那样生成帮助类来承载迭代器的状态,在这方面,也在其它的很多方面,ruby是一种透明的语言,你写一个ruby程序,只要集中精力于完成工作,而不必搭建用来支持语言本身的脚手架。

迭代器在访问已经存在数据的数组和哈希时没有限制,就像我们在菲波纳奇例子中看到的,一个迭代器能返回传来的值。这种能力被用在Ruby的输入/输出类中,它们实现了一个迭代器的界面来返回一个I/O流中连续的行(或者字节)。


f = File.open("testfile")
f.each do |line|
  print line
end
f.close



produces: 


This is line one
This is line two
This is line three
And so on…



看一下另一种迭代器的实现,在Smalltalk语言中也支持集合的迭代器,如果你要求Smalltalk程序员求数组元素的和,他们会像这样来使用inject函数:


sumOfValues              "Smalltalk method"
    ^self values
          inject: 0
          into: [ :sum :element | sum + element value]



inject是这样工作的,当关联的代码块第一次被调用时,sum被赋给inject的参数值(在这里是0),element取数组第一个元素。第二次和以后调用到代码块时,sum被赋给上次调用代码块时返回的值,这样sum就跑完了全程,inject最终的结果是代码块最后被调用的值。

ruby没有inject方法,但是很容易实现。这个例子中,我们把它增加到Array类中。在100页我们可以看到如何使它更通用。


class Array  
  def inject(n)  
     each { |value| n = yield(n, value) }  
     n  
  end  
  def sum  
    inject(0) { |n, value| n + value }  
  end  
  def product  
    inject(1) { |n, value| n * value }  
  end  
end  
[ 1, 2, 3, 4, 5 ].sum      >>      15  
[ 1, 2, 3, 4, 5 ].product      >>      120  



尽管迭代器经常使用在代码块上,但也可以用在其它方面,我们来看一下。

事务代码块

代码块可以定义为在某种事务控制下运行的一系列代码,举例来说,你经常要打开一个文件,对文件的内容进行一些处理,然后确保在使用完毕后关闭了它。你可以使用常规的代码来完成这些,不过我们这里要表现的是如何让文件自己负责关闭它自己,我们要用代码块来做。看下面这个粗略的实现(忽略了错误处理):


class File
  def File.openAndProcess(*args)
    f = File.open(*args)
    yield f
    f.close()
  end
end

File.openAndProcess("testfile", "r") do |aFile|
  print while aFile.gets
end


 
结果:


This is line one
This is line two
This is line three
And so on…


 
这个小例子展示了一些技巧。OpenAndProcess方法是一个类方法,这意味着它不依赖于任何特定的File对象就可以被调用。我们希望它能像常规的File.open方法那样获得同样的参数,但我们又确实不想关心这些参数具体是什么,所以,我们指定参数为*args,意为“把实参放到数组中传递给方法”,然后我们调用File.open,把*args作为一个参数传递给它。数组被分成单独的参数。来来回回的结果是OpenAndProcess透明地传递它所接收的参数给File.open了。

一旦文件被打开,OpenAndProcess调用yield,把打开的文件对象传递给代码块。当代码块返回后,文件被关闭,这样,关闭的责任就从文件对象的使用者转移给文件本身了。

最后,这个例子使用do…end来定义代码块。使用这种形式和使用大括号的形式之间的区别在于,do…end定义要低级于{…},这在234页会有详细讨论。

这种文件管理自己的生命周期的技巧非常有用,所以Ruby的File类直接就支持了这种特性。如果File.open有一个关联的代码块,那么这个代码块将会随一个文件对象而被调用,到代码块终止时文件对象被关闭。这很有趣,意味着File.open有两种不同的行为。当调用它的时候有代码块,它执行代码块然后关闭文件;如果没有代码块,它返回文件对象。这种特性因为Kernel::block_given?的存在而成为可能,如果当前方法有关联的代码块,返回true。使用它,你可以像下面这样来实现File.open(又一次忽略了错误处理):


class File
  def File.myOpen(*args)
    aFile = File.new(*args)
    # 如果有代码块,传递文件,然后等代码块返回时关闭文件
    if block_given?
      yield aFile
      aFile.close
      aFile = nil
    end
    return aFile
  end
end



代码块可以转换为闭包

让我们回到我们的点唱机,还记得它吗。我们要用一些代码来完成用户界面,比如人们用来选择歌曲和控制点唱机的按钮。我们需要把这些按钮和某些行为联系起来:按下STOP按钮,歌曲停止。Ruby的代码块用来完成这个就最方便不过了。我们先假设人们已经用硬件实现了一个Ruby的扩展,它给了我们一个基本的按钮类(在169页我们讨论扩展Ruby)


bStart = Button.new("Start")
bPause = Button.new("Pause")
# …



当用户按下其中一个按钮时发生了什么?在Button类中,硬件调用一个回调方法,buttonPressed。为这些按钮增加功能的显见的途径就是创建Button类的子类,在每一个子类中实现它们自己的buttonPressed方法。


class StartButton < Button
  def initialize
    super("Start")       # 调用Button的initialize
  end
  def buttonPressed
    # 开始播放…
  end
end

bStart = StartButton.new



这里有两个问题,第一个,这会导致出现大量的子类。如果Button的界面改变了,我们需要非常多的维护。第二,按钮按下时执行的操作处在一个错误的级别,它们不应该是按钮的特性,而应该是点唱机的特性,我们用块来修改这些错误。


class JukeboxButton < Button
  def initialize(label, &action)
    super(label)
    @action = action
  end
  def buttonPressed
    @action.call(self)
  end
end

bStart = JukeboxButton.new("Start") { songList.start }
bPause = JukeboxButton.new("Pause") { songList.pause }



这里关键是JukeboxButton#initialize的第二个参数。如果一个方法的最后一个参数有&前缀,Ruby就会在方法被调用时查找一个代码块,这个代码块被转变成一个Proc类的对象并且分配成参数。你可以把这个参数当作任意的变量。在我们的例子中,我们把它赋给实例变量@action。当回调方法buttonPressed被调用时,我们使用对象的proc#call方法来调用块。

(译者注:如果你感到不好理解,我来解释一下:看这一句bStart = JukeboxButton.new("Start") { songList.start },当bStart引用的对象实例化时,调用JukeboxButton#initialize,这时{ songList.start }被作为第二个参数传递给JukeboxButton#initialize,在JukeboxButton#initialize中,实例变量@action被赋值成{ songList.start },等到JukeboxButton#buttonPressed被执行时,@action通过proc#call来调用块{ songList.start })

创建一个Proc对象需要很多工作吗?有趣的是,它不过比一堆代码多一点点东西而已。和一个代码块关联的(因此就是一个Proc对象)就是这个代码块被定义时的上下文关系:self的值,方法,变量,常量等的视图,Ruby充满魔力的地方就是代码块可以一直使用所有的这些原始视图信息,即使定义它时的环境已经消失,在其它的语言中,这种能力被称为闭包

让我们看一个例子,这个例子使用了proc方法,它把一个代码块转换成一个Proc对象。


def nTimes(aThing)  
  return proc { |n| aThing * n }  
end  
 
p1 = nTimes(23)  
p1.call(3)      >>      69  
p1.call(4)      >>      92  
p2 = nTimes("Hello ")  
p2.call(3)      >>      "Hello Hello Hello "  



nTimes方法返回一个Proc对象,它引用了方法的参数aThing,尽管参数已经不在代码块被调用的时间范围,但仍然保留了对代码块的访问能力。



[注2]

谈到Liskov,想要简单介绍一下OOP的历史:

面向对象技术鼻祖是挪威人克里斯坦.尼加德(Kristen Nygaard),他在1962年发明了颇具传奇色彩的Simula语言,并在该语言中创造出了许多沿用至今的面向对象概念。1970年前后,阿兰.凯(Alan Kay)与他的同事们在施乐(Xerox)公司发明了优雅的、纯粹的Smalltalk语言。Smalltalk提出了许多新概念,如消息和继承机制等。同样在1970年代,芭芭拉.莉丝柯夫(Barbara Liskov)使抽象数据结构的理论和实现获得了重大进展。她在LISP语言的基础上,通过增加面向对象机制,发明了著名的CLU语言,该语言支持隐藏内部数据的设计方法。此外,1980年代初期诞生的Ada语言也为面向对象技术贡献了泛型和包等重要概念。在Ada语言的基础上,格雷迪.布彻(Grady Booch)还首次提出了"面向对象设计"这一现代软件工程术语。

Ruby汲取了许多OOP语言的精髓,所以在这个学习的旅程中,如果碰到了你的熟人,大可开怀一笑,万不可学那小气之人,以为某种语言是自己的独家兵器,别人使不得的样子。


[注2由夏克补充]



[注3]

在这里我们把Ruby作者关于代码块和闭包的谈话全文粘贴:

使用 Blocks 做循环抽象

Bill Venners: 
  Ruby 支持 blocks 和 Closure 结构. 什么是 blocks 和 Closure,他们如何使用? 

Yukihiro Matsumoto:
  Blocks 基本上就是匿名函数。你可能熟悉诸如Lisp 或 Python等其他语言中的Lambda 函数。 你可以向另外一个函数传递一个匿名函数,这个函数可以调用这个被传递过来的匿名函数。例如, 函数可以通过一次传递给匿名函数一个元素来执行循环迭代。在那些可以将函数当作第一类型的编程语言中,这是个通常的方式,称为高排序函数样式。 Lisp 可以这样,Python 也是如此,甚至就连C 也可以通过函数指针实现这点。很多其他语言也可以做这样的编程。在 Ruby中,不同之处只是在高排序函数语法风格上有所不同。在其他语言中,你必须显示的指出一个函数可以接受另外一个函数作为参数。但是在Ruby 中,任何方法都可以 Block 作为一个隐性参数被调用。在方法中,你可以使用 yield关键字和一个值来调用 block. 

Bill Venners: 
  Block 的好处是什么? 

Yukihiro Matsumoto:
  基本上,Block 是被设计来做循环迭代抽象的。Block 最基本的使用就是让你以自己的方式定义如何循环迭代。 

  例如,如果你有一个列表,序列,矢量组或者数组,你可以通过使用标准库中提供的方法来实现向前循环迭代,但是如果你想从后往前实现循环迭代呢?如果使用 C 语言,你得先设置四件事情:一个索引,一个起始值,一个结束条件和一个递增变量。这种方式不好,因为它暴露了列表的内部实现方法,我们希望能够隐藏内部逻辑,通过使用 Block 我们可以将内部循环迭代的方式隐藏在一个方法或者函数中。比如,调用list.reverse_each,你可以对一个列表实现一个反向的循环迭代,而不需要知道列表内部是如何实现的。 

Bill Venners: 
  就是说,我传递一个 Block 结构,这个 Block 中的代码可对循环迭代中每个元素做任何事情,至于如何反向遍历就取决于List 本身了。换句话说,我就是把原本在 C 语言 Loop 循环中写的那些代码作为一个 Block 来传递。 

Yukihiro Matsumoto:
  对,这意味着你可以定义许多迭代的方式。你可以提供一种向前循环迭代的方式,一种向后循环迭代的方式,等等。这全取决于你了。C#也有迭代器,但是它对于每个类只有一个迭代器。在 Ruby 中你可以拥有任意数量的迭代器。例如,如果你有一个 Tree 类,可以让人以深度优先或者广度优先的方式遍历,你可以通过提供两种不同的方法来提供两种遍历方式。 

Bill Venners: 
  让我想想是否我了解了这点,在 Java 中,它们是通过 Iterator 接口实现抽象迭代的,例如,调用程序可以让 Collection 来实现 Iterator。但是调用程序必须使用循环来遍历Iterator 返回的元素。在 For 循环中, 我的代码实现对每个循环迭代的元素的处理,这样循环语句将总是显示在调用程序中。 使用 Block , 我并不调用一个方法来获取一个迭代器,我只是调用一个方法,同时将我希望对循环迭代中每个要处理的元素的处理代码作为一个 Block 块结构传递给该函数。 Block 的好处是不是将一些代码从调用程序中的 for 循环中提取出来。 

Yukihiro Matsumoto:
  实现循环迭代的具体细节应该属于提供这个功能的类。调用程序应该尽可能的少知道这些。这就是 Block 结构的本来目的。实际上,在早期版本的 Ruby 中,使用 Block 的方法被称为迭代器,因为它们就是被设计来实现循环迭代的。但是在 Ruby发展过程中,Block的用途在后来已经得到了很大的增强,从最初的循环抽象到任何事情。 

Bill Venners: 
  例如。。。。 

Yukihiro Matsumoto:
  我们可以从Block 中创建一个 Closure 对象,一个 Closure 对象就是像 Lisp中实现的那种匿名函数。 你可以向任何方法传递一个匿名函数(即 Closure)来自定义方法的行为。另外举个例子,如果你有一个排序的方法用于排序数组或者列表,你可以定义一个 Block来定义如何在元素之间进行比较,这不是循环迭代。这不是个循环,但是它使用了 Block 。 


使用Closures 

Bill Venners: 
  什么使得 Block 成为了一个 Closure? 

Yukihiro Matsumoto:
  Closure 对象包含可以运行的代码,是可执行的,代码包含状态,执行范围。也就是说在Closure 中你捕捉到运行环境,即局部变量。因此,你可以在一个Closure中引用局部变量,即是在函数已经返回之后,他的执行范围已经销毁掉,局部变量依然作为一部分存在于Closure对象中,当没有任何对象引用它的时候,垃圾搜集器将处理它,局部变量将消失。 

Bill Venners: 
  这么说,局部变量基本上是被方法和Closure 对象共享的?如果 Closure 对象更新了变量,方法可以看到,如果方法更新了变量,Cosure 对象也可以看到。 

Yukihiro Matsumoto:
  是的,局部变量在Closure 和方法之间共享,这是真正的 Closure,它不仅仅是复制。 

Bill Venners: 
  一个真正的 Closure 有什么好处?一旦我将一个 Block 变为一个 Closure,我能用它做什么? 

Yukihiro Matsumoto:
  你可以将一个 Closure 转换为一个 Block,所以 Closure 可以被用在任何Block可以使用的地方。通常,Closure 用来将一个 Block的状态保存在一个实例变量中,因为一旦你将一个 Block 转换为一个 Closure, 它就是一个通过变量可以引用的对象了。当然Closure 也可以像其他语言中那样使用,例如传递给对象以实现对方法行为的定义。如果你希望传递一些代码来自定义一个方法,你当然可以传递给它一个Block. 但是如果你想将同样的代码传递给两个方法(当然这是非常少见的情况),但是如果你确实想这么做,你可以将一个 Block 转换为一个 Closure ,将同一个Closure传递给多个方法。 

Bill Venners: 
  原来如此,但是获取上下文环境有什么好处呢?真正让Ruby 的 Closure不同的是它捕捉运行时间的上下文环境,局部变量等等。那么到底拥有上下文环境有什么好处是我们无法通过传递给对象一个代码块所获得的呢? 

Yukihiro Matsumoto:
  实际上,说实在的,最主要的原因是向 Lisp 语言表达敬意,Lisp提供了真正的Closure 结构,所以我希望继续提供这个功能。 

Bill Venners: 
  我看到的一个不同之处是: 数据在Closure 对象和方法之间共享。我想我可以在一个常规的非 Closure 结构的 Block 中放入任何需要的环境数据作为参数来传递,但是 Block 仅仅是对环境数据的一份复制,并不是真正的 Closure.它并没有共享环境数据。共享是Closure 和普通的传统函数对象不同的地方。 

Yukihiro Matsumoto:
  是的,共享允许你做一些有趣的代码演示,但是我觉得它对于程序员的日常工作并没有想象的那么有用。这没什么太大的关系,例如像 Java 的内部类那样的普通复制,在许多场合都在使用。但是通过Ruby 的Clousure 结构,我希望表达我对 Lisp 文化的致意。


[注3由夏克补充]

转载自[Ruby中文化计划]  翻译:viking

从目前为止我们所举的例子来看,也许你会对我们之前宣称的Ruby是一种面向对象语言感到迷惑。好,这章我们就来证实它。我们将讨论一下如何在Ruby中创建类和对象,以及Ruby比其它面向对象语言更强大的某些方面,同时我们还要实现我们下一个数万美元的产品—-网络爵士和蓝草自动唱片点唱机。

几个月的工作后,我们的研发人员觉得我们的点唱机需要歌曲(songs)。所以用Ruby创建一个类来表示歌曲看起来是个不错的主意。我们知道一首真正的歌有名字(name),作者(artist),和演奏时间(duration),所以在我们的程序中创建的类要和这个类似。

我们将从创建一个最基本的Song类开始[就像我们在第九页提及的那样,类名要用一个大写字母起始,而方法名用小写字母开头],它将包含一个简单的方法initialize

class Song
  def initialize(name, artist, duration)
    @name     = name
    @artist   = artist
    @duration = duration
  end
end



在Ruby程序里,initialize是一个特别的方法,当我们调用Song.new要创建一个Song对象时,Ruby就创建一个未初始化的对象然后调用initialize方法,并把我们传递给new方法的所有参数都传递给initialize方法,这让你有机会写入设置你的对象属性的代码。

对于Song类,initialize方法接受三个参数。这些参数在方法中以局部变量的形式存在,所以它们要遵循局部变量的命名原则—-以小写字母开头。

每个对象描述每一首歌曲,所以我们需要我们的每个Song对象都包含自己的歌名(song name),演唱者(artist)和演奏时间(duration)。这意味着我们要在对象中以‘实例变量’的形式存储这些值。在Ruby中,实例变量通常都有"@"作为前缀,在我们的例子中,参数name的值被赋值给实例变量@name,参数artist的值被赋值给实例变量@artist,参数duration(用秒数表示歌曲长度)被赋值给实例变量@duration。

测试一下我们漂亮的新类:


aSong = Song.new("Bicylops", "Fleck", 260) 
aSong.inspect      >>   "#<Song:0×401b4924 @duration=260, @artist=\"Fleck\", @name=\"Bicylops\">" 



不错,它似乎工作了。默认情况下,inspect可以被传递给任何对象,它返回对象的id和其所有实例变量。结果显示我们成功了。

经验告诉我们,开发过程中要多次输出Song对象的内容。默认的inspect方法的输出格式显然不能让人满意。幸运的是,Ruby提供了标准消息,to_s,它可以发送给那些想要用字符串形式表示的对象。我们在我们的song中试一下。


aSong = Song.new("Bicylops", "Fleck", 260) 
aSong.to_s  >> "#<Song:0×401b499c>" 



哦,看起来不是很有用――它只是得到了对象的id。更好的解决办法是我们在类里重载to_s,在此之前,我们先花点时间看一下本书是如何对类进行定义的。

在Ruby中,类永远不会关闭,你可以向一个已存在的类一直添加方法,无论是你自己写的类还是标准的内置类都是这样。你要做的只是创建一个已存在的类的定义,然后就可以随时随地把你的新内容添加进去了。

对我们来说这太伟大了,我们进行到本章的时候,我们要不断地给我们的类添加新的特性,在这里我们只展示一下如何定义类来添加新的方法,同时,原来的那个方法依旧在那里不变,这可以使我们不必在每个例子中都重写一遍。不过,如果你正从头开始写代码,你可能会把所有的方法统统扔到一个单独的类定义中。

好啦,这就够啦!让我们回过头来给我们的Song类添加一个to_s方法吧。


class Song
  def to_s 
    "Song: #{@name}–#{@artist} (#{@duration})" 
  end 
end 
aSong = Song.new("Bicylops", "Fleck", 260) 
aSong.to_s      >>   "Song: Bicylops–Fleck (260)" 



很好,我们有进展了。不过,我们忽略了一些微妙的地方。我们说过Ruby的所有对象地都支持to_s,但并没说怎么支持。这个答案要涉及到继承、子类和当你把消息传递给对象时,Ruby是怎样决定由哪个方法来执行的。这些内容可以自成一节,所以… …


继承和消息

继承允许你创建一个类,这个类是另一个类的改进版或是特殊版。比如说我们的点唱机有歌曲的概念,我们把它抽象成Song类,后来,市场发展了,我们被告知需要提供对卡拉OK的支持,一支卡拉OK歌曲本质上还是一首歌(它没有歌声,不过这和我们没什么关系),不过它多了和时间同步的歌词,当我们的点唱机演奏一首卡拉OK时,歌词应该随着音乐的时间在屏幕上滚动过去。

一个解决办法是定义一个新类,KaraokeSong,就像Song类,不过多了歌词。


class KaraokeSong < Song
  def initialize(name, artist, duration, lyrics)
    super(name, artist, duration)
    @lyrics = lyrics
  end
end



在这个类的定义中,"<Song"所在的行表示KaraokeSong是Song的子类。(不要惊奇,这就是说Song类是KaraokeSong类的父类。人们常常谈论父子关系,如此说来KaraokeSong的父亲就是Song了)。暂时不要管initialize方法,我们稍候再讨论super的用法。

我们来创建一个KaraokeSong对象,检查我们代码的运行情况。(在目前的系统中,歌词存放在一个包含文本和时间信息的对象中,不过为了测试我们的代码,在这里我们使用字符串来存储,这是我们这种无类型语言的又一大优势—-在我们运行代码前不必定义任何东西)。


aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the…") 
aSong.to_s     >>     "Song: My Way–Sinatra (225)" 



不错,正常运行,可是为什么to_s方法不显示歌词呢?

这取决于你传送信息给一个对象时Ruby是如何决定哪个方法被调用的。当Ruby编译到aSong.to_s时,它并不知道to_s的真正位置,它忽略过去等到程序运行的时候再处理,到那时,Ruby首先在aSong的类中查找,如果它的类中实现了与这个消息同名的方法,这个方法就会被执行,否则,Ruby将在它的父类中寻找该方法,然后是祖父类…沿着族谱向上寻找,如果到最后都没有发现这个方法,Ruby将返回一个错误信息。(事实上,你可以截取这个错误信息,以便在运行时修正错误,355页的Object#method_missing有详细论述。)

现在回到我们的例子上。当我们给KaraokeSong类的对象aSong发送to_s信息时,Ruby首先在KaraokeSong类中寻找to_s方法,但没有找到,然后在其父类Song中寻找,结果找到了,to_s方法是我们在18页定义的。现在知道为什么to_s不显示歌词了,原来to_s根本不知道有歌词这回事。

我们完善一下代码来实现KaraokeSong#to_s,有好几种办法可以完成这项任务,我们先从一个不好的开始。我们从Song类中把to_s方法的代码拷贝过来,再加上歌词。


class KaraokeSong 
  # … 
  def to_s 
    "KS: #{@name}–#{@artist} (#{@duration}) [#{@lyrics}]" 
  end 
end 
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the…") 
aSong.to_s  ? "KS: My Way–Sinatra (225) [And now, the...]"



我们终于成功显示了@lyrics这个实例变量,为了这个目的子类直接访问了父类的实例变量,那么为什么说这不是个好办法呢?

这和好的编程风格(还有去耦)有关。我们在父类的内部闲逛,结果是我们的子类实现起来要和父类很紧密。如果我们把Song的持续播放时间改成用微秒做单位,那样KaraokeSong就会显出一些古怪的值,一首‘My Way’的卡拉OK持续要3750分钟,这会让人疯掉的。

每个类都应该只处理自身的内部状态,当KaraokeSong#to_s被调用的时候,我们让它调用父类的to_s方法来处理子类中的歌词细节,它会添加歌词信息然后返回结果,这里的诀窍是Ruby的关键字super。若你不带参数调用super,Ruby发送一个消息给当前类的父类,请求父类调用与当前方法同名的方法,并且把我们传递给当前方法的参数传递过去。现在让我们实现我们新的增强版to_s。


class KaraokeSong < Song 
  # Format ourselves as a string by appending 
  # our lyrics to our parent’s #to_s value. 
  def to_s 
    super + " [#{@lyrics}]" 
  end 
end 
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the…") 
aSong.to_s  ? "Song: My Way–Sinatra (225) [And now, the...]" 



我们明确地告诉Ruby,KaraokeSong是Song类的一个子类,但是我们没有指定Song自身的父类。如果在定义类的时候你不指定一个父类,Ruby就默认指定Object类为其父类(看到Java的影子了吗)。这意味着所有的类都把Object类当作是它们的祖先,Object类的实例方法基本上适用于Ruby的每个类。在18页我们说过to_s方法对所有的对象都适合,现在应该明白为什么了,因为to_s正是Object类中大约35个实例方法中的一个,Object类中的方法列表在351页。


继承和混合

一些面向对象语言(特别是C++)支持多重继承从而继承每个父类的功能,多重继承即一个类可以继承于多个直接父类。这种特性既强大又危险,因为它会使继承层次变得晦涩难懂。

其它一些语言,比如说Java,只提供了单继承,一个类仅有一个直接父类,虽然很简洁(也容易实现),但也存在缺点—-在真实世界中,事物常常是从多个源继承得到属性(比如说,一个球既是弹跳的物体也是球形的物体)。

Ruby提供了一种巧妙而且功能强大的折衷办法,让你既可以拥有单继承的简练又可以拥有多重继承的强大。一个Ruby类只许有一个直接父类,这时Ruby是单继承的,同时Ruby又可以包含任意多个混合的功能(混合就像是类定义的一部分),这时Ruby实现了类似多重继承的功能而又没有什么缺陷。关于混合我们会在98页详细介绍。

目前为止,我们已经看到了类和它的方法,现在该看看对象了,比方说Song类的实例。


对象和属性

迄今为止我们所创建的Song对象都有一些内部状态(比如说歌曲标题和作者),对这些对象来说这些状态是私有的,就是说其它对象不可以访问该对象的实例变量。一般来说,这是一件好事,它意味着对象只须负责自己的安全。

但是,如果一个对象太过私有化就没有用处了—-你可以创建它,但不能用它来做什么。为了让外部环境跟它交流,就要定义很多方法来访问和操作这个对象的状态,这些外在的特性叫做对象的属性。(译者注:这里属性指的是那些用来访问和操作对象状态的方法)

对于我们的Song类,可能我们最需要是如何得到它的标题和作者(这样我们才能在播放一首歌时显示它们)还有播放持续时间(这样就可以用进度条表示进度)。


class Song 
  def name 
    @name 
  end 
  def artist 
    @artist 
  end 
  def duration 
    @duration 
  end 
end 
aSong = Song.new("Bicylops", "Fleck", 260) 
aSong.artist    >>      "Fleck" 
aSong.name      >>      "Bicylops" 
aSong.duration  >>      260 



在这里,我们定义了3个访问方法来返回3个实例属性的值。因为这种方法用的太普遍了,所以Ruby专门提供了一个简便的形式attr_reader来帮你创建这样的访问方法。


class Song 
  attr_reader :name, :artist, :duration 
end 
aSong = Song.new("Bicylops", "Fleck", 260) 
aSong.artist    >>      "Fleck" 
aSong.name      >>      "Bicylops" 
aSong.duration  >>      260 



这个例子介绍了一些新东西。:artist这种结构返回对应artist的对象标识,你可以把:artist当作是变量artist的名字,而artist是变量的值。这个例子我们定义了三个访问方法: name, artist, duration,它们对应的实例变量@name, @artist, @duration同时被自动创建。这样定义的访问方法就和上面我们手写的是完全一样的。

可写属性

有时候你需要从对象外面来修改对象的属性。例如,假设Song类的播放持续时间最初只是一个估值(可能是从CD封面或者MP3的文件推测出来的),我们第一次播放歌曲时,得到歌曲的真实长度,然后把它回存到Song对象中。

在类似C++和JAVA这样的语言中,你可能要用到setter方法:


class JavaSong {                     // Java code
  private Duration myDuration;
  public void setDuration(Duration newDuration) {
    myDuration = newDuration;
  }
}
s = new Song(….)
s.setDuration(length)



在Ruby中,对象属性可以和变量一样被读取,从前面的程序中对aSong.name的调用就可以看出,自然你也会想到,既然变量的值可以修改,那属性的值就也应该可以修改。遵循最小意外原则(参看[序]节的注1),Ruby也这样认为。


class Song 
  def duration=(newDuration) 
    @duration = newDuration 
  end 
end 
aSong = Song.new("Bicylops", "Fleck", 260) 
aSong.duration      >>      260 
aSong.duration=257   # set attribute with updated value 
aSong.duration      >>      257 



赋值语句"aSong.duration = 257"调用了aSong类的duration=方法,将257作为参数传递给它。实际上,以等号结尾的方法定义让方法名能够出现在赋值语句左边。

Ruby也提供了创建写属性方法的快捷方式。


class Song
  attr_writer :duration
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.duration = 257




虚拟属性

这些属性访问方法不是只能对对象的实例变量进行简单包装,比如,你可能希望歌曲播放持续时间以分钟为单位,而不是现在的以秒为单位。


class Song 
  def durationInMinutes 
    @duration/60.0   # force floating point 
  end 
  def durationInMinutes=(value) 
    @duration = (value*60).to_i 
  end 
end 
aSong = Song.new("Bicylops", "Fleck", 260) 
aSong.durationInMinutes      >>      4.333333333 
aSong.durationInMinutes = 4.2 
aSong.duration      >>      252 



这里我们用属性访问方法创建了一个虚拟的实例变量。对于这个类的外部而言,durationInMinutes和其它的属性没有什么区别,但并不存在和它对应的实例变量。

这不只是有趣而已,在Bertrand Meyer划时代的著作《Object-Oriented Software Construction》中,称之为统一访问原则(Uniform Access Principle)。通过隐藏实例变量和实际计算值的不同,你就不必自己来实现这些处理,等到将来也可以很方便地修改它的工作方式同时又不用顾及成千上万使用你的类的文件了,这无疑是一场伟大的胜利。



类变量和类方法

到目前为止,我们创建的所有的类都包含了实例变量和实例方法:变量和类的一个特定实例关联,而方法操作这些变量。有时类自己也需要有自己的状态,因此引入了类变量的概念。

类变量

类变量在类的所有对象中是共享的(译者注:就像JAVA中类的static变量),并且可以被我们下文要提到的类方法访问。对于一个给定的类,特定的类变量只有一个拷贝。类变量的命名以两个"@"开头,就像"@@count",与全局变量和实例变量不同,类变量在使用前必须要初始化,这种初始化通常不过是类定义中一个简单的赋值语句而已。

举个例子来说,我们的点唱机可能需要统计每首歌曲被点唱的次数,这个次数可能是Song对象的一个实例变量,当歌曲播放的时候,这个变量的值就增加。我们同时也想知道被点唱的歌曲的总数目,我们可以搜索所有Song对象然后把它们加在一起,要不就冒着被赶出"优秀设计殿堂"的危险使用全局变量,呵呵,两种方法我们都不用,我们使用类变量。


class Song
  @@plays = 0
  def initialize(name, artist, duration)
    @name     = name
    @artist   = artist
    @duration = duration
    @plays    = 0
  end
  def play
    @plays += 1
    @@plays += 1
    "This  song: #@plays plays. Total #@@plays plays."
  end
end



为了方便调试,我们用Song#play来返回一个字符串显示这首歌播放的次数,以及所有的歌播放的总数,我们来简单地测试以下。


s1 = Song.new("Song1", "Artist1", 234)  # test songs.. 
s2 = Song.new("Song2", "Artist2", 345) 
s1.play       >>      "This  song: 1 plays. Total 1 plays." 
s2.play       >>      "This  song: 1 plays. Total 2 plays." 
s1.play       >>      "This  song: 2 plays. Total 3 plays." 
s1.play       >>      "This  song: 3 plays. Total 4 plays." 



对一个类和它的实例来说,类变量是私有的,如果你想要在类的外部访问它,就需要写一个访问方法,这个方法既可以是一个实例方法,也可以是一个类方法,我们进入下一节。

类方法

有时,一个类需要提供不依赖于任何特定实例对象的方法。

我们已经碰到过这样的方法,new方法创建一个新的Song对象,但和特定的歌曲没有什么关系。


aSong = Song.new(….)



你会发现在Ruby的库中充满了类方法,比如,File类实现在文件系统中打开文件,但是File类也提供了几个类方法用来操作文件,它们不需要打开文件所以也就没有File对象,例如你要删除文件,调用类方法File.delete,把文件名传递过去就可以了。


File.delete("doomedFile")



类方法与实例方法的定义方式不同。定义类方法要在方法名前加上类名和一个点号(".")。


class Example

  def instMeth              # 实例方法
  end

  def Example.classMeth     # 类方法
  end

end



点唱机是按每首歌的点唱次数收费的,而不是按歌曲长度收费,所以短的曲目就比长的要有利可图。为了避免在歌单(SongList)中出现太长的歌,我们在SongList中定义一个类方法来检查歌曲是否超过某个限制,我们用一个类常量来设置这个限制,类中的一个简单的常量(还记得常量吗,大写字母开头..)。


class SongList 
  MaxTime = 5*60            # 定义这个限制为5分钟

  def SongList.isTooLong(aSong) 
    return aSong.duration > MaxTime 
  end 
end 

song1 = Song.new("Bicylops", "Fleck", 260) 
SongList.isTooLong(song1)      >>      false 
song2 = Song.new("The Calling", "Santana", 468) 
SongList.isTooLong(song2)      >>      true 



单例与其它构造器

有时你可能会想要重写Ruby的默认构造方法,我们仍旧用点唱机来做例子。因为我们在全国各处有很多点唱机,于是就想要让维护工作变得简单一些。我们需要把在点唱机上发生的事件以日志形式全部记录下来,像被点唱的歌曲,收到的钱,可疑的流量等等,为了节约带宽,这些日志保存在本地,我们需要一个类来处理这些日志,但是,我们希望一个点唱机只有一个日志对象,还希望所有使用这个日志对象的其它对象共享它(译者注:意即所有对象只使用一个日志对象)。

通过使用《设计模式》中提到的单例模式,要做到上述这些要求就只有一种办法来创建日志对象,调用Logger.create方法,还要确保只有一个日志对象被创建。


class Logger
  private_class_method :new
  @@logger = nil
  def Logger.create
    @@logger = new unless @@logger
    @@logger
  end
end



把Logger的new方法声明为私有来防止其他人调用默认构造器创建日志对象,取而代之,我们提供了一个类方法Logger.create,它使用类变量@@logger来保持对一个单个的日志对象的实例的引用,每次调用这个方法时都返回这个实例。(我们这里介绍的单例的引用并不是线程安全的,如果多个线程同时运行,就有可能会创建多个日志对象。我们可以使用ruby提供的单例混合,而不必自己处理线程安全问题,在468页有详细描述。)我们可以查看方法返回的对象标识符来观察一下。


Logger.create.id      >>      537766930 
Logger.create.id            >>      537766930



使用类方法来伪造构造器,也可以让那些使用你的类的人倍感轻松。举一个小例子,Shape类描述正多边形,创建Shape类的实例需要提供边数和总的周长给构造器。


class Shape
  def initialize(numSides, perimeter)
    # …
  end
end



但是,几年以后这个类用在另外的程序中,程序员需要通过指定名字和边的长度来创建多边形而不是用周长,这时只需要简单地向Shape类中添加一些类方法。


class Shape
  def Shape.triangle(sideLength)
    Shape.new(3, sideLength*3)
  end
  def Shape.square(sideLength)
    Shape.new(4, sideLength*4)
  end
end



类方法还有许多有趣而强大的功能,不过继续探讨它们不会加快我们开发点唱机的进度,还是让我们继续吧。


访问控制

设计了一个类的接口,重点要考虑的是你的类暴露给外界多大权限的访问,如果允许过多的访问,会导致你的程序与外界耦合紧密—-使用你的类的用户会更多的依赖类的实现细节,而不是它自己的逻辑接口。好在Ruby改变对象属性的唯一途径是调用对象的方法,这样,控制了对方法的访问,也就控制了对对象的访问。一个非常好的原则是永远不要把会使对象的属性值非法的方法暴露,Ruby提供了三种保护级别。

      公有方法pubic methods    可以被任何对象调用,不存在访问控制。方法默认都是公有的(initialize除外,它永远是私有的)。

      保护方法protected methods   可以被定义它的类和其子类的对象访问,访问只限于家族内。

      私有方法private methods   不能被外界调用,因为调用私有方法时无法指定对象,所以只能在定义它的类和类直接派生的对象中使用。

"protected"和"private"两者的区别非常微妙,在ruby中两者间的区别甚至要超出其它大多数面向对象语言。如果一个方法是保护的,它可以在定义它的类或者子类的实例中调用。如果一个方法是私有的,则只能在调用它的对象的上下文处调用,不可能直接调用另一个对象的私有方法,即便这个对象和该对象都是同一个类的实例。

Ruby和其他OO语言另一个重要的不同点在于,访问控制是动态确立的,就是说程序运行时,而不是静态的,只有你的代码执行受限的方法时才会得到访问违例。

设定访问控制

在一个类或模块的定义中你可以使用public,protected,private三种方式来设定方法的访问级别,它们的每一种又可以有两种不同的使用方式。

如果不带参数,这三种方式设置随后的方法为默认访问控制,这恐怕是尽人皆知的。如果你是C++或JAVA程序员,你会使用像public这样的关键字来达到同样的效果。


class MyClass

      def method1    # 默认是 ’public’
        #…
      end

  protected          # 随后的方法是 ’protected’

      def method2    # 这里是 ’protected’
        #…
      end

  private            # 随后的方法是 ’private’

      def method3    # 这里是 ’private’
        #…
      end

  public             # 随后的方法是 ’public’

      def method4    # 这里是 ’public’
        #…
      end
end



另一种方式,把方法作为参数列在访问控制后面,来设置这些方法的访问级别。
 

class MyClass

  def method1
  end

  # … and so on

  public    :method1, :method4
  protected :method2
  private   :method3
end



类的initialize方法自动被声明为私有的。 

现在来看一些例子,假设我们在建模一个记帐系统,每一个借方对应一个贷方,因为我们想确保这个规则不被任何人打破,所以我们把处理借贷业务的方法私有化,然后我们按照事务处理的程序定义我们的外部借口。


class Accounts

  private

    def debit(account, amount)
      account.balance -= amount
    end
    def credit(account, amount)
      account.balance += amount
    end

  public

    #…
    def transferToSavings(amount)
      debit(@checking, amount)
      credit(@savings, amount)
    end
    #…
end



保护访问用在当对象需要访问和它同一个类的其它对象的内部状态的时候,例如,我们想要让Account对象之间可以比较它们的余额的差额但又希望把各自的余额隐藏起来(也许是因为我们把它们放在不同的表格中)。


class Account
  attr_reader :balance       # 访问方法 ’balance’

  protected :balance         # 把它私有化

  def greaterBalanceThan(other)
    return @balance > other.balance
  end
end



因为balance方法是保护的,所以它只能被Account类的对象访问。


变量

现在我们创建了所有这些对象,但是问题是要保证不会丢失它们,变量就是用来跟踪对象的,每个变量保存一个对象的引用,看看下面的代码。


person = "Tim" 
person.id      >>      537771100 
person.type      >>      String 
person      >>      "Tim" 



第一行,我们使用"Tim"创建了一个新的String对象,对这个对象的一个引用被放置在本地变量person中,上面的简单测试显示变量和对象的id、类型、值确实保存在一个字符串名字当中。

那么,变量是对象吗? 

答案是NO!一个变量只是一个对象的简单引用,对象漂浮在某处的大池子里(堆栈,大多数时候是这里),变量指向对象。

我们再看一个稍微复杂的例子 


person1 = "Tim" 
person2 = person1 
person1[0] = ’J' 
person1      >>      "Jim" 
person2      >>      "Jim" 



怎么回事?我们改变了person1的第一个字符,但是person1和person2都从"Tim"变成了"Jim"。

这是因为变量保存对象的引用,而不是对象本身。把person1赋值给person2并不创建新的对象,只是简单地把person1的对象引用拷贝给person2,所以person1和person2都指向同一对象。

赋值给了对象一个别名,可能同一个对象会有多个变量引用,这会不会给你的代码带来问题呢?会,不过不想你想象的那么麻烦(Jaca的对象也是这样的)。上面的例子中,你可以使用String的dup方法来避免混淆,它创建一个有着相同内容的新String对象。


person1 = "Tim" 
person2 = person1.dup 
person1[0] = "J" 
person1      >>[/tab]"Jim" 
person2      >>[/tab]"Tim" 



如果你不想别人修改一个特定的对象,可以冻结它(我们在251页详细讨论冻结对象)。尝试修改一个被冻结的对象,Ruby会抛出一个TypeError异常。 


person1 = "Tim"
person2 = person1
person1.freeze       # 避免对象被修改
person2[0] = "J"
produces: 
prog.rb:4:in ’=': can’t modify frozen string (TypeError)
from prog.rb:4



(译者注:可能有人会感到突兀,为什么变量放在这里介绍,这里也体现了Ruby是真正面向对象的语言,所以一切皆对象,变量只不过是对象的引用,而不是对象自身。既然所有的东西都是对象,而这所有的东西都要靠变量来表示,所以这里就要先介绍一下变量了。)

转载自[Ruby中文化计划]  翻译:夏克 

刚开始写这本书的时候,我们有一个宏伟的计划(那时我们还年轻),我们希望从头到尾地给出这门语言的文档,从类和对象开始,到语法细节的深入研讨。那时候,这看上去是一个好主意,毕竟,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概述,你会看到这是一个Ruby指南。它用一个小章节来开始,介绍了一些Ruby独特的术语和概念。这一章也大致介绍了其它章要用到的基本语法。这个指南剩下的部分是对这门语言从头到脚的一番审视,这里我们会谈到类和对象、类型、表达式和等等其它方面,我们还用了一个简短的章节来教你怎样应付你所遇到的麻烦。

Ruby最伟大的一个地方就是它和它所处环境的集成。第二部分Ruby的设置中我们来研究它。你将会看学到实际运行Ruby时的一些知识,还有怎样在Web中使用Ruby。你会学到利用Tk来创建GUI程序,还会学到在微软Windows环境中使用Ruby,包括API编程、COM使用、Windows自动化等。你还会发现用自己的代码来扩展Ruby是多么的容易。

第三部分是Ruby详细,包含了学多高级材料。这里你会看到这个语言的血脉,元类模型、tainting、reflection和marshaling等。第一次看到这里你可能想要快速翻过这部分,但我们发现自己在使用这部分中的表格时更甚于这本书中其它的部分。

第四部分是Ruby库参考,它很大,包括了800个方法的说明,其中至少有40个内置类和模型,我们还用了另外70页来描述其它一些有用的Ruby内置的库模块。

所以,你应该怎样来阅读这本书呢?当然,这要看你自己了。

如果你熟悉程序设计和面向对象的程序设计,你可能希望跳着来看这本书,这样的话我们建议你:

如果你是个初学者:你最好从第一部分的教材开始,把库参考放在手边,随写随查。把基本的类比如Array,Hash和String弄明白,等你熟悉了这套环境以后,你也许就该希望深入研究第三部分中的高级主题了。

如果你已经精通Perl,Python,Java或者Smalltalk,我们建议你从第2章开始,这样你可以慢慢地深入到第三部分中的高级部分,然后再来看第四部分的库参考。

专家、gurus、和“我讨厌愚蠢的教材”的类型的人可以直接潜入到第18章的语言参考书目中,在199页,略过库参考,然后把这本书作为一个咖啡杯垫。

当然,一边学习一边用自己的方式来工作也没有什么错。

不要忘记,如果你碰到了麻烦却找不出错误,帮助还是有效的,看看附录C.

转载自[Ruby中文化计划]  翻译:夏克 

这是一本关于Ruby程序设计的教程和参考书,使用Ruby你可以写出优良而高效的代码,并且充满乐趣。

夸张吗,但如果你读完这本书,我们相信你会同意的,我们的经验告诉我们这是真的。

编写Pragmatic Programmers系列的时候,我们尝试了很多的语言以期找到使我们的生活更加简易,使我们的工作更加出色的工具,但我们一次次被这些语言所挫败。

我们的目的是解决问题,而不是调试编译器,我们喜欢那些适合于我们的动态语言,没有强制的规则,我们需要通过代码就可以清楚地交流,我们看重在代码中精确有效地表述一个需求的简便能力,我们写的代码越少,出的错就越少。(并且我们的手腕和手指也会为此感到庆幸)

我们希望越高效越好,最好是我们的代码一次通过,编译器花费的时间就是我们那被偷走的开发时间。我们也希望能像编辑代码一样来测试代码,如果你等了两个小时来完成循环,你还不如使用穿孔卡片来完成你的工作呢。

我们想要一个工作在更高抽象层次的语言,越是高级的语言,我们花在转述需求给代码的时间就越少。

当我们发现了Ruby的时候,我们意识到我们找到了。相比我们曾经使用过的其它任何语言,Ruby把你解放了。你可以把精力集中在解决问题上而不是跟编译器和语法纠缠。它让你有机会成为一个更好的程序员,因为你可以把时间花在为你的用户提供解决方案,而不是为编译器。

Ruby使人心跳

先拿一个真正的面向对象的语言,比如SmallTalk,放弃陌生的语法而采用更方便的基于文件的源代码(译者注:此处译者不太明白),添加上像Python和Perl这些语言的柔韧和便利,你就得到了Ruby。

OO狂热迷会喜欢Ruby中的好多东西,比如纯粹的面向对象(任何东西都是对象),元类、闭包、迭代器和无所不在的异类集;Smalltalk用户会有回家的感觉;而C++和Java的用户会感到嫉妒;同时,Perl和Python的用户会发现许多他们所喜欢的特性:完整的正则表达式支持,与底层操作系统的紧密结合,方便的快捷方式,还有动态评估。

Ruby很容易学会,很简单就可以写出日常工作的代码,而且一旦你写好了,也很容易维护和升级。看起来很困难的事情往往没有实际上那么困难。Ruby遵循最小意外原则(注1),事情按你所设想的进行,只有很小的例外。这确实和你现在编程的方式不一样。

我们把Ruby称为透明语言,就是说Ruby不会让你的思路被无数的语法所蒙蔽,也不会为了完成简单的目的却要翻遍无数的支持代码,用Ruby写程序使你更接近于问题本身。你不用经常让你的思路和设计迁就于那些低级语言,用Ruby你发现你可以表达得直接而优雅。这意味着你可以更快地编写代码,也意味着你的代码可读和可维护。

使用Ruby,我们经常惊讶于一次可以写那么多代码,而且这些代码一次就通过,很少会有语法错误,没有类型障碍,远少于通常的Bug。很少会出错,没有令人讨厌的行尾分号,没有为了保持同步而让人厌烦的类型声明(尤其是分隔的文件),没有多余的语句仅仅是为了让编译器高兴,没有易错的框架代码。

所以,为什么学习Ruby?因为我们觉得它有助于你更好地编程,有助于你把精力集中到手头上的问题而不分心,它能使你的生活变得更容易。

Ruby是一种什么样的语言?

在从前,语言之间的区别是很简单的,或者是编译语言,就像C或者Fortran,或者是解释型语言,像BASIC。编译语言给你速度和低级访问,解释语言高级但是却很慢。

时代在改变,于是乎事情就变得不再简单了。一些语言的设计者渐渐把他们的创造品呼为“脚本语言”。我们猜想他们的意思是他们的语言是被解释执行并且可被用来代替批处理文件和shell脚本,把其它程序和底层操作系统协调组织在一起,Perl、TCL、Python现在都被称为脚本语言。

那怎样才能被称为是一门真正的脚本语言?坦白讲我们也不知道这种区别是否值得探寻。在Ruby中,你能访问所有的底层操作系统特性,Perl和Python能做的你都可以用Ruby来做,而且更简洁。Ruby根本的区别在于,它是一个有着坚实理论基础和优雅简洁的语法的程序语言。你可以把Ruby脚本搞得很乱,但基本上你不会这样做。而是更倾向于提供一个解决方案来写出这样的一套程序,它更容易理解,维护简单,很容易在将来扩展和重用。

尽管我们把Ruby用在处理脚本工作上,不过大多数时候我们还是把它看作多用途的程序语言。我们用它写GUI应用和中间层服务器过程,我们还用它排列这本书的格式,还有人用它管理服务器和数据库。Ruby用在Web页上,可以提供数据库接口和动态内容。有人用Ruby写人工智能和机器学习的程序,而且至少有一个人用它来研究自然进化。Ruby为数学研究也提供了很大帮助。全世界的人们使用它把他们各自不同的程序结合在一起。Ruby真的是一门伟大的语言,为许多领域的问题解决提供了帮助。

Ruby适合我吗?

对于程序员来说,Ruby不是万能药。有时,你会需要特定的语言:也许是环境使然,也许你需要专门的库,也许涉及到性能,或者仅仅因为培训的内容。我们不会完全放弃像Java和C++这样的语言(尽管我们希望有这一天)。

不过,Ruby可能比你所能想象到的还要适合你。它易于扩展,很容易就能和第三方库联编。它跨平台,相对来说是一种轻量级语言,消耗很少的系统资源。学习起来容易,我们曾经认识一个人,他只看了一天这本书,就能把Ruby代码写到他的产品系统中了。我们曾经用Ruby实现了一个X11窗口管理器的一部分,而这正常情况应该是用C完成的。Ruby帮助我们在几小时内写出其它语言需要几天时间的代码。

一旦你体会到Ruby的舒适,我想你会掉过头来选择它作为你的程序语言了。

Ruby版本

本书使用1.6版的Ruby,2000年9月发布。

Ruby版本的编号方式和其它开源软件一样,有着偶数子版本号(1.0、1.2、1.4等等)的发布是稳定、公开的发布。它们预先包装好然后发布在各Ruby网站上。

开发版有奇数子版本号,就像1.1和1.3,这需要你下载后自己来build,就像xxvii页上描述的那样。

安装Ruby

你可以从ftp://ftp.netlab.co.jp/pub/lang/ruby获得Ruby,或者从526页的附录C镜像站点列表中获得。你可以找到最新的稳定发布,也有许多开发版的发布。

你能找到Ruby的源代码发布,也可以找到适用于Windows或者其他操作系统的二进制发布(比如适用于Windows的二进制发布在http://www.pragmaticprogrammer.com/ruby/downloads/ruby-install.html

提示:最最新的Ruby

对那些需要最最新版本的人(就像我们写这本书一样),你可以从开发者的工作仓库直接得到开发版。

Ruby开发者使用CVS(Concurrent Version System,从http://www.cvshome.com处免费得到)作为他们的修订控制系统。你可以匿名登陆然后执行下面的CVS命令:

% cvs -d :pserver:anonymous@cvs.netlab.co.jp:/home/cvs
   login
(Logging in to anonymous@cvs.netlab.co.jp)
CVS password: guest
% cvs -d :pserver:anonymous@cvs.netlab.co.jp:/home/cvs
   checkout ruby 


开发者最新留下的完整源代码树就拷贝到你的机器里的ruby目录了,从地球的另一边升级你本地的源代码树,这不是很美妙吗?

编译Ruby

在Ruby的发布中你可以找到一个叫README的文件,里面声明了安装的细节。简单说,在POSIX-based系统上编译Ruby使用四个命令,和其它开源软件一样:./configure,make,make test和make install。你也可以在其它环境下编译Ruby(包括Windows),使用POSIX模拟环境比如cygwin[参看http://sourceware.cygnus.com/cygwin]。或者使用本地编译器—-参看win32目录中的ntsetup.bat。

运行Ruby

安装了Ruby后,你可能希望运行一下程序试试看。不象编译环境,运行Ruby有两种方式—-交互或者作为一个程序。

交互运行Ruby

交互运行Ruby最简单的方式就是在命令行中输入ruby:

% ruby
puts "Hello, world!"
^D
Hello, world!


这里我们输入了一个简单的puts表达式和一个文件结束符(在我们的系统上是CTRL-D)。它虽然能运行,不过你要是希望排版的话还是有点儿痛苦,并且你也看不到你输入的代码如何运行。

在sample目录中你可以找到一个叫eval.rb的脚本,它稍微好一点儿,因为你输入的每一个表达式的值它都会显示:

% cd sample
% ruby eval.rb
ruby> a = "Hello, world!"
"Hello, world!"
ruby> puts a
Hello, world!
nil
ruby> ^D
%


这里我们能看到puts的输出和返回值(就是nil)。

看上去很不错,就是你不能使用多行的表达式,并且不能编辑行,也不能返回使用原来的行(就像在shell中使用历史命令那样)。

比eval.rb更好的是irb—-Interactive Ruby。irb是一个Ruby Shell,有完整的历史命令行,可以编辑行。你可以配置它的许多选项,在517页有它的附录。我们建议你熟练使用irb,这样就可以运行我们的大多数交互例程。

Ruby程序

最后,你可以从一个文件中运行Ruby程序,就像其它Shell脚本,Perl程序,或者Python程序。你可以简单地把脚本名称作为参数来运行Ruby程序。

% ruby myprog.rb


或者你可以使用Unix "shebang"符号作为你的程序文件的第一行。[如果你的系统支持的话,你可以在shebang行的Ruby路径中使用#!/usr/bin/env ruby来避免hard-coding,这会在你的路径中寻找ruby然后执行它。]

#!/usr/local/bin/ruby -w
puts "Hello, World!"


如果你把源文件设置为可执行(比如,使用chmod +x myprog.rb),Unix让你把文件当作程序一样运行:

% ./myprog.rb
Hello, World!


在微软Windows中你可以使用文件关联实现相同的功能。

资源

访问Ruby站点,http://www.rubycentral.comhttp://www.ruby-lang.org,看看最新的信息,和新闻组或者邮件列表中其他的Ruby用户聊天(参看附录C)。

我们也希望能收到您的反馈:注释,建议,文本中的错误,例子中的疑问统统欢迎,我们的E_Mail:rubybook@pragmaticprogrammer.com

如果你要告诉我们书中的错误,我们会加到勘误表中:

http://www.pragmaticprogrammer.com/ruby/errata/errata.html

最后,http://www.pragmaticprogrammer.com/ruby中有书中的例子的源代码,按页存放。

感谢

一本书是一项庞大的工程,我们不可能在没有帮助的情况下完成它,谢谢帮助我们的所有的朋友,不管老的还是新的。Mike Hendrickson, John Fuller, the ever-helpful Julie Steele, and the wonderful Julie DiNicola. 谢谢你们。

我们的书评人也是很奇妙的。George Coe、Bob Davison、Jeff Deifik、Hal Fulton、Tadayoshi Funaba、Clemens Hintze、Kazuhiro Hiwada、Kikutani Makoto、Mike Linksvayer、Aleksi Niemel、Lew Perin、Jared Richardson、Armin Roehrl、Conrad Schneiker、Patrick Schoenbach、and Eric Vought。

一些人帮我们完成了本书的一些部分。Tadayoshi Funaba 和我们交换了数不清的邮件,直到我们最后理解了Date模块。Guy Decoux和Clemens Hintze耐心地回答我们关于Ruby扩展的问题。Masaki Suketa使我们搞明白了WinOLE模块。

Ruby最初的大多数文档都是日语写的,不过现在也产生出越来越多英译作品,有很多值得记住的作者名字,在这里我们只选出Goto Kentaro,他已经出品了很多高质量的文章,并且把它们放在网上。

最后,我们一定要感谢Yukihiro "Matz" Matsumoto,Ruby的作者。我们已经记不清问了他多少问题,还有他耐心的回答,除了发明了一个非常棒的语言,他也培养了一个令人惊奇的支持社群和开放文化,这是这门语言能够兴隆的所在。

谢谢你们所有的人。Domo arigato gozaimasu。

Dave Thomas 和 Andy Hunt
THE PRAGMATIC PROGRAMMERS
http://www.pragmaticprogrammer.com

符号约定

贯穿整本书,我们采用下面的排版符号。

例子中的文字编码采用打字机字体:

class SampleCode
  def run
    #…
  end
end


Fred#doIt是Fred类的一个例程方法的引用,而Fred.new[有的Ruby文档把类方法写成Fred::new,这是一种完全合法的Ruby语法,不过我们刚刚发现Fred.new读起来不容易转义。]是类方法,Fred::EOF是类常量。

书中包含了许多Ruby代码片断。我们尽可能写出它们运行后的结果,简单的情况下,我们在表达式的同一行中写出表达式的值,比如:

a = 1 
b = 2 
a + b       >>      3


有时,我们也对一些赋值语句感兴趣,我们就这样:

a = 1       >>      1
b = 2       >>      2
a + b       >>      3


如果程序执行后产生出复杂的输出,我们就在代码下面写出结果:

3.times { puts "Hello!" }


produces:

Hello!
Hello!
Hello!


在一些库文档中,为了指出输出中有空格,我们" "表示(译者注:这里是一张小图片,我们用正常的空格表示,有必要的话会注明)。

命令行使用Roman字体,参数使用italic字体,可选参数括在方括号中:
ruby [
            flags
            ]*
             [
            progname
            ] [
            arguments
            ]+

[注1]:

最小意外原则主要针对界面而不是代码,看一个小小的例子就可以明白:

不好的界面:
 int multiply(int a, int b)
 {
   return a + b;
 }

好的界面:
 int multiply(int a, int b)
 {
   return a * b;
 }

或者这样:
 int add(int a, int b)
 {
   return a + b;
 }

但这个却不好:
  int write_to_file(const char* filename, const char* text)
  {
    printf("%s\n", text);   /*看出来了吗,filename并没有使用*/
  }

最小意外原则一般指给用户最少的意外,有人总结了Ruby的最小意外原则是:

1、中庸
   不要让用户去猜你的意图
   也不要把他们当作傻瓜

2、总会存在意外
   因为有误会的可能
   背景知识不同也是一个原因

3、意外也分好坏
   好的意外是因为它使你的代码看起来very good
   不要经常重复地出现意外

4、自然而不是简单
   简单不是最终目标

本书的作者认为Ruby的最小意外原则是事情按你所设想的进行,只有很小的例外。还有很多人对这个原则阐述了自己不同的观点,可以在google中搜索。

Larry Wall是Perl的作者,他很幽默,在回答网友的问题时他提到“很多方面上我还是很喜欢 Ruby 的,这是因为那些部分是从 Perl 借过去的。:-)Ruby 的主要问题在于它的最小意外原则可能让人误入歧途,要减少谁的意外?专家和初学者对不同的事情感到意外,从一个小程序写成大程序的人和从开始就写大程序的人可能对不同的事情感到意外”。

有趣的是,Matz并不认为Ruby遵循什么最小意外原则,在2003年9月和Bill Venners的谈话中,Bill Venners问他Ruby为什么要遵循最小意外原则,Matz回答说:“我并不没有声明Ruby遵循最小意外原则,有人觉得Ruby的设计符合这个原则,所以他们就这样传说,实际上我可不这样认为。我希望在编程的时候尽量减少挫折,这是设计Ruby的初衷,我自己在编程时得到了很多乐趣,等到Ruby公布以后,很多人说他们也感受到了我的感受,所以他们就开始说Ruby遵循最小意外原则,事实上这是误导。因为每个人的技术背景不同,有人来自Python社区,有人来自Perl社区,他们各自为Ruby的不同方面感到意外。然后他们找到我说,“我对这种语言的这个特性感到非常惊讶,所以Ruby违反了最小意外原则。”等等。最小意外原则不是为你一个人设置的,等你非常好的掌握了Ruby以后再来谈最小意外原则吧。比如,我在开始进行Ruby设计以前是一个C++程序员,用C++进行开发有两三年,但是就算是在我使用了两年C++以后,C++还是经常使我感到意外”。

[注1由夏克补充]

转载自[Ruby中文化计划]  翻译:夏克 
 
人类都有创作的冲动,我自己就很喜欢创作一些东西。如果我没法学好画画唱歌,那至少我还可以写一写程序。

在我接触到计算机没多久,我就深深地爱上了程序语言。我觉得只要经验丰富,编写一个完美的程序语言应该是不成问题的,而且我自己就要去编写它呢。但是,慢慢地我知道了,想要设计出那种理想中的完美语言要比我想象的难的多。尽管如此,退而求其次,我依旧希望设计出一套能够满足自己日常大部分工作需要的语言。这是我学生时代的梦想。

几年以后我和同事们讨论脚本语言时,感叹它们的强大和实用。作为一个有着15年以上资历的面向对象爱好者,我觉得面向对象也非常适合于脚本设计。于是我在网上游历了一番,发现了Perl和Python,但它们并不十分让我满意,我心中的语言应该比Perl更强大,比Python更面向对象。

然后,我想起了我的旧梦,决定设计自己的语言。一开始,我仅把它当作一个玩具,但是它逐渐成长为一个能够取代Perl的更好的语言。我给它起了一个名字叫Ruby,珍贵的红宝石,并且在1995年把它面向公众发布了。

那以后,许多人对它产生了兴趣,无论你信不信,反正现在在日本,使用Ruby的人要比使用Python的多,我也希望全世界的人都能够接受它。

我认为人生应该是充满乐趣的,至少在一个阶段里。基于这样的信条,Ruby被设计成既简单又好玩。它让你把注意力集中到程序设计本身而没有其它的负担。如果你不相信我的话,读一下这本书,然后试试Ruby,我想你会重新认识你自己。

我非常感谢加入到Ruby社区的人们,他们给我很多帮助。尽管我把Ruby当作我的孩子,但实际上,它是众人努力的结果,没有他们的帮助,Ruby不可能发展到现在这样。

我特别要感谢本书的作者,Dave Thomas和Andy Hunt。Ruby没有很好的文档支持,因为我乐于写代码而不喜欢写文档,Ruby的文档一直都不是很完善,但是现在Dave和Andy为你完成了这项工作。

他们对这个来自远东的无名语言感兴趣,他们研究它,阅读成千上万行的源代码,写了无数的测试脚本和邮件,澄清了语言中模糊的地方,发现Bug(甚至他们修复了许多Bug),最后编译出这本伟大的书,Ruby终于有帮助文档了。

他们在这本书中投入的精力不会白费,他们在写作的时候,我也在修改语言本身,我们一起升级,这本书可以说十分准确地描述了Ruby这套语言。

我希望Ruby和这本书一起帮助你更加容易和充满乐趣地编程。
Yukihiro Matsumoto, a.k.a. "Matz"
Japan, October 2000

2005年05月29日

经过一个星期的努力,终于完成[RUBY问题集]的站点设计,先正式开张,并诚招各界喜欢RUBY编程的朋友们加盟,欲加入我们的朋友请跟我联系:sequh@126.com

[RUBY问题集]的站点地址:http://www.moer.net/ruby/ask

2005年04月21日

这些从开始翻译以来,经过了很长时间。一开始,我比较急躁,总是希望在最短的时间内把这些文档翻译出来,甚至文章我都没有看过,直接就开始翻译,结果可想而知,翻译出来的东西都不好意思拿出来。

这些天终于静下心来,仔细考虑一下,为什么要翻译,为什么要学习ruby,为什么要为ruby付出自己的这些精力,我不再急躁了,终于可以潜下心来,仔细研究ruby的方方面面,翻译的时候也可以结合自己的想法了。

站点决定要迁移到http://www.moer.net/ruby,计划在新站点上主要做好三个方面:

1、ruby问题集

这个想法是从我刚开始学习vb的时候,在问专家获益颇多,非常喜欢问专家这个网站,所以也希望能做一个ruby的问专家站点,这对于初学者来说是非常实用的。

2、ruby文档的翻译

新站将要招收一些新人,一方面负责回答ruby的问题集部分,另一方面要负责翻译ruby的重要文档。

3、ruby项目的开发

我们不仅仅要学习ruby,更重要的是要使用它,否则舍本逐末了。学习一门语言最好的途径就是研究实例程序,我们会成立自己的项目进行共同开发,以期给初学者实践和学习源代码的机会。

现在我已经完成问题集部分的asp页面了,正在写文档管理系统部分的asp页面,当然我们还需要一个论坛,这都在规划中。

可怜的是,这一切现在仅仅是我一个人在做,vking原来还帮我翻译了一篇,可是我都没有时间来审核他的翻译,希望多多的人来加入到我们当中,我的联系方式:sequh@126.com

2004年10月08日

http://www.oreilly.com/catalog/ruby/

Ruby in a Nutshell

2004年09月22日

David Mertz 博士mertz@gnosis.cx
简化工程师,Gnosis Software, Inc.
2002 年 3 月

专栏图标
对于 XML 处理,至少可以采取两种态度。一种是采用可以从许多编程语言调用的标准 API。第二种是修改 XML 处理库以适应正用于开发 XML 应用程序的编程语言的特定功能。 在本专栏的前几篇文章中,David 研究了使用他自己的 Python xml_picklexml_objectify 库以及 Haskell HaXml 库的第二种方法的多个版本。相当新、但发展很快的 Ruby 编程语言的常用库也采用第二种方法。 这里,David 介绍了 Ruby Electric XML(REXML),这种库采用 Ruby 的长处,并围绕它们构建 XML 处理。REXML 具有类似于 SAX 的流样式和 DOM 的树样式的 API,但没有直接将它本身限制于这两种 API。

首先,让我介绍一下 Ruby 语言。这里,我不能完全保证能使不熟悉 Ruby 的读 者快速掌握这种语言 — 既然如此,我建议参阅参考资料中的文章。 但我自己作为一名学习 Ruby 语言的程序员,我可以让您了解它为什么很有趣。 Ruby 是一种脚本语言,曾经被描述为“更好的 Perl”。此外,每一种更新的脚本语言可能也都被这样描述过,包括 Python。 当从完全 Smalltalk 式的 OOP 态度来看,对于 Ruby,描述变得更为确切,不是说 Perl 出了什么错(这是没有对某种语言的攻击),而是说 Ruby 保留了 Perl 的许多简明性及其许多捷径。 而且(至少对我而言),Ruby 达到了简明性, 而且还避免了在某些 Perl 代码中发现的“可执行行噪声”质量问题。同时, 与 Python 版本相比,许多 Ruby 结构“感觉”更直接(即使它们并没有真正节省太多总体长度)。

REXML 是由 Sean Russell 编写的库。它不是 Ruby 的唯一 XML 库,但它是很受欢迎的一个,并且是用纯 Ruby 编写(NQXML 也是用 Ruby 编写的, 但 XMLParser 封装了用 C 编写的 Jade 库)。 在他的 REXML 概述中,Russell 评论道:

我有这样的问题:我不喜欢令人困惑的 API。有几种用于 Java 实现的 XML 解析器 API。其中大多数都遵循 DOM 或 SAX,并且在基本原理上与不断出现的众多 Java API 非常相似。也就是说,它们看 上去象是由从未使用过他们自己的 API 的理论家设计出来的。 通常,现有的 XML API 都很令人讨厌。他们采用一种被明确设计成非常简单、一流且功能强大的标记语言, 然后用讨厌的、过多的和大型 API 对它进行封装。甚至是为了进行最基本的 XML 树操作,我总是不得不参考 API 文档; 没有任何东西是凭直觉的,而且几乎每个操作都很复杂。

虽然我并不认为它有多么令人心烦,但我同意 Russell 的观点:XML API 对于大多数使用它们的人来说无疑带来了过多的工作量。

使容易的事情变得更容易
我猜想处理 XML 文档的所有程序员中有 80% 真正需要的只是一种提取数据并 将它们作为结构化的数据容易地进行操作的方法。DOM 使这一事情变得困难,而 SAX 使 它变得更困难。在前几篇文章中,我提倡我自己的 Python xml_objectify 模块的清晰性和简明性。让我使用文件 address.xml(它描述一个地址簿)来快速重复一个示例。

如何使用 xml_objectify 来引用嵌套数据

 >>> from xml_objectify import XML_Objectify >>> addressbook = XML_Objectify('address.xml').make_instance() >>> print addressbook.person[1].address.city New York

我们需要略微知道一点数据的格式……但不必很多(有关本文中使用的样本文档, 请参阅参考资料)。我们需要知道文档的根元素是地址 簿(但它的名称不必是
)。而且我们需要知道该文档可以列出多人(但是,如果只有一个人, 他可以作为 addressbook.personaddressbook.person[0] 来引用,那样也不会发生错误)。 从概念上讲,还需要知道人有地址,地址有城市。知道这些就够用了

相反,DOM — 它将其本身标榜为 OOP 化的 XML — 却要我们完成所有的操作步骤。第一个难题涉及根元素; 要完成这个任务至少有五种不同的方法可以想到:

使用 DOM 来命名 XML 文档根元素

 >>> from xml.dom import minidom >>> dom = minidom.parse('address.xml') >>> dom.firstChild  >>> dom._get_documentElement()  >>> dom._get_firstChild()  >>> dom.getElementsByTagName('addressbook')[0]  >>> dom.childNodes[0]  

您还必须对究竟什么是方法、什么是属性做一些猜测(或在手边放一本手册)。 假设我们知道需要根元素,则 ._get_documentElement() 方法可能 是最好的选择。现在,如果我们想要向下找到第二个人的城市,就象 xml_objectify 示例中那样,应该怎么做呢?

如何使用 DOM 来引用嵌套数据

 >>> addressbook = dom._get_documentElement() >>> print addressbook.getElementsByTagName('person')[1].\ .. getElementsByTagName('address')[0].getAttribute('city') New York

这种样式相当冗长,但或许是最接近的 DOM 等价样式。 您可以直接使用 .childNodes 属性数组来保存一些字符, 但这种样式是脆弱的,例如,如果
有子元素, 而 没有的话。您还必须知道一些本质细节,city 是元素属性,而不是子标记内容(任何一种方法都可能对正在讨论的基本数据有意义)。

以树方式使用 REXML
REXML 的目的是正好够用。在最大程度上,它能很好地完成任务。 实际上,REXML 支持两种不同样式的 XML 处理 — “树”和“流”。 第一种样式是 DOM 所尝试要做的更简单的版本;第二种样式是 SAX 所尝试要做的更简单的版本。 让我们先研究树样式。假设我们要提取上一个示例中的同一个地址簿文档。 下面的示例来自我所创建的经修改的 eval.rb; 标准 eval.rb(链接到 Ruby 教程)可以根据对复杂对象的表达式求值显示非常长的计算结果 — 我的 eval.rb 在没有错误发生的情况下不作出反应:

如何使用 REXML 来引用嵌套数据

 ruby> require "rexml/document" ruby> include REXML ruby> addrbook = (Document.new File.new "address.xml").root ruby> persons = addrbook.elements.to_a("//person") ruby> puts persons[1].elements["address"].attributes["city"] New York 

这个表达式很普通。.to_a() 方法创建文档中所有 元素的数组,在其它命名中它可能是有用的。 元素有点象 DOM 节点,但它其实更接近于 XML 本身(而且使用起来也更简单)。.to_a() 的参数是 XPath,在这种情况下,可以标识文档中任何地方的所有 元素。如果我们只需要第一层上的元素,可以使用:

创建匹配元素的数组

 ruby> persons = addrbook.elements.to_a("/addressbook/person")

我们甚至可以更直接地将 XPath 用作 .elements 属性的重载索引。例如:

使用 REXML 来引用嵌套数据的另一种方法

 ruby> puts addrbook.elements["//person[2]/address"].attributes["city"] New York

请注意,XPath 使用基于 1 的索引,不象 Ruby 和 Python 数组使用基于 0 的索引。换句话说, 它仍是我们正在检查其所在城市的同一个人。通过查看 REXML 元素本身,可以了解这个人的更多信息:

用 REXML 显示元素的 XML 源代码

 ruby> puts addrbook.elements["//person[2]/address"]
ruby> puts addrbook.elements["//person[2]/contact-info"]

此外,XPath 不必只与一个元素匹配。我们已在定义 persons 数组时看见过,但另一个示例强调了这一点:

将多个元素与 XPath 匹配

 ruby> puts addrbook.elements.to_a("//person/address[@state='CA']")

与此相反,.elements 属性的索引只产生第一个匹配的元素:

当 XPath 只匹配第一次出现时

 ruby> puts addrbook.elements["//person/address[@state='CA']"]
ruby> puts addrbook.elements.to_a("//person/address[@state='CA']")[0]

也可以通过 REXML 中的 XPath 类使用 XPath 地址, 它具有诸如 .first().each().match() 这样的方法。

REXML 元素的一个独特的惯用方法是 .each 迭代器。虽然 Ruby 有一个可对集合进行操作的循环结构 for, 但 Ruby 程序员通常更喜欢使用迭代器方法来将控制传递给代码块。下面的两种结构是等价的, 但第二种结构有更为自然的 Ruby 感觉:

通过在 REXML 中匹配 XPath 进行迭代

 ruby> for addr in addrbook.elements.to_a("//address[@state='CA']") | puts addr.attributes["city"] | end Sacramento Los Angeles ruby> addrbook.elements.each("//address[@state='CA']") { | |addr| puts addr.attributes["city"] | } Sacramento Los Angeles

以流方式使用 REXML
出于“正好够用”的目的,REXML 的树方式可能是 Ruby 语言最简单的方法。 但 REXML 还提供了一种流方式,它象是 SAX 的更轻量级的变体。 正如使用 SAX 一样,REXML 没有向应用程序程序员提供来自 XML 文档的缺省数据结构。 相反,“listener”或“handler”类负责提供响应文档流中各种事件的一组方法。 以下是常用集合:开始标记、结束标记、遇到的元素文本等等。

虽然流方式远远没有象以树方式工作那样容易,但通常它的速度要快很多。REXML 教程声称流方式的速度要快 1500 倍。 虽然我没有尝试过对它进行基准测试,但我猜想这是一种有限的情况(我的小示例在树方式中也是瞬间完成的)。 总之,如果速度要紧,那么速度上的差异很可能是显著的。

让我们研究一个非常简单的示例,它所做的事情与上面的“列出加州城市”示例相同。 对它进行扩展以用于复杂的文档处理相对比较简单:

REXML 中 XML 文档的流处理

 ruby> require "rexml/document" ruby> require "rexml/streamlistener" ruby> include REXML ruby> class Handler | include StreamListener | def tag_start name, attrs | if name=="address" and attrs.assoc("state")[1]=="CA" | puts attrs.assoc("city")[1] | end | end | end ruby> Document.parse_stream((File.new "address.xml"), Handler.new) Sacramento Los Angeles 

流处理示例中要注意的一件事情是,标记属性被作为一组数组传递, 它要处理的工作比起散列要稍微多一点(但可能在库中创建会更快)。

结束语
这篇文章研究了另外一种比起 DOM、SAX 和 XSLT 麻烦的 API 更轻量级的替代方法。 连同前几篇文章中研究的 xml_objectify、PYX 和 HaXml 选项一起,Ruby 程序员还得到了一种处理 XML 的快速方法,而不必经历陡峭的学习曲线。

参考资料

  • Maya Stodte 为 IBM developerWorks 编写了 Ruby 简介,但已经过了两年,Ruby 在这段时间内已有一定的发展。
  • Joshua Drake 也编写了一篇描述一些基本 Ruby 结构的较新文章。
  • 幸运的是,Ruby 在其网站上有一个极好的教程。 语言参考和其它文档也值得一读。
  • 我还阅读了 O’Reilly 出版的题为 Ruby in a Nutshell 的书籍, 由 Ruby 创建人 Yukihiro Matsumoto 所著。作为一名学习 Ruby 的程序员,我承认: 这本书很适合我,但可能不太适合更有经验的 Ruby 用户。 尽管如此,我仍认为这本书“没有被十分完整翻译”(原著是用日文写的)。 虽然作为参考这本书组织得很好,但有许多描述仍让我无法确定语言中那些偶尔的微妙之处。
  • REXML 主页包含一个非常好的教程, 它并不完整,但它可以很好地帮助您尽快熟悉。
  • 请访问有关 Ruby 和 XML 的新闻和讨论的网站
  • 可以从 http://gnosis.cx/download/address.xml 找到本文中使用的地址簿示例。
  • 最后,请了解一下 IBM WebSphere Studio Application Developer, 这是一个易于使用的集成开发环境,可用于构建、测试和部署 J2EE 应用程序,包括从 DTD 和模式生成 XML 文档。
关于作者
David Mertz David Mertz 希望一切都变得美好。可以通过 mertz@gnosis.cx 与 David 联系;在
http://gnosis.cx/publish/ 上详细介绍了他的生活。 欢迎对过去的、这一篇和以后的专栏文章提出意见和建议。