learning notes ruby
TRANSCRIPT
Learning notes Ruby doc
Roger Xia
2007.12
Programming Ruby
• http://www.moer.net/ruby/doc/
Ruby 的新特性
• Ruby是面向对象的语言第一,每个对象都有一个唯一的 object identifier( 简写作 object id) ,就是对象标识符。第二,你可以定义实例变量,变量的值对每个实例来说都是唯一的,这些实例变量记录着对象的属性。
这里值得注意的是 Ruby 和其它语言之间的一个主要的区别,在 Java 中(比方说),你会看到计算一个数的绝对值需要调用一个单独的函数,把这个数作为参数传递给这个函数,可能你会这样写:
number = Math.abs(number) // Java code
在 Ruby 中,计算绝对值的功能内置在数字中 ---- 细节是由它们内部自己实现的。你简单地把 abs 这个信息发送给数字对象,就可以让它完成工作。
number = number.abs
这种特性 Ruby 的所有对象都具有:在 C 中你要写 strlen(name) 而在 Ruby 中可以是 name.length 等等,这就是我们为什么要说 Ruby 是一个真正的面向对象语言的部分原因。
Ruby 的新特性• Ruby的一些基本知识
def sayGoodnight(name) result = "Goodnight, " + name return resultend
# Time for bed...puts sayGoodnight("John-Boy")puts sayGoodnight("Mary-Ellen")
不需要在行尾加上分号,一行一个语句 Ruby 注释开始于 # 号,结束在行尾 方法是由关键字 def 来定义的,紧跟着方法的名字,方法的参数括在圆括号中 , 简单地用关键词 end 来结束方法体 不需要声明变量 result ,当我们赋值给它的时候,它就存在了 puts sayGoodnight("John-Boy") = puts sayGoodnight "John-Boy" = puts(sayGoodnight "John-Boy") = puts(sayGoodnigh
t("John-Boy")) Ruby 的 string 对象,有很多种方法来创建字符串对象,不过最通用的莫过于使用字符串的字面值 ):在单引号或者双引
号之间的字符序列。两者之间的不同是 Ruby 在转换字面值到字符串对象时所处理的数量,单引号的情况下会少。除了一些例外,字符串字面值就是字符串的值。双引号的情况下, Ruby 要做更多的处理, Ruby 要修改双引号括住的字符串,字符串中的 #{ 表达式 } 序列要用表达式的值来替换 , 如果表达式是一个简单的全局变量、实例或者类变量,那么就不必写出大括号
一个 Ruby 方法的返回值默认是最后被求的表达式的值,所以我们可以省略掉 return 语句 def sayGoodnight(name) "Goodnight, #{name}"end
Ruby 名字用法约定:名字前面的第一个字符表明这个名字的用法,局部变量、方法参数和方法名称应该用一个小写字母开头或者一个下划线; fishAndChips, x_axis, _26 全局变量用美元符作为前缀 ($) , $Global, $debug, $CUSTOMER, $_ 而实例变量用 @ 开头, @name, @X 类变量用 @@ 开头; @@x_pos, @@SINGLE最后,类名、模块名和常量应该大写字母开头 : MyClass, Jazz_Song词首字母后面可以是字母、数字和下划线的任意组合(规则规定, @ 后面不可以直接跟数字)。
Ruby 的新特性• 数组和哈希
Ruby 的数组和哈希是有序集合。两者都保存对象的集合,都可以通过键来访问元素。数组的键是一个整数,而哈希支持任何对象作键。数组和哈希都可以生长以便容纳新的元素,对访问元素来说,数组的效率高,但哈希却更灵活。
a = [ 1, 'cat', 3.14 ] # 三个元素的数组 # 访问第一个元素a[0] >> 1 # 设置第三个元素的值a[2] = nil # 输出数组的值 a >> [1, "cat", nil]
创建一个空数组 empty1 = [] 或 empty2 = Array.new # 用数组对象的构造器
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'} nstSection['oboe'] >> "woodwind" instSection['cello'] >> "string" instSection['bassoon'] >> nil
Ruby 的新特性• 控制结构
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 weight < 100 and numPallets <= 30 pallet = nextPallet() weight += pallet.weight numPallets += 1end
如果 if 语句或者 while 语句的主体是一个简单的表达式,这时 Ruby 语句修饰符就是一个很有用的快捷方式。简单地写下这个表达式,后面跟上 if 或者 while 和条件语句。 puts "Danger,Will Robinson" if radiation>3000 square = square*square while square < 1000 这些语句修饰符看起来和 Perl程序员使用的很相似。
…
Ruby 的新特性• 正则表达式
正则表达式虽然晦涩难懂,但确实是处理文本的绝佳工具。《Mastering Regular Expressions》 一个正则表达式就是在一个字符串中用来匹配的特定的模式字符。在 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”匹配所有的可打印字符,简单的一个字符“ .” (一个点号)匹配任意字符。
匹配算符“ =~” 用在正则表达式匹配一个字符串的时候。当模式在字符串中被匹配到后, =~返回它开始的位置,否则返回 nil 。这意味着你可以在 if 语句或者 while 语句中使用正则表达式。 if line =~ /Perl|Python/ puts "Scripting language mentioned: #{line}"end
字符串中与正则表达式匹配的部分也可以被替换成不同的文本,这要使用 Ruby 的替代方法。line.sub(/Perl/, 'Ruby') # 用 "Ruby" 替换第一个 "Perl"line.gsub(/Python/, 'Ruby') # 用 "Ruby" 替换所有的 "Python“
…
Ruby 的新特性• 代码块 (blocks)和迭代器
Ruby 的一个非常特别的功能 -- 代码块:可以和方法调用关联的一系列代码,就好像这些代码是方法的参数一样,这是一个令人难以置信的强大特性。你可以使用代码块实现回调(但不像 Java 的匿名内部类那么简单),传递一系列代码(但要比 C 的函数指针更加复杂),和实现迭代器。代码块是用大括号或者 do...end 括起来的一系列代码:{ puts "Hello" } # 这是一个代码块
do # club.enroll(person) # 这也是代码块 person.socialize #end #
一旦你创建了一个代码块,就可以把它和一个方法调用关联在一起。那个方法能够调用代码块一次或者更多次,用 Ruby 的 yield 语句。 def callBlock yield yieldend
callBlock { puts "In the block" }
结果:In the blockIn the block
你可以在调用 yield 时给出参数,这些参数传递给代码块。在代码块中,列举变量的名字来接受参数,这些参数被用 "|" 括着。a = %w( ant bee cat ) # 创建一个数组a.each { |animal| puts animal } # 迭代所有的内容 produces: antbeecat
…
Ruby 的新特性• 代码块 (blocks)和迭代器
我们来看看实现 Array 类的 each迭代器的可能的方法。 each迭代器遍历数组的每个元素,每次都调用 yield ,类似的代码可能会是下面这样:
# 在 Array 类中 ...def each for each element yield(element) endend
这样你就可以使用数组的 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
…
Ruby 的新特性• 读写
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
有很多种方式来把输入读取到你的程序中,也许,最传统的就是使用 gets 例程,它从你的程序的标准输入流中返回下一行。line = getsprint line
gets 例程有一个附带效果,它除了返回读取的行,还把它储存到全局变量 $_ 中,这个变量很特殊,在很多环境中它作为默认变量使。如果你调用 print 而没有带参数,它显示 $_ 的内容;如果你写一个 if 或者while 语句时仅仅使用一个正则表达式作为条件,那么这个表达式匹配的对象是 $_ 。尽管一些纯粹主义者把这看作是令人讨厌的野蛮行径。例如,下面的程序现实输入流中的所有行中包含 "Ruby" 单词的行。
while gets # assigns line to $_ if /Ruby/ # matches against $_ print # prints $_ endend Ruby 方式的写法是使用迭代器:
ARGF.each { |line| print line if line =~ /Ruby/ }
这里使用了预定义对象 ARGF ,它描述可以被程序读取的输入流。
…
类,对象,变量 • 类
class Song def initialize(name, artist, duration) @name = name @artist = artist @duration = duration endend
在 Ruby程序里, initialize 是一个特别的方法,当我们调用 Song.new 要创建一个 Song 对象时, Ruby 就创建一个未初始化的对象然后调用 initialize 方法,并把我们传递给 new 方法的所有参数都传递给 initialize 方法,这让你有机会写入设置你的对象属性的代码。
aSong = Song.new("Bicylops", "Fleck", 260) aSong.inspect >> "#<Song:0x401b4924 @duration=260, @artist=\"Fleck\", @name=\"Bicylops\">" 默认情况下, inspect 可以被传递给任何对象,它返回对象的 id 和其所有实例变量。 Ruby提供了标准消息, to_s ,它可以发送给那些想要用字符串形式表示的对象 aSong.to_s >> "#<Song:0x401b499c>" 更好的解决办法是我们在类里重载 to_s
…
类,对象,变量 • 类在 Ruby中,类永远不会关闭,你可以向一个已存在的类一直添加方法,无论是你自己写的类还是标准的内置类都是这样。你要做的只是创建一个已存在的类的定义,然后就可以随时随地把你的新内容添加进去了。class Song def to_s "Song: #{@name}--#{@artist} (#{@duration})" end end aSong = Song.new("Bicylops", "Fleck", 260) aSong.to_s >> "Song: Bicylops--Fleck (260)"
类,对象,变量 • 继承和消息
class KaraokeSong < Song def initialize(name, artist, duration, lyrics) super(name, artist, duration) @lyrics = lyrics endend
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...") aSong.to_s >> "Song: My Way--Sinatra (225)"
每个类都应该只处理自身的内部状态,当 KaraokeSong#to_s 被调用的时候,我们让它调用父类的 to_s 方法来处理子类中的歌词细节,它会添加歌词信息然后返回结果,这里的诀窍是 Ruby 的关键字 super 。若你不带参数调用 super , Ruby 发送一个消息给当前类的父类,请求父类调用与当前方法同名的方法,并且把我们传递给当前方法的参数传递过去。 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 就默认指定 Object 类为其父类(看到 Java的影子了吗)。
类,对象,变量 • 继承和混合
一些面向对象语言(特别是 C++ )支持多重继承从而继承每个父类的功能,多重继承即一个类可以继承于多个直接父类。这种特性既强大又危险,因为它会使继承层次变得晦涩难懂。
其它一些语言,比如说 Java ,只提供了单继承,一个类仅有一个直接父类,虽然很简洁(也容易实现),但也存在缺点 ---- 在真实世界中,事物常常是从多个源继承得到属性(比如说,一个球既是弹跳的物体也是球形的物体)。
Ruby提供了一种巧妙而且功能强大的折衷办法,让你既可以拥有单继承的简练又可以拥有多重继承的强大。一个 Ruby 类只许有一个直接父类,这时 Ruby 是单继承的,同时 Ruby又可以包含任意多个混合的功能(混合就像是类定义的一部分),这时 Ruby 实现了类似多
重继承的功能而又没有什么缺陷。
类,对象,变量 • 单继承与混合插入(Mix-Ins)
Bill Venners: 在你的一篇文章中曾经写道:“ Ruby只支持单继承,我把它作为一个Ruby 的一个特性。”为什么单继承是值得你考虑的呢?混合插入又是什么?
Yukihiro Matsumoto: 单继承是非常优秀的,整个类继承结构形成了一颗单树根结点的单叉树,叫做对象,这是非常容易理解的。在一些支持多继承的程序设计语言当中,类形成了一个网络,这样反而难以理解。
虽然单继承具有简单的树形结构,使得他非常地优秀,但是有时我们会想在单继承的限制外类之间共享要素或者方法。 Java 在这种情况下必须定义一个接口来共享方法签名,通常是使用某种从一个对象到另一个对象的托管。在 Ruby 当中,我们定义了一个叫做“模”的东西,看起来很像类,但有很多的限制:即是你不能为模创建实例,模不能从类继承得到。所以在 Ruby 当中,如果你定义了一个具有方法的模并且将这个模插入了类当中,这样类就具有了这些方法,包括签名和实现。如果你这时将同样的模插入到另外一个类当中,就会有两个类来共享这个实现。这赋予了多继承相当多的益处,同时又没有破坏单继承简单的树形模式。
这种插入模的方法被称为“混合插入( Mix-ins )”。混合插入最初是从 LISP 语言文化中开始的,是作为多继承的一个使用。事实上,一个混合插入是一个使用多继承的精确手段。所以在 LISP 当中使用多继承是一种象征。而在 Ruby 当中,我们通过支持类和模强制你使用混合插入。
类,对象,变量 • 对象和属性
class Song def name @name end def artist @artist end def duration @duration end end
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 是变量的值
类,对象,变量 • 可写属性 class JavaSong { // Java code
private Duration myDuration; public void setDuration(Duration newDuration) { myDuration = newDuration; }}s = new Song(....)s.setDuration(length)
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 实际上,以等号结尾的方法定义让方法名能够出现在赋值语句左边
Ruby也提供了创建写属性方法的快捷方式。
class Song attr_writer :durationendaSong = 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” ,与全局变量和实例变量不同,类变量在使用前必须要初始化
有时,一个类需要提供不依赖于任何特定实例对象的方法。
我们已经碰到过这样的方法, new 方法创建一个新的 Song 对象,但和特定的歌曲没有什么关系。
aSong = Song.new(....)
类方法与实例方法的定义方式不同。定义类方法要在方法名前加上类名和一个点号 (".") 。
class Example
def instMeth # 实例方法 end
def Example.classMeth # 类方法 end
end
类,对象,变量 • 单例与其它构造器 有时你可能会想要重写 Ruby 的默认构造方法,我们仍旧用点唱机来做例子。因为我们在全国各处有很多点唱机,于是就想要让维护工作变得简单一些。
我们需要把在点唱机上发生的事件以日志形式全部记录下来,像被点唱的歌曲,收到的钱,可疑的流量等等,为了节约带宽,这些日志保存在本地,我们需要一个类来处理这些日志,但是,我们希望一个点唱机只有一个日志对象,还希望所有使用这个日志对象的其它对象共享它(译者注:意即所有对象只使用一个日志对象)。
通过使用《设计模式》中提到的单例模式,要做到上述这些要求就只有一种办法来创建日志对象,调用 Logger.create 方法,还要确保只有一个日志对象被创建。
class Logger private_class_method :new @@logger = nil def Logger.create @@logger = new unless @@logger @@logger endend
把 Logger 的 new 方法声明为私有来防止其他人调用默认构造器创建日志对象,取而代之,我们提供了一个类方法 Logger.create ,它使用类变量 @@logger 来保持对一个单个的日志对象的实例的引用,每次调用这个方法时都返回这个实例。(我们这里介绍的单例的引用并不是线程安全的,如果多个线程同时运行,就有可能会创建多个日志对象。我们可以使用 ruby提供的单例混合,而不必自己处理线程安全问题。)我们可以查看方法返回的对象标识符来观察一下。
Logger.create.id >> 537766930 Logger.create.id >> 537766930
使用类方法来伪造构造器,也可以让那些使用你的类的人倍感轻松。举一个小例子, Shape 类描述正多边形,创建 Shape 类的实例需要提供边数和总的周长给构造器。
class Shape def initialize(numSides, perimeter) # ... endend
但是,几年以后这个类用在另外的程序中,程序员需要通过指定名字和边的长度来创建多边形而不是用周长,这时只需要简单地向 Shape 类中添加一些类方法。
class Shape def Shape.triangle(sideLength) Shape.new(3, sideLength*3) end def Shape.square(sideLength) Shape.new(4, sideLength*4) endend
类,对象,变量 • 访问控制设计了一个类的接口,重点要考虑的是你的类暴露给外界多大权限的访问,如果允许过多的访问,会导致你的程序与外界耦合紧密 ---- 使用你的类的用户会更多的依赖类的实现细节,而不是它自己的逻辑接口。好在 Ruby 改变对象属性的唯一途径是调用对象的方法,这样,控制了对方法的访问,也就控制了对对象的访问。一个非常好的原则是永远不要把会使对象的属性值非法的方法暴露, Ruby提供了三种保护级别。
pubic methods 可以被任何对象调用,不存在访问控制。方法默认都是公有的( initialize 除外,它永远是私有的)
protected methods 可以被定义它的类和其子类的对象访问,访问只限于家族内
private methods 不能被外界调用,因为调用私有方法时无法指定对象,所以只能在定义它的类和类直接派生的对象中使用
在 ruby 中 "protected" 和 "private" 两者间的区别甚至要超出其它大多数面向对象语言。如果一个方法是保护的,它可以在定义它的类或者子类的实例中调用。如果一个方法是私有的,则只能在调用它的对象的上下文处调用,不可能直接调用另一个对象的私有方法,即便这个对象和该对象都是同一个类的实例。
Ruby 和其他 OO 语言另一个重要的不同点在于,访问控制是动态确立的,就是说程序运行时,而不是静态的,只有你的代码执行受限的方法时才会得到访问违例。
类,对象,变量 • 变量
现在我们创建了所有这些对象,但是问题是要保证不会丢失它们,变量就是用来跟踪对象的,每个变量保存一个对象的引用 person = "Tim" person.id >> 537771100 person.type >> String
person >> "Tim" 变量是对象吗?
NO! 一个变量只是一个对象的简单引用,对象漂浮在某处的大池子里(堆栈,大多数时候是这里),变量指向对象
person1 = "Tim" person2 = person1 person1[0] = 'J' person1 >> "Jim" person2 >> "Jim"
变量保存对象的引用,而不是对象本身。把 person1 赋值给 person2并不创建新的对象,只是简单地把 person1 的对象引用拷贝给 person2 ,所以 person1 和 person2 都指向同一对象 .
类,对象,变量 • 变量 你可以使用 String 的 dup 方法来避免混淆,它创建一个有着相同内容的新 String 对象
person1 = "Tim" person2 = person1.dup person1[0] = "J" person1 >>[/tab]"Jim"
person2 >>[/tab]"Tim"
如果你不想别人修改一个特定的对象,可以冻结它。尝试修改一个被冻结的对象, Ruby 会抛出一个 TypeError异常。
person1 = "Tim"person2 = person1person1.freeze # 避免对象被修改person2[0] = "J"produces: prog.rb:4:in '=': can't modify frozen string (TypeError)from prog.rb:4
容器,代码块,迭代器 • 容器
在实现以前,我们需要决定怎样在 SongList 对象中储存歌曲列表,有三种方法,使用 Ruby 的 Array 类型或者 Hash 类型,要不然就自己做一个列表结构。
数组Array 类保存着对象引用的集合。每一个对象的引用在数组中都有一个位置。数组有大量有用的方法,通过它们你可以把数阻当
成堆、栈、集、队列、双列、先入先出列等a = [ 3.14159, “pie”, 99 ]a.type >> Array a.length >> 3 a[0] >> 3.14159 a[1] >> “pie” a[3] >> nil b = Array.new b.type >> Array b.length >> 0 b[0] = “second” b[1] = “array” b >> [“second”, “array”]
数组通过 []操作符来索引,就像大多数的 Ruby操作符,这实际上也是一个方法(在 Array 类中)所以也可以在子类中重载a = [ 1, 3, 5, 7, 9 ] a[-1] >> 9 以使用一对数字来索引数组, [start,count] 。这会返回一个新数组,它由从 start 开始的 count 个对象的引用组成 a[1, 3] >> [3, 5, 7] a[-3, 2] >> [5, 7] # 可以使用区间来索引数组,开始和结束位置之间插入两个或者三个点,两个点的形式表示包含结束位置,三个点不包含 a[1..3] >> [3, 5, 7] a[1...3] >> [3, 5] #[]操作符对应的有 []=操作符,通过它可以设置数组元素的值 a[1] = 'bat' >> [1, "bat", 5, 7, 9] a[3] = [ 9, 8 ] >> [1, "bat", 5, [9, 8], 9] a[6] = 99 >> [1, "bat", 5, [9, 8], 9, nil, 99]
容器,代码块,迭代器 • 容器
数组# 如果 []= 的索引有两个数(开始和长度)或者是一个区间,那么在原始数组中的对应元素就会被操作符右边的值代替;如果索引的长度为 0 ,那么右边的值就插入到开始位置前面,不删除元素;如果右边也是一个数组,它的元素代替原始数组的元素;如果索引所选择的元素数目和右边的不一样,那么就自动调整数组的大小来适应 a = [ 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]
哈希哈希(有时被认为是数组和字典的结合)和数组一样是用来储存对象引用的集合。区别于通过整数来索引数组,你可以通过任意类型的对象来索引哈希:字符、正则表达式等;对比于数组,哈希有一个显见的好处就是可以使用任意对象做索引,但同时一个显见的不好处就是它的元素是无序的,所以你不能简单地把哈希用作堆栈或者队列。
实现一个 SongList容器我们先列出在 SongList 中所需的基本的方法的一个列表。
append( aSong ) >> list # 向列表中添加指定的歌曲deleteFirst() >> aSong # 从列表中删除第一首歌曲并返回该歌曲deleteLast() >> aSong # 从列表中删除最后一首歌并返回该歌曲[anIndex] >> aSong # 从列表中返回 anIndex 所索引的歌曲,可以是整数索引或者歌曲的标题。
这个列表给我们一些实现方法的提示。在列表尾端添加歌曲的功能,在最前和最后位置删除歌曲的功能。建议使用双列 ---- 一个两端队列 ---- 这样我们可以使用 Array 来实现,同样,数组也支持用一个整数来索引歌曲。
但是我们也需要使用歌曲标题来索引歌曲,可能会想到使用哈希,那样用标题做键歌曲做值。那么可以使用哈希吗?也许可以,不过这样有问题。首先哈希是无序的,所以我们不得不使用一个辅助的数组来跟踪列表。一个更大的麻烦是哈希不支持多个键对应一个值,这会给我们的播放列表带来麻烦,因为一首歌可能会被播放许多次。我们可以在一个歌曲的数组中搜索需要的歌曲标题,如果这会成为执行上的瓶颈,那么我们会在后面加入一些基于哈希的查找特性。
容器,代码块,迭代器 • 容器
我们从一个基本的 initialize 方法开始我们的类,创建一个数组用来存放歌曲和一个引用它的实例变量 @songs 。class SongList def initialize @songs=Array.new endend
SongList#append 方法在 @songs 数组末尾添加歌曲,返回它自己也就是当前的 SongList 对象。这是一个有用的特性,可以让我们把多个 append 调用联接在一起,后面会看到这个例子。class SongList def append(aSong) @songs.push(aSong) self endend
然后添加 deleteFirst 和 deleteLast 方法,简单地用 Array#shift 和 Array#pop 来分别实现。class SongList def deleteFirst @songs.shift end def deleteLast @songs.pop endend
下一个方法是 [] ,通过索引来访问元素。如果索引是整数(在这里我们用 Object#kind of? 来检查),那么返回该位置的元素。class SongList def [](key) if key.kind_of?(Integer) @songs[key] else # ... end endend
现在需要添加通过歌曲标题来索引的功能,这要求扫描整个歌曲列表,检查每一首歌曲标题。我们需要先来熟悉一下 Ruby 最简洁的一个特性:迭代器。
容器,代码块,迭代器 • 代码块和迭代器
class SongList def [](key) if key.kind_of?(Integer) return @songs[key] else for i in [email protected] return @songs[i] if key == @songs[i].name end end return nil endend
它能工作,但能不能做的更自然些呢?这里 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 endend
我们还可以把 if 用作语句修饰符来缩短句子。class SongList def [](key) return @songs[key] if key.kind_of?(Integer) return @songs.find { |aSong| aSong.name == key } endend
find 方法是一个迭代器,一个重复调用代码块的方法。迭代器和代码块是 Ruby 最有趣的特性中的两个,所以我们花些时间来研究一下它们(这个过程中我们也可以看到我们的 [] 方法是如何真正工作的)。
容器,代码块,迭代器 • 代码块和迭代器实现迭代器
一个 Ruby迭代器不过是一个简单的方法,它可以调用代码块。一个代码块出现在一个方法调用的代码附近,代码块和方法的最后一个参数处在同一行;代码块中的代码并不被执行,而是 Ruby 保存代码块出现时的上下文关系(局部变量、当前对象等等),然后进入到方法中。这正是魅力所在。在方法中,代码块通过 yield 语句被调用,这使得代码块就好像是一个方法一样。当 yield执行时,它调用代码块中的代码。代码块退出时,控制就马上被返回给 yield 后面的语句。def threeTimes yield yield yieldendthreeTimes { puts "Hello" } 结果 : HelloHelloHello代码块最有趣的地方是你可以给它传递参数还可以取得它的返回值,例:打印一段菲波纳奇数列def fibUpTo(max) i1, i2 = 1, 1 # 并行赋值 while i1 <= max yield i1 i1, i2 = i2, i1+i2 endendfibUpTo(1000) { |f| print f, " " }结果:1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987在这个例子中, yield 有一个参数,这个值被传递给关联的代码块。代码块的定义中,参数出现在两个竖条之间。这个例子中,变量 f 接受了 yield 传递来的值,所以代码块就连续地显示出数列的数了。如果代码块的参数数目和 yield 传递来的不一样时该怎么办呢?这和并行赋值下的规则是一致的(些小的差别是:如果代码块只有一个参数那么 yield 传递来的多个参数会转换成一个数组)。
容器,代码块,迭代器 • 代码块和迭代器代码块的参数也可能是已存在的局部变量,如果是这样的话,代码块执行完毕后变量的新值会被保留下来。这可能会导致不可预测的结果,但是她也带来一个性能上的优势,变量已经存在了。
代码块也可以给方法返回值,在代码块中最后被计算的表达式的值被作为 yield 的值回传给方法。 Array 类中的 find 方法就是如此。 [find 方法实际上在 Enumerable 模块中被定义,该模块混合在 Array 中。 ] 它的实现就像下面这样。Array 类完成了它所能完成的,访问数组元素,撇开程序代码而专注于自身特定的需求。另外两个通用的迭代器是 each 和 collect 。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 [1, 3, 5 ].each { |i| puts i } ["H", "A", "L"].collect { |x| x.succ } >> ["I", "B", "M"]
容器,代码块,迭代器 • 代码块和迭代器事务代码块
代码块可以定义为在某种事务控制下运行的一系列代码,举例来说,你经常要打开一个文件,对文件的内容进行一些处理,然后确保在使用完毕后关闭了它。粗略的实现(忽略了错误处理):
class File def File.openAndProcess(*args) f = File.open(*args) yield f f.close() endend
File.openAndProcess("testfile", "r") do |aFile| print while aFile.getsend结果:This is line oneThis is line twoThis is line threeAnd so on...
一旦文件被打开, OpenAndProcess 调用 yield ,把打开的文件对象传递给代码块。当代码块返回后,文件被关闭,这样,关闭的责任就从文件对象的使用者转移给文件本身了。这种文件管理自己的生命周期的技巧非常有用,所以 Ruby 的 File 类直接就支持了这种特性。如果 File.open 有一个关联的代码块,那么这个代码块将会随一个文件对象而被调用,到代码块终止时文件对象被关闭。这很有趣,意味着 File.open 有两种不同的行为。 class File def File.myOpen(*args) aFile = File.new(*args) # 如果有代码块,传递文件,然后等代码块返回时关闭文件 if block_given? yield aFile aFile.close aFile = nil end return aFile endend
容器,代码块,迭代器 • 代码块和迭代器代码块可以转换为闭包
class JukeboxButton < Button def initialize(label, &action) super(label) @action = action end def buttonPressed @action.call(self) endend
bStart = JukeboxButton.new(“Start”) { songList.start }bPause = JukeboxButton.new(“Pause”) { songList.pause }
这里关键是 JukeboxButton#initialize 的第二个参数。如果一个方法的最后一个参数有 & 前缀, Ruby 就会在方法被调用时查找一个代码块,这个代码块被转变成一个 Proc 类的对象并且分配成参数。你可以把这个参数当作任意的变量。在我们的例子中,我们把它赋给实例变量 @action 。当回调方法 buttonPressed 被调用时,我们使用对象的 proc#call 方法来调用块。
创建一个 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 "
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 ,你可以对一个列表实现一个反向的循环迭代,而不需要知道列表内部是如何实现的。
Ruby 作者关于代码块和闭包的谈话全文 • 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 。
Ruby 作者关于代码块和闭包的谈话全文 • 使用 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 传递给多个方法。
Ruby 作者关于代码块和闭包的谈话全文 • Bill Venners: 原来如此 , 但是获取上下文环境有什么好处呢?真正让 Ruby 的 Closure 不同的是它捕捉运行时间的上下文环境,局部变量等等。那么到底拥有上下文环境有什么好处是我们无法通过传递给对象一个代码块所获得的呢?
Yukihiro Matsumoto : 实际上,说实在的,最主要的原因是向 Lisp 语言表达敬意, Lisp提供了真正的 Closure 结构,所以我希望继续提供这个功能。
Bill Venners: 我看到的一个不同之处是: 数据在 Closure 对象和方法之间共享。我想我可以在一个常规的非 Closure 结构的 Block 中放入任何需要的环境数据作为参数来传递,但是 Block 仅仅是对环境数据的一份复制,并不是真正的 Closure. 它并没有共享环境数据。共享是 Closure 和普通的传统函数对象不同的地方。
Yukihiro Matsumoto : 是的,共享允许你做一些有趣的代码演示,但是我觉得它对于程序员的日常工作并没有想象的那么有用。这没什么太大的关系,例如像 Java 的内部类那样的普通复制,在许多场合都在使用。但是通过 Ruby 的 Clousure 结构,我希望表达我对 Lisp 文化的致意。
标准类型 数字
整数的写法是可选的前缀 + 一串数字,数字串中的下划线会被忽略。(前缀包括:0 表示八进制 , 0x 表示十六进制 , 0b 表示二进制)
123456 # Fixnum123_456 # Fixnum (忽略了下划线 )-543 # 负 Fixnum123_456_789_123_345_789 # Bignum0xaabb # 十六进制0377 # 八进制-0b101_010 # 二进制(负)
• 也可以通过在前面加上问号来得到 ASCII码字符对应的整数值和转义序列的值 ?a # 普通字符?\n # 换行符 (0x0a)?\C-a # CTRL+a (0x01)?\M-a # ALT+a?\M-\C-a # CTRL+ALT+a?\C-? # 删除键 所有的数字都是对象,可以相应许多消息 整数还支持几个有用的迭代器 ,包括 times, upto 和 downto ,表示在两个整数之间从上或者下迭代,还有 step ,它更像传统的 for循环
字符串
Ruby 的字符串是 8位字节的简单序列