Ruby 教程

jihua 贡献于2012-05-13

作者 微软用户  创建于2010-10-18 03:22:00   修改者zks  修改于2011-03-28 06:28:00字数75113

文档摘要:Ruby语言的发明人是日本人松本行弘(Matsumoto Yukihiro),大家亲切的称呼他"Matz"。 可能会出乎大家的意料,Ruby并不是一种近年来才诞生的语言,它的历史可以追溯到1993年,Ruby之父Matz开始对脚本语言感兴趣。在通过一些分析和思考之后,Matz认为脚本语言是可以变得很强大和灵活的,于是他准备把脚本语言作为他的发展方向。 和很多人一样,Matz是一个面向对象程序设计的fans,自然而然他想研究一种支持面向对象程序设计的脚本语言。随后的一段时间,他到网络上搜集了一些相关的资料,并且发现了Perl 5,当时Perl 5还没有发布。通过一段时间了解后,Matz.发现Perl 5这并不是他想的东西,所以他放弃了把Perl当作一个面向对象的脚本语言使用的念头。
关键词:

 第一部分 Ruby语言基础 第一章 Ruby语言概述 §1.1 Ruby的历史 Ruby语言的发明人是日本人松本行弘(Matsumoto Yukihiro),大家亲切的称呼他"Matz"。 可能会出乎大家的意料,Ruby并不是一种近年来才诞生的语言,它的历史可以追溯到1993年,Ruby之父Matz开始对脚本语言感兴趣。在通过一些分析和思考之后,Matz认为脚本语言是可以变得很强大和灵活的,于是他准备把脚本语言作为他的发展方向。 和很多人一样,Matz是一个面向对象程序设计的fans,自然而然他想研究一种支持面向对象程序设计的脚本语言。随后的一段时间,他到网络上搜集了一些相关的资料,并且发现了Perl 5,当时Perl 5还没有发布。通过一段时间了解后,Matz.发现Perl 5这并不是他想的东西,所以他放弃了把Perl当作一个面向对象的脚本语言使用的念头。随后Matz转向了Python,Python是一个解释型的、面向对象语言,但是Matz发现Python并不能完全算作“面向对象”语言。Matz认为Python是面向对象和过程化程序设计语言(Procedural Programming Language)的混合产物。Matz希望找到的是一种比Perl更强大、比Python更面向对象的语言,但是很遗憾, 这样的语言当时在地球上并不存在。于是Matz打算自己设计一个全新的编程语言。1993年2月24日是一个值得纪念的日子,在这一天Ruby诞生了。 1995年12月Matz推出了Ruby的第一个版本Ruby 0.95。 在1996年以前,都是Matz.一个人在开发进行Ruby的开发。后来随着Ruby社区的渐渐形成,很多社区成员给了Matz许多有意义的帮助,包括提交bug和patch等。现在,Ruby像其他开源项目一样,有自己的开发团队,任何有能力的个人或团体都可以参与Ruby的开发与进化。 §1.2 Ruby名字的由来 首先明确一点,Ruby并不是其他单词的缩写。受Perl的影响,Matz也想用一种宝石来命名他的新语言,他使用了他的一位同事的生肖石-红宝石。后来,Matz意识到Ruby这个名字十分恰当,首先,在生肖石中,Pearl代表六月,而Ruby代表七月。在字体大小上,Pearl大小是5pt, ruby的大小是5.5pt。所以Ruby这个名字对于一种Perl的后续语言十分合适。 §1.3 Ruby的特点 Ruby是一种功能强大的面向对象的脚本语言,可以使用它方便快捷地进行面向对象程序设计。与Perl类似,而且Ruby具有强大的文本处理功能,使文本处理变得简单。此外还可以方便地使用C语言来扩展Ruby的功能。 若您曾经“想要一种简单的面向对象的语言”,或者认为“Perl的功能虽然好用,但它的语法真让人受不了”,又或者觉得“LISP系列语言的思想不错,但到处都是括号真让人讨厌,最起码算式应该按照通常的样式书写”。那么,Ruby或许能让您满意。 归纳起来,Ruby有以下优点: l 解释型执行,方便快捷 Ruby是解释型语言,其程序无需编译即可执行。 l 语法简单、优雅 语法比较简单,类似Algol系语法。 l 完全面向对象 Ruby从一开始就被设计成纯粹的面向对象语言,因此所有东西都是对象,例如整数等基本数据类型。 l 内置正则式引擎,适合文本处理 Ruby支持功能强大的字符串操作和正则表达式检索功能,可以方便的对字符串进行处理。 l 自动垃圾收集 具有垃圾回收(Garbage Collect,GC)功能,能自动回收不再使用的对象。不需要用户对内存进行管理。 l 跨平台和高度可移植性 Ruby支持多种平台,在Windows, Unix, Linux, MacOS上都可以运行。Ruby程序的可移植性非常好,绝大多数程序可以不加修改的在各种平台上加以运行。 l 有优雅、完善的异常处理机制 Ruby提供了一整套异常处理机制,可以方便优雅地处理代码处理出错的情况。 l 拥有很多高级特性 Ruby拥有很多高级特性,例如操作符重载、Mix-ins、特殊方法等等,是用这些特性可以方便地完成各种强大的功能。 同时,由于是解释型语言,Ruby也有下列缺点: l 解释型语言,所以速度较慢 l 静态检查比较少 §1.4 Ruby和Python的比较 Python是Ruby的劲敌。其功力深厚,可谓“千年蛇妖”。但matz认为Python的功能仍不完美,不然就不会创造Ruby了。 第二章 Ruby编程环境 §2.1 Ruby的安装 Ruby支持多种平台,包括Windows、Linux、各种类UNIX、MacOS X等。 §2.1.1 在Windows 95/98/Me/XP上安装Ruby 对于使用Windows平台的用户,安装Ruby是相当简单直接的事情。最方便的方法是使用“One-Click Ruby Installer”。 不知你有没有听说过SourceForge?SourceForge 是全球最大的开放源代码软件开发平台和仓库。它集成了很多开放源代码应用程序,为软件开发提供了整套生命周期服务。在Ruby世界,也有一个类似的网站,那就是Rubyforge。“One-Click Ruby Installer”是Rubyforge上的一个开源项目,也是Rubyforge上下载量最大的项目之一。这个项目将Ruby语言核心和一系列常用扩展集成到了一起,还包含支持Ruby的免费的IDE工具FreeRIDE和SciTE,除了这些之外还包括帮助文档,示例代码,RubyGems包管理器,Fox GUI库,fxri(Interactive Ruby Help & Console)等。和正如它名字所示,使用它,Ruby安装变得前所未见的容易。你可以在下面的地址下载到它的最新版本: http://rubyforge.org/projects/rubyinstaller/ §2.1.2 在Linux上安装Ruby 在linux下Ruby的安装要稍微复杂一些,推荐使用源码编译的方式安装,这样可以保证安装的是最新版本。 首先到ruby主站http://www.ruby-lang.org/en/ 下载源代码,下载完毕后解压到目录,然后使用以下命令: ./configure ./make; make install 执行上面的命令需要root权限,默认安装到/usr/local下。你也可以使用“./configure --prefix=自定义路径”来指定安装目录。 windows上的ruby one-click installer默认安装了RubyGems,但在Linux下我们需要手动安装RubyGems。RubyGems是一个Ruby的包管理器,我们后边会讲到它。 首先从Rubyforge下载RubyGems的最近版本,地址如下: http://rubyforge.org/projects/rubygems/ 解压RubyGems以后到相应目录下输入ruby setup.rb,屏幕上打印一些日志以后会告诉你安装成功,执行gem -v可以查看gem安装版本号。 §2.2 运行Ruby 下面,我们将以Windows平台下的Ruby环境举例如何运行Ruby。 §2.2.1 使用Ruby 将“Hello World”作为学习计算机语言第一个学写的程序,现在已经成为一种传统。该程序最早出现在由Brian Kernighan和Dennis Ritchie写的经典计算机程序设计教程《The C Programming Language》。我们来看看Ruby世界的“Hello World”: 在Windows中,打开命令行提示符窗口,在提示符上输入“Ruby”并回车,Ruby解释器就会运行并等候输入程序。Ruby可执行文件应该包含在系统搜索路径内。 输入下面的程序: print "Hello World!" 然后按Ctrl+D再按回车键,你就会看到Ruby执行程序的输出结果: 你也可以先将代码保存为文件,然后使用再Ruby解释器执行: §2.2.2 使用FreeRIDE和SciTE FreeRIDE是一个支持Ruby语言的免费IDE环境。FreeRIDE本身就是使用Ruby语言开发,它也是Rubyforge上的重要项目之一。 可以使用FreeRIDE来编写调试和执行Ruby代码,FreeRIDE内置了交互式变成环境和Ruby语言在线帮助,功能十分强大。 Scintilla是一个免费的源代码编辑控件,它完全开放源代码,并允许用户自由地用于开源软件或是商业软件中。SciTE是用这个控件开发了一个编辑软件,在“One-Click Ruby Installer”中,SciTE集成了Ruby语言支持,使用起来非常方便。相比FreeRIDE,它的特点就是使用简单。 §2.2.3 使用fxri Fxri是一个Ruby交互帮助和控制台工具。它不仅可作为语言的在线帮助,而且可以用作交互式Ruby解释器来执行程序。对于学习Ruby语言,fxri是一个非常方便的帮手。 不知你有没有听说过Fox ToolKit,它是相当轻巧的开放源代码的图形库。FXRuby是RubyForge上的一个项目,提供了Ruby语言使用Fox ToolKit的接口。而Fxri正是基于FXRuby开发,Fxri同样是RubyForge上的项目。这样你应该可以猜到Fxri名字的由来J Fxri同时集成了Ruby-irb和Ruby-ri的功能,有了它,你可以抛开Ruby-irb,Ruby-ri了,但如果你用的不是Windows系统的话,算我没说J §2.3 Ruby-irb Ruby-irb是交互式Ruby(Interactive Ruby)的简称,用来从标准输入读入并执行Ruby代码的工具,像一个shell。 使用命令“irb”进入交互式模式,然后可以象输入命令行命令一样输入Ruby代码,代码执行的结果会立刻显示: §2.4 Ruby-ri 和Perl一样,Ruby也设计了嵌入式文档。 ruby-ri就是查看文档的工具。Ruby-ri的执行命令为“ri”,例如你可以通过“ri String.new”来查询String类的new方法: §2.5 RubyGems RubyGems是Ruby社区流行的包管理工具,在以前如果要下载一个Ruby扩展或者应用程序的话,你需要先下载相应的zip包,然后解压缩,再将应用或者扩展安装到Ruby对应的目录中。但是有了 RubyGems所有这些麻烦都没有了,你只需要一条命令就可以从远程服务器上下载相应的包,如果相应的应用包含其他扩展,RubyGems会提示你从远程安装所依赖的扩展。安装后 RubyGems会运行相应的程序生成rdoc帮助文档。当然你也可以将软件包下载到本地运行RubyGems本地安装命令。 统一化的管理带来的好处就是简单,有了RubyGems包管理器,Ruby应用的安装将变得前所未见的容易。RubyGems是Rubyforge下载量最大的项目之一,现在Ruby社区的应用都在朝着RubyGems的方向发展,RubyGems也将成为Ruby事实上的包管理器标准。 RubyGems包管理器的可执行命令是“gem”,gem命令包含很多子命令和相应的选项,例如: gem -h/--help – 显示命令帮助 gem -v/--version – 显示Gems的版本号 第三章 类与对象 Ruby是一种真正的面向对象程序设计语言,面向对象指以对象为中心的理论体系。 l 封装(Encapsulation) 将内部结构和算法隐藏起来,以确保只有特定的过程(也叫方法)才能直接操作数据,其结果是不能从外部直接使用数据构造,同时一旦内部构造发生变化也不会对外界造成不良影响。这种隔离方法就叫做封装。 l 继承 l 多态(Polymorphism) 根据对象的不同选择合适的操作。在Ruby中的实现方法是,根据被调的对象的不同来选择不同的方法。 虽然有很多语言都宣称自己是面向对象的,但是他们往往对面向对象的解释都一样,大多是以自己特有的方式来解释什么是面向对象,而在实际情况中,这些面向对象语言又采用了很多非面向对象的做法。 以 Java 为例:如果你想取一个数字取绝对值,java 的做法是: int num = Math.abs(-99); 也就是将一个数值传递给 Math 类的一个静态函数 abs 处理。为什么这么做?因为在 java 中,数值是基本类型不是类。 而在 Ruby 中,任何事物都是对象,也就是说,数字–99就是对象,取绝对值这样的操作应该属于数字本身,所以Ruby的做法就是: c = -99.abs 在Ruby中,你所操作的一切都是对象,操作的结果也是对象。 §3.1 类的定义 类是对具有同样属性和同样行为的对象的抽象,Ruby中类的声明使用class关键字。定义类的语法如下, class ClassName def method_name(variables) #some code end end 类的定义要在class…end之间,在上面的格式中,ClassName是类名,类名必须以大写字母开始,也就是说类名要是个常量。 看下面的例子: class Person def initialize(name, gender, age) @name = name @gender = gender @age = age end end 若某个类已经被定义过,此时又用相同的类名进行类定义的话,就意味着对原有的类的定义进行追加。 class Test def meth1 puts "This is meth1" end end class Test def meth2 puts "This is meth2" end end 在Test类中,原有meth1方法,我们又追加了meth2方法,这时候,对于Test类的对象,meth1和meth2同样可用。 §3.2 对象,属性和方法 类在实例化后生成对象,在强调对象归属于某类时,有时候我们也使用实例对象一词。 方法(Method)是对对象进行的操作。操作对象(被调)以self来表示。在Ruby中,除去内部类的对象以外,通常对象的构造都是动态确定的。某对象的性质由其内部定义的方法所决定。 看下面的例子,我们使用new方法构造一个新的对象, class Person def initialize(name, gender, age) @name = name @gender = gender @age = age end end people = Person.new('Tom', 'male', 15) 我们可以使用Person.new方法来创建一个Person类的实例对象。以@打头的变量是实例变量,他们从属于某一实例对象,Ruby中实例变量的命名规则是变量名以@开始,您只能在方法内部使用它。 initialize方法使对象变为“就绪”状态,initialize方法是一个特殊的方法,这个方法在构造实例对象时会被自动调用。 对实例进行初始化操作时,需要重定义initialize方法。类方法new的默认的行为就是对新生成的实例执行initialize方法,传给new方法的参数会被原封不动地传给initialize方法。另外,若带块调用时,该块会被传给initialize方法。因此,不必对new方法进行重定义。 在Ruby中,只有方法可以操作实例变量,因此可以说Ruby中的封装是强制性的。在对象外部不可以直接访问,只能通过接口方法访问。 class Person def name @name end def gender @gender end def age @age end end people = Person.new('Tom', 'male', 15) puts people.name puts people.gender puts people.age 输出结果为: Tom male 15 在Ruby中,一个对象的内部属性都是私有的。上面的代码中,我们定义了方法name,gender,age三个方法用来访问Person类实例对象的实例变量。注意name,gender,age访问只能读取相应实例变量,而不能改变它们的值。 我们也可以用成员变量只读控制符attr_reader来达到同样的效果。 class Person attr_reader :name, :gender, :age end 类似地,我们可以定义方法去改变成员变量的值。 class Person def name=(name) @name=name end def gender=(gender) @gender=gender end def age=(age) @age=age end end people = Person.new('Tom', 'male', 15) people.name = "Henry" people.gender = "male" people.age = 25 也可以用成员变量写控制符attr_writer来达到同样的效果。 class Person attr_writer :name, :gender, :age end 我们也可以使用attr_accessor来说明成员变量既可以读,也可以写。 class Person attr_accessor :name, :gender, :age end 也可以使用attr控制符来控制变量是否可读写。attr 只能带一个符号参数, 第二个参数是一个 bool 参数,用于指示是否为符号参数产生写方法。它的默认值是 false,只产生读方法,不产生写方法。 class Person attr :name, true #读写 attr :gender, true #读写 attr :age, true #读写 attr :id, false #只读 end 注意attr_reader,attr_writer,attr_accessor和attr不是语言的关键字,而是Module模块的方法。 class Test attr_accessor :value end puts Test.instance_methods - Test.superclass.public_methods 执行结果为: value value= 上面代码中,我们使用Test.instance_methods得到Test类所有的实例方法,使用Test.superclass.public_methods得到Test父类所有的实例方法,然后相减就得到Test类不包含父类的所有的实例方法。 由于instance_methods方法返回值为一个Array,所以我们作差值运算,Array的具体操作后面章节会讲到。 也可以重定义方法,重定义一个方法时,新的定义会覆盖原有的定义。 下面的例子重定义类中的方法meth1, class Test def meth1 puts "This is meth1" end end a = Test.new a.meth1 class Test def meth1 puts "This is new meth1" end end a. meth1 执行结果为: This is meth1 This is new meth1 重定义同一个类时,意味着对原有定义进行补充,不会覆盖原来的定义。而重定义方法时,则会覆盖原有定义。 我们可以使用self标识本身,self和Java中的this有些类似,代表当前对象。 class Person def initialize(name, gender, age) @name = name @gender = gender @age = age end def <=>(other) self.age <=> other.age end end <=> 方法通常意思为比较,返回值为-1,0或1分别表示小于,等于和大于。 §3.3 继承 Ruby继承的语法很简单,使用 < 即可。 class Student < Person def initialize(name, gender, age, school) @name = name @gender = gender @age = age @school = school end end Ruby语言只支持单继承,每一个类都只能有一个直接父类。这样避免了多继承的复杂度。但同时,Ruby提供了mixin的机制可以用来实现多继承。 可以使用super关键字调用对象父类的方法,当super省略参数时,将使用当前方法的参数来进行调用。 class Base def meth(info) puts "This is Base #{info}" end end class Derived < Base def meth(info) puts "This is derived #{info}" super end end obj1 = Derived.new obj1.meth("test") 执行结果为: This is derived test This is Base test 如果传入的参数被修改再调用super的话,那么将会使用使用修改后的值。 class Base def meth(info) puts "This is Base #{info}" end end class Derived < Base def meth(info) puts "This is derived #{info}" info = "over" super end end obj1 = Derived.new obj1.meth("test") 执行结果为: This is derived test This is Base over §3.4 特殊方法与特殊类 特殊方法是指某实例所特有的方法。一个对象有哪些行为由对向所属的类决定,但是有时候,一些特殊的对象有何其他对象不一样的行为,在多数程序设计语言中,例如C++和Java,我们必须定义一个新类,但在Ruby中,我们可以定义只从属于某个特定对象的方法,这种方法我们成为特殊方法(Singleton Method)。 class SingletonTest def info puts "This is This is SingletonTest method" end end obj1 = SingletonTest.new obj2 = SingletonTest.new def obj2.info puts "This is obj2" end obj1.info obj2.info 执行结果为: This is This is SingletonTest method This is obj2 有时候,我们需要给一个对象定义一系列的特殊方法,如果按照前面的方法,那么只能一个一个定义: def obj2.singleton_method1 end def obj2.singleton_method2 end def obj2.singleton_method3 end …… def obj2.singleton_methodn end 这样做非常繁复麻烦,而且无法给出一个统一的概念模型,因此Ruby提供了另外一种方法, class << obj …… end obj是一个具体的对象实例,class << 代表它的特殊类。 class SingletonTest def meth1 puts "This is meth1" end def meth2 puts "This is meth2" end end obj1 = SingletonTest.new obj2 = SingletonTest.new class << obj2 def meth1 puts "This is obj2's meth1" end def meth2 puts "This is obj2's meth2" end end obj1.meth1 obj1.meth2 obj2.meth1 obj2.meth2 执行结果为: This is meth1 This is meth2 This is obj2's meth1 This is obj2's meth2 §3.5 类变量与类方法 类变量被一个类的所有实例对象共享,也可以被类方法访问到。类变量名以‘@@’,开始,例如‘@@number’。和全局变量,实例变量不同,类变量在使用前必须初始化: class Person @@number = 0 #使用前必须有初值 def initialize(name, gender, age) @name = name @gender = gender @age = age @@number += 1 end end 类变量是私有的,在类外无法直接访问,你只能通过实例方法和类方法去访问它。 同样,类方法是属于一个类的方法,定义类方法时需要在方法前加上类名: class Person @@number = 0 def initialize(name, gender, age) @name = name @gender = gender @age = age @@number += 1 end def Person.getNumber #类方法 return @@number end end 除了Person.getNumber这种方式定义类方法外,还可以使用其它方式定义类方法,在后续章节可以陆续见到。 §3.4 存取控制 当你设计一个类时,你需要决定哪些属性和方法可以在类外被访问到,哪些属性和方法在类外被隐藏。如果一个类有过多的属性和方法在类外可以被访问到,那么势必破坏这个类的封装性。幸运的是在Ruby中,只能通过方法去改变一个类的属性,这样我们只需要考虑方法的存取控制。 方法的存取控制有三种: l 公有方法(Public Method) n 方法在任何地方都可以被调用,这是方法的默认存取控制。除了initialize和initialize_cpoy方法,他们永远是私有方法。 l 保护方法(Protected Method) n 方法只能被定义这个方法的类自己的对象和这个类的子类的对象所访问。 l 私有方法(private Method) n 方法只能被定义这个方法的类的对象自己访问,即使是这个类的其他对象也不能访问。 Ruby中的保护方法和私有方法与一般面向对象程序设计语言的概念有所区别,保护方法的意思是方法只能方法只能被定义这个方法的类自己的对象和子类的对象访问,私有方法只能被对象自己访问。 class Test def method1 #默认为公有方法 … end protected #保护方法 def method2 … end private #私有方法 def method3 end public def test_protected(arg) #arg是Test类的对象 arg.method2 #正确,可以访问同类其他对象的保护方法 end def test_private(arg) #arg是Test类的对象 arg.method3 #错误,不能访问同类其他对象的私有方法 end end obj1 = Test.new obj2 = Test.new obj1.test_protected(obj2) obj1.test_private(obj2) 可以看到,和C++/Java相比,Ruby提供了更好的封装性。 也可以使用以下更简单的形式: class Test def method1 ... end def method2 ... end def method3 ... end def methdo4 ... end public :method1 protected :method2 private :method3, :method4 end Ruby和C++/Java的一个显著不同是存取控制是程序运行时决定的而不是静态绑定的。所以只有在访问一个受限制的方法时才会产生运行时错误。 §3.6 元类 在Ruby中一切都是对象。类和实例对象都是对象。这句话听起来有点拗口,让我们来看一个例子: class Person def initialize(name, gender, age) @name = name @gender = gender @age = age end end a = Person.new('Tom', 'male', 15) puts a.object_id => 22429840 puts Person.object_id => 22429960 没错,类也是对象,这是Ruby和C++/Java的一个显著不同,在C++/Java中,类仅仅是一个数据抽象,并没有类也是对象这样的概念。而在Ruby中存在着元类的概念,类也是对象,所有类都是元类的实例对象。和C++/Java相比,Ruby的面向对象程度更高。 可以看到,类对象和实例对象一样有自己的ojbect_id,你可以象调用一个实例对象的方法一样去用它去调用类方法。所有类对象的类是Class类,Oject类是所有类的基类。 003:0> Object.class => Class 004:0> Object.superclass => nil 这样,我们可以从另一个角度去理解类变量与类方法,类变量就是一个类对象的实例变量,类方法就是指一个类对象类的特殊方法。 类方法具体可分为两种:第一种是在所有的类的父类Class中定义的,且被所有的类所共享的方法;第二种是各个类所特有的特殊方法。 类方法中的self指的是类本身,这点需要牢记,这样我们可以使用多种方式定义类方法。 class Test #定义类方法方式1 def Test.meth1 # ... end #定义类方法方式2 def self.meth2 # ... end #定义类方法方式3 class << Test def meth3 # ... end end #定义类方法方式4 class << self def meth4 # ... end end end §3.7 Ruby的动态性 可以重新定义同一个方法, class RedefTest def meth puts "This is meth" end end obj1 = RedefTest.new obj1.meth class RedefTest def meth puts "This is new meth" end end obj1.meth 执行结果为: This is meth This is new meth 可以使用undef_method取消一个方法的定义, class UndefTest def meth puts "This is meth" end end obj1 = UndefTest.new obj1.meth class UndefTest undef_method(:meth) end obj1.meth 执行结果为: This is meth test.rb:14: undefined method `meth' for # (NoMethodError) §3.8 变量 变量名长度只受内存大小的限制。可以通过区分Ruby变量名的首字符来区分它是局部变量、实例变量、类变量、全局变量还是常量。通常情况下,变量名的第二位字符以后是数字、字母或下划线,但有的内部变量名比较特殊,如“$?”。 §3.8.1 局部变量 局部变量以小写字母或下划线开始。 num = 1 foo 局部变量的作用域起始于声明处,结束于该声明所在的块、方法定义、类/模块定义的结尾。 2.times { p defined?(num) num = 10 p num } 输出为: nil 10 nil 10 即使声明部分未被解释器执行仍有效,因为已经经过解释器的处理。 v = 1 if false p defined?(v) p v 输出为: "local-variable" nil 但若块已经变成过程对象的话,则局部变量将一直持续到该过程对象终结为止。若多个过程对象引用同一个作用域的话,局部变量将被这些对象所共享。 (to-do例子) §3.8.2 实例变量 以@开始的变量是实例变量,实例变量属于特定的对象。 class Person def initialize(name, gender, age) @name = name @gender = gender @age = age end end 上面的例子中,@name, @gender,@age都是实例变量。可以在类或子类的方法中引用实例变量。若引用尚未被初始化的实例变量的话,其值为nil。 §3.8.3 类变量 以@@开始的变量是类变量。类变量在类的定义中定义,可以在类的特殊方法、实例方法等处对类变量进行赋值和引用。类变量被类,类的子类和他们的实例对象共享。 class Person @@number = 0 #使用前必须有初值 def initialize(name, gender, age) @name = name @gender = gender @age = age @@number += 1 end end 类变量是私有的,在类外无法直接访问,你只能通过实例方法和类方法去访问它。可以把类变量看作一种被类、子类以及它们的实例所共享的全局变量。 模块中定义的类变量(模块变量)被所有包含该模块的类所共享。 module TestModule @@foo = 10 end class Klass include Foo p @@foo += 1 # => 11 end class Base include Foo p @@foo += 2 # => 12 end §3.8.4 全局变量 以$开始的变量是全局变量,全局变量可以在程序的任何地方加以引用。全局变量无需变量声明。引用尚未初始化的全局变量时,其值为nil。 Ruby运行时环境预定义了一系列的全局变量,有关预定义的全局变量的信息,请参见附表。 §3.8.5 常量 常量以大写字母开始,常数的定义和初始化由赋值过程完成。 PI = 3.14 E = 2.71 若对已定义的常数进行赋值的话,会出现警告信息。若引用未定义的常数会引发NameError异常。 PI = 3.14 obj1 = 2 * PI * 10 PI = 3.1415 # warning: already initialized constant PI obj2 = Foo #uninitialized constant Foo (NameError) 常量可以定义在类和模块中,不能定义在方法中。 class Meth PI = 3.14 #OK end def circle_area(arg) PI = 3.14 #ERROR PI * arg * arg end 若想在外部访问类或模块中的常数时,要使用“::”操作符。 class Meth PI = 3.14 end def circle_area(arg) Math::PI * arg * arg end 在类定义表达式生成类对象的同时,还会将类对象赋值给一个与该类同名的常数,引用类名也就是引用该常数。 class Test end p Test.class #Class p Test #test 若想访问Object类中的常数(顶层的常数)时,也需要也使用"::"操作符,但操作符左边为空。 §3.8 与定义有关的操作 §3.8.1 alias Alias关键字给方法或全局变量添加别名。可以给方法名指定一个标识符或Symbol作为别名。给方法添加别名时,别名方法将和此刻的原始方法绑定,此后即使重新定义了原始方法,别名方法仍然保持着重定义前的老方法的特性。若改变了某方法的内容后,又想使用修改前的方法时,别名会很有用。也可以使用Module#alias_method给方法添加别名。 # 定义meth方法 def meth puts "This is meth" end #设定别名 alias :orig_meth :meth #重定义foo def meth puts "This is new meth" end p meth 执行结果为: This is new meth nil 给全局变量设定别名意味两个名称指向同一个全局变量。当你向一个赋值时,另一个也会被改变。 $abc = 1 alias $xyz $abc $xyz = 2 p [$abc, $xyz] # => [2, 2] 但是不能给正则表达式中的变量$1,$2等添加别名,另外,有些全局变量对于解释器来说是举足轻重的,若重新定义它们的话,有时会影响解释器的正常工作。 §3.8.2 undef undef用来取消一个方法的定义,也可以使用Module#undef_method方法取消方法的定义。undef会取消方法名和方法定义之间的关系,即使超类中有同名方法,调用时也会引发异常。 class Base def meth puts "This is Base#meth" end end class Derived < Base def meth puts "This is Derived#meth" end end class Test1 < Derived def meth puts "This is Test1#meth" end undef_method(:meth) end obj1 = Test1.new obj1.meth 执行结果为: Tes1.rb:22: undefined method `meth' for # (NoMethodError) 而Module#remove_method方法只负责取消当前类中方法名和方法定义之间的关系,父类的同名方法仍可调用,这点差别非常重要。 class Base def meth puts "This is Base#meth" end end class Derived < Base def meth puts "This is Derived#meth" end end class Test2 < Derived def meth puts "This is Test2#meth" end remove_method(:meth) end obj2 = Test2.new obj2.meth 执行结果为: This is Derived#meth 用alias添加别名或用undef取消定义时,会修改类的接口,而不受父类的限制。继承和Mix-in的功能都是在类中添加方法,而undef则可以取消方法。但是,如果取消了类所必需的方法(被其他方法所调用的方法)的话,其后果不堪设想。 §3.8.3 defined? Defined?用来判断表达式是否定义。若表达式尚未定义,则返回nil,若已经定义,则返回一个字符串描述该表达式的种类。 defined? Val #=> nil defined? true #=> “true” defined? $* #=> "global-variable" defined? Array #=> "constant" defined? Math::PI #=> "constant" defined? num = 0 #=> "assignment" defined? 100 #=> "expression" defined? 100.times #=> "method" 虽然defined?看起来像一个方法,实际上是Ruby语法中的操作符,因此不会对参数进行计算。因此下面的表达式并不会输出“abc”。 defined? print("abc\n") 如果是方法未定义,或方法使用undef或Module#remove_method取消了原有定义,defined?都将返回nil。 注意如果一个方法以大写字母开头,使用defined? 判断时需要在方法名后添加"()"时,否则方法名会被当做常数处理。 def Foo(arg) end p defined? Foo # => nil p defined? Foo() # => "method" Foo = 1 p defined? Foo # => "constant" 还可以使用下列特殊用法: l 判断yield是否可用 defined? yield 若yield调用可用,则返回真,具体返回值为字符串"yield"。它的作用同block_given?一样,可以判断能否以带块方式来调用某方法。 class Base def foo puts defined? yield end end a = Base.new a.foo a.foo {} 执行结果为: nil yield l 判断super是否可用 defined? super 若super可被调用,则返回真, 具体返回值为字符串"super"。 class Base def foo end end class Derived < Base def foo puts defined? super end def fun puts defined? super end end obj = Derived.new obj.foo obj.fun 执行结果为: super nil l 返回没有赋值但已经定义的局部变量. defined? a = 1 #=> assignment p a # => nil l 在正则表达式中使用 /(.)/ =~ "foo" p defined? $& # => "$&" p defined? $1 # => "$1" p defined? $2 # => nil 第四章 基本类型 §4.1 Array Array也称作数组,是一系列元素的有序集合。你可以显式使用Array类的new方法来创建一个数组对象,你也可以用方括号包围起来一些以逗号分隔的数字或字符串构成一个数组。 007:0> a = [ "first" "second" "third" ] => ["firstsecondthird"] 008:0> a = [ "first", "second", "third" ] => ["first", "second", "third"] 009:0> a.class => Array 010:0> a.length => 3 011:0> a[0] => "first" 012:0> a[1] => "second" 013:0> a[2] => "third" 014:0> a[3] => nil 015:0> b = Array.new => [] 016:0> b.class => Array 017:0> b.length => 0 018:0> b[0] = "first" => "first" 019:0> b[1] = "second" => "second" 020:0> b => ["first", "second"] 数组可以使用 [] 来索引,其实 [] 是Array类的一个方法,它甚至可以被子类覆盖(overridden)。Ruby中比较有趣的是有多种对数组的索引方法,你可以用负数来索引数组。负数表示从尾部开始,例如索引为-1表示最后一个元素,索引为-2表示倒数第二个元素,以此类推。 021:0> a = [ 1, 2, 3, 4, 5] => [1, 2, 3, 4, 5] 022:0> a[-1] => 5 023:0> a[-2] => 4 024:0> a[-9] => nil 你也可以使用一对数来索引数组,第一个数表示开始位置,第二数表示从开始位置起的元素数目。 025:0> a = [ 1, 2, 3, 4, 5] => [1, 2, 3, 4, 5] 026:0> a[1, 3] => [2, 3, 4] 027:0> a[3, 1] => [4] 028:0> a[-3, 1] => [3] 你甚至可以用一个范围来索引数组,.. 表示包含尾部元素,... 表示不包含尾部元素。 029:0> a = [ 1, 2, 3, 4, 5] => [1, 2, 3, 4, 5] 030:0> a[1..3] => [2, 3, 4] 031:0> a[1...3] => [2, 3] §4.2 Hash Hash也称作哈希表哈希表,类似于数组但是每个元素都有索引,有时候也被称作关联数组,哈希数组或字典。哈希表和数组不同,数组只能使用数字索引,而哈希表则可以使用任何对象索引。哈希表和数组的另一个显著不同是哈希表中的元素是无序的。在Ruby中每个哈希表都是Hash类的对象。 在哈希表中,我们称索引为Key,被索引的元素称为Value。 我们可以使用=>连接的元素来创建一个哈希表,注意哈希表外部是使用大括号包围。 032:0> h = { "first" => "Amy", "second" => "Mike", "third" => "Tom" } => {"third"=>"Tom", "second"=>"Mike", "first"=>"Amy"} 033:0> h.length => 3 034:0> h["first"] => "Amy" 035:0> h['second'] => "Mike" 036:0> h[100] = "Henry" => "Henry" 037:0> h["nine"] = "Rose" => "Rose" 038:0> h => {"third"=>"Tom", "second"=>"Mike", 100=>"Henry", "first"=>"Amy", "nine"=>"Rose"} §4.3 Number Ruby支持整数类型和浮点数类型。整数可以是任意长度(这个长度只和内存大小有关)。在一定范围内的整数被视为Fixnum类的对象。超出这个范围的整数被视为Bignum类的对象。 num = 81 6.times do puts "#{num.class}: #{num}" num *= num end 运行结果: Fixnum: 81 Fixnum: 6561 Fixnum: 43046721 Bignum: 1853020188851841 Bignum: 3433683820292512484657849089281 Bignum: 11790184577738583171520872861412518665678211592275841109096961 和C/C++相同,Ruby规定以0开头的数为八进制数,以0x开头的数为十六进制数,以0b开头的数为二进制数。 001:0> 16 => 16 002:0> 020 => 16 003:0> 0x10 => 16 004:0> 0b10000 => 16 一个数中间可以用下划线连接,下划线自动被忽略。 005:0> 123_456_789 => 123456789 可以使用"?\C-x"或"?\cx"生成控制字符。 如果一个数包含小数点或者包含"e",那么这个数将被转为Float类的对象。 012:0> 1.0.class => Float 013:0> 1.0e3.class => Float §4.4 String String也称作字符串,是单引号或双引号包围起来的一串字符。单引号和双引号的意义有所不同,双引号包围的字符作变量替换,单引号包围的变量不做替换。可以在字符串中使用 #{expr} 嵌入代码。 022:0> "The seconds in a day is: #{24*60*60}" => "The seconds in a day is: 86400" 023:0> 'The seconds in a day is: #{24*60*60}' => "The seconds in a day is: \#{24*60*60}" 也可以使用 %q 和 %Q 来生成字符串对象。%q 相当于单引号,%Q相当于双引号。 051:0> %q/Single quote/ => "Single quote" 052:0> %Q/Double quote/ => "Double quote" 053:0> %q/ #{50*50} / => " \#{50*50} " 054:0> %Q/ #{50*50} / => " 2500 " %q 和 %Q 后面的第一个字符为分隔符。二哥分隔符之间的字符被认为一个是字符串。但是如果这个分隔符是 [ { <, 那么结束标志为匹配的 ] } >。 055:0> %q{This is a string} => "This is a string" 056:0> %Q[This is a string] => "This is a string" 057:0> %q => "This is a string" 你也可以使用“Here Document”的方法来生成字符串,这种方法规定 << 之后的字符串作为结束标志。 string = < (1..10).to_a => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 004:0> ('bar'..'bat').to_a => ["bar", "bas", "bat"] 005:0> (1...10).to_a => [1, 2, 3, 4, 5, 6, 7, 8, 9] Range除了表示一个序列外还可以出现在条件语句中。在条件语句中,可以把Range看作一个双向开关,当第一个条件满足时打开开关,当第二个条件满足时关闭开关。 a = [0, 1, 2, 3, 4, 5, 6] a.each do |i| print i, " " if i == 1 .. i == 5 end 执行结果为 1 2 3 4 5 上述代码中if i == 1 .. i == 5表示只有满足i==1且不满足i==5时条件为真,当不满足i==1或满足i==5时条件为假,直观地看,就是表示元素需要位于范围之中。 可以使用 === 来测试一个元素是否在某个范围: 093:0> (1..10) === 3 => true 094:0> (1..10) === 30 => false 095:0> (1..10) === 2.71828 => true 096:0> ('a'..'f') == 'c' => false 097:0> ('a'..'f') == 'g' => false Range也可以位于case语句之内: score = 98 case score when 85..100 then puts "A" when 70...85 then puts "B" when 60...70 then puts "C" else puts "D" end 执行结果为: A §4.6 Symbol Symbol是个简单的对象,它使用名字作为唯一的标识符。Symbol对象代表解释器内部一个唯一的名字。Symbol的产生很简单,只需要给一个字符序列前添加“:”或使用“to_sym”方法。 Symbol对象从属于Symbol类。 String和Symbol两者具有紧密的联系。每个symbol都有个字符串的名字(可以使用to_s方法得到)。而每个String可以请求它的相应symbol(通过to_sym方法)。String和Symbol是紧密相联的,但它们不是同一个东西,他们分别是String类和Symbol类的对象。 有读者可能会问,为什么要存在Symbol对象呢?因为symbol可以大大提高速度。Symbol的内部表示是一个整数,用来做Hash表中检索字符串的关键字,而Ruby语言执行时解析器、运算器需要大量的类名字、方法名字的检索,这可以大大加快解析和执行时字符串查找的速度。 想想,如果没有Symbol,如果需要用方法名称作为参数时,我们必须给一个字符串用来表示方法的名称,解释器处理时首先要作字符串解析,然后才能找到出相应的方法,而如果使用Symbol会大大加快这一速度。 在使用中,Symbol往往表示一个名字,例如一个变量 foo的值为1,那么 :foo可以理解为变量名,如果直接引用foo,会得到1,但如果是 :foo就指变量名本身。 Symbol对象是唯一的。每次你在代码中使用:test, 你是要引用一个名字为"test"的Symbol类的对象。Ruby保证系统中只有一个名字为test的Symbol对象, 所以所有对:test的引用都将引用同一个对象。 001:0> module One 002:1> class Test 003:2> end 004:1> $f1 = :Test 005:1> end => :Test 006:0> module Two 007:1> Test = 1 008:1> $f2 = :Test 009:1> end => :Test 010:0> def Test() 011:1> end => nil 012:0> $f3 = :Test => :Test 013:0> $1.object_id => 4 014:0> $2.object_id => 4 015:0> $3.object_id => 4 §4.7 正则表达式 正则表达式的类是Regexp,可以使用/或%r生成正则表达式。 103:0> a = /\s*[a-f]/ => /\s*[a-f]/ 104:0> a.class => Regexp 105:0> b = %r{\s*[a-f]} => /\s*[a-f]/ 106:0> b.class => Regexp 107:0> c = Regexp.new('\s*[a-f]') => /\s*[a-f]/ 108:0> c.class => Regexp 你可以使用Regexp#match(string)方法或者=~运算符来匹配正则表达式,你也可以使用!~来测试是否不匹配。 113:0> sentence = "This is a dog." => "This is a dog." 114:0> sentence =~ /dog/ => 10 115:0> sentence =~ /a/ => 8 116:0> /o/ =~ sentence => 11 117:0> sentence !~ /xyz/ => true 另外,在匹配正则表达式时,会将匹配到的字符串存放在 $& 变量中,$' 变量中存放已经匹配过的字符序列,$` 变量中存放还未匹配的字符序列。 118:0> sentence = "This is a dog." => "This is a dog." 119:0> sentence =~ /a/ => 8 120:0> puts $& a => nil 121:0> puts $' dog. => nil 122:0> puts $` This is => nil 第五章 代码块和迭代器 §5.1 代码块(Block) §5.1.1 什么是代码块 在Ruby中在在大括号之间的代码或放在do/end之间的代码是一个代码块。代码块只能出现在一个方法的后边,它紧接在方法最后一个参数的同一行上。代码块的内容并不会被马上执行,当执行到被调用的方法时,解释器的运行时环境会记住代码块出现的现场,然后执行被调用的方法。 [1,2,3,4,5].each { |i| puts i } [1,2,3,4,5].each do |i| puts i end 一般的使用习惯是:(to-do 具体解释) l 当关心边际(side effect)效应时使用 do/end。 l 当关心返回结果应时使用大括号。 §5.1.2 代码块与对象 代码块并不是对象,但可以方便的转化为Proc类的对象。有三种转化的方法: l 将一个代码块传递给最后一个参数以&开始的方法。 def meth1(p1, p2, &block) puts block.inspect puts block.call end meth1(1, 2) { "This is a block" } l 使用Proc.new方法,后边的参数为一个代码块: block = Proc.new { "a block" } l 调用Kernel.lambda方法: block = lambda { "a block" } 前两种方法是等价的,而第三种方法,用 lambda 生成的 Proc 对象和用 Proc.new 生成的 Proc 对象之间是有差别的。这是一个微妙的差别,这个差别与 return 关键字相关。lambda 中的 return 从 lambda 返回。而 Proc 中的 return 从外围方法返回。 可以看以下两个例子: def test_proc p = Proc.new { return 1 } p.call puts "Never come here" end test_proc #=> 1 执行后"Never come here"不会被输出,执行p.call相当于在test_proc方法内执行了return语句。 def test_lambda p = lambda { return 1 } result = p.call puts "The value is: #{result}" end test_lambda 执行后的结果为: The value is: 1 可见使用lambda生成的Proc对象执行call方法调用时,return表示从lambda包围得块内返回。 在一个代码块中执行next语句会导致代码块返回。返回值就是next语句后带的参数。如果next后没有参数,那么返回值为nil。 def meth2 result = yield "The block result is #{result}" end puts meth2 { next 9 } pr = Proc.new { next 100 } puts pr.call pr = lambda { next } puts pr.call 执行结果为: The block result is 9 100 nil §5.2 迭代器(Iterator) §5.2.1 什么是迭代器 简单的讲,一个迭代器就是一个能接受代码块的方法。当初为了进行迭代操作而设置了带块方法,所以现在它仍然常常被称作迭带器。 [1,2,3,4,5].each { |i| puts i } 上述代码中,each方法反复调用代码块,我们称each方法为一个迭代器。 迭代器(Iterator)即指调用带块方法。实际上,在早期版本的 Ruby 中,使用代码块的方法被称为迭代器,因为它们就是被设计来实现循环迭代的。但是在Ruby发展过程中,代码块的用途在后来已经得到了很大的增强,从最初的循环抽象到任何事情。可以将那些进行迭代操作的方法叫做迭代器,但如果将所有带块方法的调用过程都看作迭带器的话,并不合适而且概念上会引起混乱   §5.2.2 使用迭代器 #一个使用迭代器的简单例子,数组中每一个元素作为参数执行其后的代码块 ['This', 'is', 'a', 'dog'].each do |entry| print entry, ' ' end 执行结果为: This is a dog #另一个使用迭代器的例子,代码块可以访问其外的数据 factorial = 1 1.upto(10) do |i| factorial*= i end puts factorial 执行结果为: 3628800 #代码块的返回值可以被调用者使用 b = [1, 2, 3, 4, 5].map do |entry| entry * entry end print b.inspect 执行结果为: [1, 4, 9, 16, 25] #代码块也可以使用一个以上的参数 result = (0..100).inject(0) do |sum, i| sum + i end print result 执行结果为: 5050 §5.2.3 yield 在方法中可以使用yield来执行代码块的内容,就好像传入的代码块是这个方法的一部分一样。每当碰到一个yield调用,代码块的内容就会被执行一次。当代码块执行结束后,程序会回到yield的那一行继续向下执行。 def twoTimes yield yield end twoTimes { puts "Hello World!" } 执行结果为: Hello World! Hello World! 你可以使用yield操作传参数给一个代码块,并且从代码块取回返回值。 def fibonacii(max) f1, f2 = 1, 1 while f1 <= max yield f1 f1, f2 = f2, f1+f2 end end fibonacii(1000) { |f| print f, " " } 执行结果为: 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 在这个例子中,yield接收一个参数,这个参数将会在执行的时候传递给指定的代码块。在代码块中,接收的参数使用两个竖线括起来放在代码块的头部。yield操作也可以有返回值,yield操作的返回值就是代码块中最后一个表达式的值。 §5.2.4 编写自己的迭代器 def factorial(count, &block) value = 1 1.upto(count) do |i| value = value * i block.call(i, value) end end factorial(5) do |i, sum| puts "factorial(#{i}) = #{sum}" end 执行结果为: factorial(1) = 1 factorial(2) = 2 factorial(3) = 6 factorial(4) = 24 factorial(5) = 120 也可以将传入的代码块保存以供以后使用: class Mathematics def initialize(&block) @block = block end def factorial(max) value = 1 1.upto(max) do |i| value = value * i @block.call(value) end end end the_value = Mathematics.new do |count| puts "Current value is #{count}" end the_value.factorial(5) 执行结果为: Current value is 1 Current value is 2 Current value is 6 Current value is 24 Current value is 120 第六章 表达式 Ruby语言的一切都有返回值,这是Ruby语言和其他程序设计语言的一个显著不同。 006:0> a = b = c = 0 => 0 007:0> print "\n" => nil 同样,if和case语句也有返回值,if和case语句的返回值就是if和case中最后一个执行语句的值。 014:0> if( 1+1 == 2) 015:1> "Like in school." 016:1> else 017:1* "What a surprise!" 018:1> end => "Like in school." §6.1 运算符 和其他程序设计语言一样,Ruby中含有丰富的运算符。但是在Ruby中,大多数运算符实际上是方法调用。例如 a+b,其实真实执行的是 a.+(b),调用a对象的+方法,b作为这个方法的参数。这样带来了相当的灵活性,你可以改变原有运算符的语义从而赋予它新的含义。 以下代码仅仅作为一个例子重写Fixnum类的 + 方法,赋予两个定长整数相加新的含义。 001:0> class Fixnum 002:1> alias the_plus + 003:1* def +(integer) 004:2> the_plus(integer) * 2 005:2> end 006:1> end => nil 007:0> 1+1 => 4 032:0> 2+2 => 8 132:0> 2+5 => 14 对于运算符(+ - * / % ** & | ^ << >> && ||),Ruby有相应形式的赋值运算符缩写形式+=, -=等。 运算符优先级: :: [] +(一元) -(一元) ! ~ * / % + - << >> & | ^ > >= < <= <=> == === != =~ !~ && || .. … ?: = += -= *= /=(所有的赋值运算符缩写) not and or 以下运算符不能作为方法调用,也就是说不能改变以下运算符的含义: … ! not && And || Or :: = += -= *= /=(所有的赋值运算符缩写) ?: §6.2 命令替换 在Shell中,可以使用反引号(`)执行命令替换。 `date` =〉Mon Nov 27 11:07:22 CST 2006 `pwd` =〉/usr/include Ruby也有这个功能。在Ruby中,可以使用反引号或%x来执行命令替换。命令替换表达式的返回值就是命令执行的输出结果。命令执行的返回值存储在全局变量$?中。 2134:0> %x{echo "Hello World!"} => "\"Hello World!\"\n" 反引号的默认行为是执行命令替换,同样,我们也可以重写它,赋予它新的含义。 alias old_backquote ` def `(cmd) result = old_backquote(cmd) if $? != 0 fail "Command #{cmd} failed: #$?" else puts "Command #{cmd} success." end result end print `date` print `data` 执行结果为: Command date success. Mon Jan 15 21:48:16 CST 2007 Command uname success. Linux §6.3 赋值运算符 to-do 定义。 赋值运算的返回值就是左值的值,所以可以进行链式赋值。 001:0> a = b = c = 5 => 5 002:0> a = ( b = 1 + 2 ) + 5 => 8 Ruby的基本赋值有两种形式,一种左边是一个对象或变量,这时把右边的值或变量的引用赋予左边。这种赋值运算由语言本身提供。 003:0> str = "This is a dog." => "This is a dog." 004:0> num = 100 => 100 另一种形式的赋值运算左边是一个类的实例的某一属性,这时候是执行这个类的方法,方法名称为“属性=”。方法的返回值就是右值的值,你可以重写这个方法从而赋予它新的含义。 001:0> class Test 002:1> def num=(num) 003:2> @num = num 004:2> end 005:1> end => nil 006:0> t = Test.new => # 007:0> t.num = 10 => 10 §6.4 并行赋值 Ruby中另一个有趣的地方是支持并行赋值。例如,交换两个变量a,b的值可以写为: a,b = b,a Ruby会先从左到右依次计算 = 右边的表达式,然后再执行赋值的动作。 008:0> x = 0 => 0 009:0> a,b,c = x, x+=1, x+=2 => [0, 1, 3] 如果左边的变量比右边的多,那么多余的变量会被赋为nil. 001:0> x, y, z = 1, 2 => [1, 2] 002:0> print z nil=> nil 如果右边的变量或值比左边的多,那么多余的会被忽略。 001:0> x, y = 1, 2, 3 # 3将被忽略 => [1, 2, 3] 也可以在数组赋值时使用并行赋值。 001:0> a = [1, 2, 3, 4, 5] => [1, 2, 3, 4, 5] 002:0> x, y = a => [1, 2, 3, 4, 5] 003:0> puts x, y 1 2 => nil 在对数组进行并行赋值时可以使用*,*出现在左边最后一个变量时,表示将数组中所有剩余的值赋给这个变量。 001:0> a = [1, 2, 3, 4, 5] => [1, 2, 3, 4, 5] 002:0> x,*y = a => [1, 2, 3, 4, 5] 003:0> puts x 1 => nil 004:0> puts y 2 3 4 5 => nil *出现在右边最后一个变量时,和左边类似。 a = [1, 2, 3, 4] b, c = 9, a =〉b == 9, c == [1, 2, 3, 4] b, c = 9, *a =〉b == 9, c == 1 b, *c = 9, a =〉b == 9, c == [[1, 2, 3, 4]] b, *c = 9, *a =〉b == 9, c == [1, 2, 3, 4] §6.5 嵌套赋值 在赋值中,左边的变量可以使用括号括起来。这样括号内的变量被视作位于一个层次。 b, (c, d), e = 1,2,3,4 =〉b == 1, c == 2, d == nil, e == 3 b, (c, d), e = [1,2,3,4] =〉b == 1, c == 2, d == nil, e == 3 b, (c, d), e = 1,[2,3],4 =〉b == 1, c == 2, d == 3, e == 4 b, (c, d), e = 1,[2,3,4],5 =〉b == 1, c == 2, d == 3, e == 5 b, (c,*d), e = 1,[2,3,4],5 =〉b == 1, c == 2, d == [3, 4], e == 5 §6.6 其他赋值 Ruby支持自加(+=)和自减运算符。和C/C++/Java一样,a = a + 2可以写成a+=2。其它类似的运算符还有%= ~= /= = += |= &= >>= <<= *= &&= ||= **=。 我们经常可以遇到类似这样的语句words[key] ||= [],他与words[key] = words[key] || []等价,意思是如果Hash表words[key]的值为空时,对words[key]赋值为一个新建的空数组,否则不变。 相应的,对于 num = 1 if num.nil? num = 1 unless num Ruby中习惯写为 num ||= 1,这样代码更简洁。 §6.7 条件运算 布尔运算符 在Ruby中定义nil和false为假,其他值为真。注意,和C/C++不同的是0并不被解释为假,空字符串也一样。 Ruby支持常见的布尔运算符,例如 and和 &&,而且还引入一个新的布尔运算符‘defined?’。 和其他程序设计语言一样,and 和 && 代表与关系。 or 和 || 代表或关系。 Not 和 ! 代表非关系。 如果参数没有定义 defined? 返回nil,否则返回一个描述串用来描述参数信息。 013:0> defined? 1 => "expression" 014:0> defined? dummy => nil 015:0> defined? printf => "method" 016:0> defined? String => "constant" 017:0> defined? $_ => "global-variable" 018:0> defined? Math::PI => "constant" 019:0> defined? a = 0 => "assignment" 020:0> defined? 30.abs => "method" 条件运算符 Ruby支持一系列条件运算符,==, ===, <=>,=~,eql? 等等,equal?。除了<=> 其他的都是类方法。 == 和 =~ 有否定形式 != 和 !~。 If和unless Ruby中的if和其他程序设计语言中的if大同小异: if x == 5 then print “The value of x is 5.” elsif x == 0 then print “The value of x is 0.” else print “The value of x is ”, x end 也可以省略then: if x == 5 print “The value of x is 5.” elsif x == 0 print “The value of x is 0.” else print “The value of x is ”, x end 如果为了代码紧凑而将代码放到同一行则不能省略then: if x == 5 then print “The value of x is 5.” elsif x == 0 then print “The value of x is 0.” else print “The value of x is ”, x end 也可以使用冒号分隔,这样代码更紧凑 : if x == 5: print “The value of x is 5.” elsif x == 0: print “The value of x is 0.” else print “The value of x is ”, x end 正如我们前面所说的,if是一个表达式,它有自己的返回值。你可以忽略这个返回值,但是他确实存在。If语句的返回值就是最后执行的语句的值。 x = 10 str = if x == 5: "x==5" elsif x == 0: "x==0" else "x==?" end 执行后str的内容为:x== ?  Ruby也支持if的否定形式unless,unless的语法和if没有差别。 unless x != 5 print “The value of x is 5.” else print “The value of x is not 5.” end Ruby也支持C/C++的 ?:运算符。 str = x == 5? "x==5":"x==?" Ruby也从Perl那里继承了一个很好的语法,你可以将条件写到表达式的后边。 puts "a = #{a}" if debug print total unless total.zero? §6.8 case表达式 Ruby中的case语句非常强大,首先我们来看一个基本用法: grade = case when point >= 85: 'A' when point >= 70 && point < 80: 'B' when point >= 60 && point < 70: 'C' when point < 60: 'D' else 'E' end 这里case语句的作用和if表达式类似,case语句的返回值就是最后一个执行的表达式的值。和if语句类似,如果写在同一行的话需要加then或冒号。 另一种也是最常用的形式是在case后列出目标,然后每个语句依次和目标比较: case input_line when "debug" print "We are in debug mode." when /p\s+(\w+)/ dump_variable($1) when "quit", "exit" exit else print "Illegal command: #{input_line}" end 另一个例子: Season = case month when 3..5 : "Spring" when 6..8 : "Summer" when 9..11: "Autumn" when 12..2: "Winter" else "Error" end Ruby提供了一个运算符===,只要一个类提供了===方法,那这个类的对象就可以出现在case语句中。例如对于正则表达式定义了===为模式匹配。 Ruby中,所有类的基类是Class类,所有类实例都是Class类的实例(to-do)。它定义===的含义为为参数所提供是否为实例的类或父类。 case shape when Square, Rectangle # ... when Circle # ... when Triangle # ... else # ... end §6.9 循环 §6.9.1 Loop Loop循环始终执行其后的方法块,直到break退出。 x = 0 loop do x += 1 if x <= 5: print x, " " else break end end 执行结果为:1 2 3 4 5 。 §6.9.2 While 当条件为真时While循环继续,条件为假时退出循环。 x = 0 while x < 10 x += 1 end §6.9.3 Until Until和While厢房当条件为假时While循环继续,条件为真时退出循环。 x = 0 until x == 9 x += 1 end §6.9.4 Iterator 和C/C++/Java不同,Ruby语言并不支持C/C++/Java中的For循环,但Ruby通过迭代器来提供更为强大的功能。先看一个例子: 4.times do puts "Hello!" end 执行结果为: Hello! Hello! Hello! Hello! 除了times方法之外,整数还提供upto和downto两个方法,看以下例子, 0.upto(9) do |i| print i, " " end 执行结果为0 1 2 3 4 5 6 7 8 9 。 也可以使用Step方法,step第二个参数表示步长: 0.step(10, 2) do |i| print i, " " end 执行结果为:0 2 4 6 8 10 。 许多容器类,例如数组,提供了each方法依次遍历容器中的数据: [1, 2, 3, 4, 5].each { |i| print i, " "} 执行结果为:1 2 3 4 5 。 如果一个类支持each方法,那么就可以使用Enumerable模块中的一些方法。 ["apple", "orange", "banana", "watermelon"].grep(/an/) do |fruit| puts fruit end 执行结果为: orange banana §6.9.5 For..In 如果一个类提供了each方法,那么相应的,这个类的对象可以使用For..in循环。例如Array类和Range类都有each方法: for fruit in ["apple", "orange", "banana", "watermelon"] print fruit, " " end 执行结果为:apple orange banana watermelon 。 for i in 1..9 print i, " " end 执行结果为:1 2 3 4 5 6 7 8 9 。 §6.9.6 Break,Redo,Next Break,Redo和Next用来改变循环的流程。 §6.9.6.1 break Break用来退出当前循环: times = 0 loop do times += 1 print "hello #{times}\n" break if times > 2 end 执行结果为: hello 1 hello 2 hello 3 与C/C++不同,如果循环有多重的话,break将退出最内层的循环。 outer = 0 loop do outer += 1 inner = 0 loop do inner += 1 print "Inner #{inner}\n" break if inner > 1 end print "Outer #{outer}\n" break if outer > 1 end 执行结果为: Inner 1 Inner 2 Outer 1 Inner 1 Inner 2 Outer 2 另一个与C/C++语言不同的地方是break只能从循环中退出,而不能从case中退出。 §6.9.6.2 redo redo语句重新执行当前这一次循环。 count = 0 for i in 1..3 print "hello #{i}\n" break if count == 1 if i > 1 count += 1 redo end end 执行结果为: hello 1 hello 2 hello 2 上面的例子中,使用redo后,循环变量i的值还是2,可见redo语句重新执行了这次循环。 和break语句类似,redo语句只对最内层的循环起作用。 3.times do count = 0 for i in 1..3 print "hello #{i}\n" break if count == 1 if i > 1 count += 1 redo end end end 执行结果为: hello 1 hello 2 hello 2 hello 1 hello 2 hello 2 hello 1 hello 2 hello 2 §6.9.6.3 next Next类似C/C++中的continue语句,跳转到当前循环的头部,执行下一次循环。 loop do times += 1 next if times == 2 print "hello #{times}\n" break if times > 3 end 执行结果为: hello 1 hello 3 hello 4 与break,redo类似,如果循环有多重,那么next只对最内侧的循环起作用。 outer = 0 loop do outer += 1 inner = 0 loop do inner += 1 next if inner == 1 print "Inner #{inner}\n" break if inner > 1 end print "Outer #{outer}\n" break if outer > 1 end 执行结果为: Inner 2 Outer 1 Inner 2 Outer 2 §6.9.7 Retry 上一节我们看到,可以使用redo重新执行当前这一次的循环,有时候,我们也需要重新执行整个循环而不是仅仅执行当前这次,这时候我们可以用时retry。在迭代、块或for语句中使用retry,意味着重启迭代器。同时迭代器的参数也将被重新计算。 一个示例如下, for i in 1..5 retry if some_condition # 从 i == 1 开始重新执行 end 看一个完整可执行的例子: count = 0 for i in 1..3 print "hello #{i}\n" break if count == 1 if i > 1 count += 1 redo end end 执行结果为: hello 1 hello 2 hello 1 hello 2 hello 1 第七章 方法 Ruby中的方法使用关键字def来定义。方法名应该以小写字母开始,如果你使用大写字母开始,Ruby解释器会认为它是一个常量,这样可能会带来名称解析错误。 在定义方法时可以使用圆括号也可以不用。 def method1 puts "Hello World!" end def method2 arg1, arg2 puts "The arguments is: #{arg1}, #{arg2}" end 一般的习惯是如果方法含有参数,那么就使用圆括号将参数括起来,否则的不要需要圆括号。 def method2(arg1, arg2) puts "The arguments is: #{arg1}, #{arg2}" end 定义方法时可给方法默认参数,注意默认参数必须位于方法的尾部。 def method3(arg1=5, arg2=9) puts "The arguments is: #{arg1}, #{arg2}" end def method3(arg1=5, arg2) #错误 方法的返回值为方法最后一个表达式的值,或者由return语句的返回的值。 和C/C++不同,Ruby中的方法总是从属于某一个对象。Ruby中没有全局函数。虽然Ruby中可以象全局函数一样定义和使用方法,但是你应当明白,Ruby中的方法总是会从属于某一个对象。 看到这里,细心的读者会提出一个问题,如果在顶层定义一个方法,那么这个方法属于谁? def meth end 在本书中,我们多次说,Ruby是一种面向对象的语言,在Ruby中的一切都是对象。那么meth方法从属于什么对象呢?我们看一个例子: public #为什么使用public看后边的解释 def meth puts self.class end puts self.class self.meth 执行结果为: Object Object 在顶层,当我们定义方法时,将自动将我们定义的方法作为Object类的私有实例方法。所以这些方法可以在任何地方调用。所以我们可以在任何地方使用Object类的任何方法和Object类所包含的模块中的任何方法,例如Kernel模块中的方法在任何地方可以随意使用。 上面的例子中,meth将作为Object类的私有方法,所以我们使用public改变它的存取属性,否则self.meth将会产生无法访问私有对象的错误。 在Ruby语言中,方法是存放到Hash表中,而键值就是方法名。定义一个方法就是在Hash表中添加一项的过程,所以,后定义的同名方法就会替换掉之前定义的方法。 def meth puts "first" end meth def meth puts "second" end meth 执行结果为: first second Ruby语言支持缺省参数,但不支持方法重载。方法重载会加重解释器语法解析的复杂度,影响执行速度。C++的选择是二者都支持,而Java的选择刚好与Ruby相反,即支持方法重载,而不支持缺省参数。 方法名可以以问号“?”,叹号“!”,等于号“=”结尾。这样的方法有约定的含义。以问号结尾的方法返回布尔值,以叹号结尾的方法表示会改变调用者的内部数据,以等于号结尾的方法表示可以作为左值。问号“?”,叹号“!”,等于号“=”是方法名的一部分。 (to-do 例子) 当一个方法被调用时,运行时环境按照如下顺序搜索: 1、 在当前对象的特殊方法中搜索 2、 在当前对象类中定义的实例方法中搜索 3、 在当前对象所包含的模块中搜索 4、 在当前对象所在类的父类中搜索 5、 在当前对象所在类的父类所包含的模块中搜索 6、 继续4和5的过程,直到顶层Object类 §7.1 运算符重定义 Ruby支持重定义运算符,下表列出了Ruby支持重定义的运算符和他们默认的含义: 运算符 默认含义 [] []= 元素索引和赋值 ** 幂运算 ! ~ 否定,求补 + - 正,负,注意他们的方法名为+@,-@以便和加减运算区分 * / % 乘法,除法,取余运算 + - 加减运算 >> << 左移,右移 & | ^ 按位与,按位或,按位异或 <= < > >= 比较运算 === Case表达式中的相等测试 <=> 范围测试 == != 正则表达式匹配相等和不等测试 =~ !~ 相等和不等测试 注意,!=和!~ 作为==和=~的否定形式,并没有相应的方法。他们经过语法分析后将被转化为肯定形式调用。例如: a != b实际是调用 !(a==b),同样,a !~ b将被转化为 !(a=~b)。 §7.2 变长参数 Ruby语言支持定义方法时,方法的参数数目不确定。只要给参数前加星号就表示参数数目不定。所有的不定数目的参数被作为一个数组。 def varadd(*num) sum = 0 num.each { |i| sum+=i } puts sum end varadd() varadd(1,2,3) varadd(1,2,3,4,5,6) 执行结果为: 0 6 21 你也可以反过来使用星号,在调用方法时,如果一个类型为数组的参数前有星号作为前缀,那么这个数组将被展开。 def meth1(arg1, arg2, arg3, arg4, arg5) print "Parameters:#{arg1},#{arg2},#{arg3},#{arg4},#{arg5} \n" end meth1(1, 2, 3, 4, 5) meth1(1, 2, 3, *['4', '5']) meth1(*(10..14).to_a) 执行结果为: Parameters:1,2,3,4,5 Parameters:1,2,3,4,5 Parameters:10,11,12,13,14 §7.3 块调用 当调用一个方法时,可以在方法后连接一个块。在方法内可以使用yield执行连接的块。 def test_block1(arg1) print arg1,"\n" yield end test_block1("Test") { print "Hello World!" } 执行结果为: Test Hello World! Kernel模块有一个block_given?的方法,可以判断方法后是否有块连接。 def test_block2(arg1) if block_given? print yield(arg1), "\n" else print arg1, "\n" end end test_block2("no block") test_block2("no block") {|s| s.sub(/no /, '') } 执行结果为: no block block 如果方法的最后一个参数以&开始,表示方法后连接的块会被转化为Proc对象,然后传递给这个参数。 def meth1(count, &block) value = 1 1.upto(count) do | i | value = value * i block.call(i, value) end end meth1(4) do | i, f_i | puts "meth1(#{i}) = #{f_i}" end 执行结果为: meth1(1) = 1 meth1(2) = 2 meth1(3) = 6 meth1(4) = 24 §7.4 方法返回值 每一个方法都有返回值,当然,你不一定要使用这个返回值。方法的返回值就是在调用方法时方法内最后一个表达式执行的结果。你也可以使用return语句,这样方法的返回值就是return语句的参数。Ruby语言习惯省略return语句,能少写的尽量少写,这也是Ruby的哲学J。 def meth1() "meth1" end def meth2(arg) if arg == 0 "Zero" else if arg > 0 "Positive" else "Negative" end end 使用return语句时返回值可以是多个,这时候返回值会被转化为一个数组,你可以用多重赋值的形式来使用这个返回值。 def meth1 return "meth1", 6 end a, b = meth1 第八章 模块 模块提供了一种组织常量,类和方法的手段。你可以使用模块来提供一个名字空间以避免名字冲突,你也可以使用模块来提供mixin的功能。 §8.1 名字空间 当程序代码越来越多,工程越来越大,开发者不可避免的会将一些常用的代码以库或别的形式重用。一般情况下,我们可以用类来组织代码,但有时候使用类组织代码并不是十分合适。这样在一个大工程中,就有可能发生名字冲突。 例如,开发者A在文件a.rb中写了如下代码,用来输出自己代码文件的版本信息, def print_version # … end 同一个项目中的另一个开发者B在文件b.rb中用同样的方法来实现同样的功能, def print_version # … end 第三个开发者C需要使用a.rb和b.rb中的一些方法,这样,当他使用print_version方法时,就产生了名字冲突,到底他调用的是哪一个文件中的print_version方法呢? 我们可以使用模块机制来解决这样的名字冲突。定义一个模块相当于定义了一个名字空间,名字空间内的元素在全局空间并不直接可见。 开发者A定义模块A_FILE, module A_FILE def print_version # … end end 开发者B定义模块A_FILE, module B_FILE def print_version # … end end 这样对于开发者C,可以这样使用print_version require ‘A’ require ‘B’ A. print_version B. print_version 类和模块的区别是,模块不能生成实例,而类不能被include。 §8.2 mixin Mix-in的意思是混合插入、糅合,就像在冰淇淋中混合多种配料可以做成美味的混合冰淇淋一样,在类中混合插入各种模块就可以添加相应的功能。模块还有另一个重要的作用,可以使用模块来实现多继承,可以在类中包含模块,这样模块中的所有方法和类中其他方法一样可以使用。 Matz坚信滥用多重继承会导致继承关系的混乱,因此Ruby中不允许使用多重继承。同时为充分发挥继承功能的优势,Ruby支持两种继承关系:1.使用is-a语句的继承;2.使用Mix-in来共享并继承模块中的功能。 module Debug Define print_info print "Class: #{self.class.name} Object ID: #{self.id}" end end class A include Debug #... end class B include Debug #... end obj1 = A.new obj2 = B.new obj1.print_info obj2.print_info 通过这样的手段我们可以实现多继承的功能,这样的模块我们称为mixin。 在Ruby中,Object,Class和Module是三个特殊的类。 Object Module Class Class是一个Module,而Module是一个Object,所以说Class是一个Object,因此,所有的数据都是Object。 §8.3 使用mixin §8.3.1 Comparable Comparable mixin提供了比较的功能。要使用Comparable mixin必须提供<=>方法,<=>的返回值为-1,0,+1用来表示元素之间的小于,等于,大于的关系。 class Person include Comparable attr :age def <=>(aPerson) @age <=> aPerson.age end def initialize(name, gender, age) @name = name @gender = gender @age = age end end aPerson = Person.new("Tom", "male", 18) bPerson = Person.new("Mike", "male", 10) cPerson = Person.new("Henry", "male", 40) puts aPerson > bPerson puts aPerson < bPerson puts aPerson >= bPerson puts aPerson <= bPerson puts aPerson == bPerson puts aPerson.between?(bPerson, cPerson) 执行结果为: true false true false false true §8.3.2 Enumerable Enumerable mixin提供了遍历,查找和排序的功能。 要使用Enumerable mixin必须提供each方法,标准做法是在each方法内对每一个元素使用yield操作。如果使用了Enumerable mixin中的max,min,或sort,那么还必须提供<=>方法,用来实现元素之间的比较关系。 以下是一个使用Enumerable mixin的例子,IntegerFinder是一个查找字符串中整数的类。 class IntegerFinder include Enumerable def initialize(aString) @string = aString end def each @string.scan(/[1-9]\d*/) { |integer| yield integer } end end aDigitFinder = IntegerFinder.new("This is 123, 234, 98 and 10") aDigitFinder.collect {|i| print i, " "} aArray = aDigitFinder.find_all {|i| i.to_i > 50 } puts "\n", aArray.to_s 执行结果为: 123 234 98 10 12323498 Enumerable mixin中含有许多与集合遍历查找相关的方法,许多标准类也使用了Enumerable mixin,借助Enumerable mixin中的方法可以方便的实现一些强大的功能,请看以下一些例子: #察看数组中的所有单词的长度是否大于4 %w{ ant bear cat}.all? {|word| word.length >= 4} #=> false #返回range中所有不符合条件的元素 (1..10).reject {|i| i % 3 == 0 } #=> [1, 2, 4, 5, 7, 8, 10] #求5到10的和 #inject方法第一次会把Range中的前两个元素作为参数传递给sum和n, #以后每次会把sum设置为块计算后的返回值。 (5..10).inject {|sum, n| sum + n } #=> 45 # 找出数组中最长的单词 longest = %w{ cat sheep bear }.inject do |memo,word| memo.length > word.length ? memo : word end longest #=> "sheep" §8.3.3 Singleton 在设计模式中,Singleton技术通常称为单件,是一种常见的设计技术,它保证在系统的某个类在任一时刻最多只有一个实例在运行。你可以参见设计模式这本书获得有关单件技术更详细的信息。在Ruby中,使用Singleton Mix-in,你可以很容易的实现单件类。 在单件类中不能使用new方法,因为在单件类中new方法的属性是私有的。需要使用instance方法得到类的实例对象。 require 'singleton' class SingletonClassTest attr_accessor :data include Singleton end # a = SingletonClassTest.new #错误,new方法为私有方法 a = SingletonClassTest.instance b = SingletonClassTest.instance puts a.inspect puts b.inspect a.data = 8 puts b.data 执行结果为: # # 8 可以看到,a和b其实指向同一个对象。 §8.4 Require, load和include 在Ruby中,可以使用load和require来包含另一个文件。每次运行到load时,load后的文件会被载入并执行。 4.times do |i| File.open("temp.rb","w") do |f| f.puts "def test" f.puts "#{i}" f.puts "end" end load "temp.rb" puts test end 执行结果为: 0 1 2 3 在上面的小程序里load "temp.rb"执行了4次,每一次temp.rb文件都不同,所以test方法执行后的结果也不同。 使用Load方法的这种特性可以实现一些强大的功能,例如: l 可以用来处理配置文件,在程序运行期间配置文件可以被动态改变。 l 可以用来实现程序的无缝升级,在升级时你不需要重启程序,只要将所需要的代码重新load。 Require和load不同,它只加载文件一次,即在第一次执行到require时载入,以后碰到require同一文件时自动忽略。已经被加载的文件保存在$”中。另外,require还可以用来加载二进制格式的库。Require可以使用绝对路径或相对路径,如果使用了相对路径,那么系统会在$:变量包含的目录中搜寻。Require和load的另一个不同是当包含文件是Ruby代码时,require可以不加后缀名。Require将当前所有加载的文件保存在$"变量中。 注意,在当前版本中,$”是一个数组,保存着使用require已经加载过的文件。但如果require使用不同的路径去包含同一个文件,这个文件就有可能被加载多次。 File.open("temp.rb","w") do |f| f.puts "def test" f.puts "1" f.puts "end" end require "temp" puts test File.open("temp.rb","w") do |f| f.puts "def test" f.puts "2" f.puts "end" end require "./temp" puts test 执行结果为: 1 2 这样就违背了require只加载一次的初衷,一些人认为这是一个bug,这个问题在Ruby的后续版本中可能被修复。所以,不要使用不同的路径去加载同一个文件。 require, load,include都是Kernel模块中的方法,他们的区别如下: l require,load用于包含文件,include则用于包含的模块。 l require加载一次,load可加载多次。 l require加载Ruby代码文件时可以不加后缀名,load加载代码文件时必须加后缀名。 l require一般情况下用于加载库文件,而load用于加载配置文件。 第九章 异常 异常(Exception)顾名思义,指的是在程序处理过程中遇到不能处理的非预期条件,它会打乱正常的执行流程。 许多种类的错误会触发异常,典型的异常例如整数除零,Ruby提供了一套处理异常的机制。如果错误发生,方法将创建一个异常对象并把它抛出到运行时系统。创建一个异常对象并将其交给运行时系统叫做抛出异常。异常对象包含了异常的信息,包括异常类型,异常发生时的堆栈状态等。运行时系统负责找到一些代码处理这个错误。 当某个方法抛出一个异常后,运行时系统需要找到一些代码来处理这个异常。运行时系统从发生异常的代码处开始,依次查找调用堆栈,直到找到相应的代码处理这个异常,然后继续执行其下的代码。注意处理异常过后程序不会返回到异常抛出处继续往下执行。 和传统错误管理技术相比,使用异常来管理错误具有如下优点: l 将正常处理逻辑与错误处理逻辑分开 l 错误沿着调用堆栈向上传递,异常处理的代码简洁容易理解 l 错误具有相应得类型,可以使用类型来区分不同的错误 §9.1 异常处理 Ruby处理异常的语法如下: begin # 正常处理流程代码 rescue …… # 处理异常 ensure # 这里的代码总会被执行 End 我们看一个例子,以下代码产生一个除零错误: begin numerator = 10 denominator = 0 value = numerator / denominator rescue ZeroDivisionError => ex puts "Error, divide zero" ensure puts "Finish the process!" end 执行结果: Error, divide zero Finish the process! 在异常产生后,如果相应的rescue语句被匹配到,那么这个异常对象会被复制到ex中。而ensure后的代码永远都会被执行到。 在Ruby中,异常类的基类是Exception,也就是说所有异常类都是Exception类直接或间接的子类。Ruby预定义了一个层次结构用来处理常见的异常。 (to-do Ruby异常层次图) 在捕捉异常时,可以使用多个rescue语句: begin # 正常处理流程代码 rescue ExceptionClass1 => exception1 # 处理当发生ExceptionClass1类型的异常 rescue ExceptionClass2 => exception2 # 处理当发生ExceptionClas21类型的异常 rescue # 处理其他异常 ensure # 这里的代码总会被执行 end 注意,rescue不加参数默认是匹配StandardError类型的异常。 Ruby使用最先匹配的规则,如果一个异常和rescue语句匹配成功,那么接下来的rescue语句都不会被匹配。在搜寻匹配的时候可以做从子类到父类的转换。 begin number1 = number2 rescue SyntaxError, NameError => ex print "The code doesn't compile: " + ex.class.to_s rescue StandardError => ex print "Error running script: " + ex.class.to_s end 执行结果为: The code doesn't compile: NameError 上面的例子中使用到number2,但是number2并没有被定义,所以产生了一个NameError。第一个rescue语句被匹配到,于是放弃继续搜寻,直接执行相应的rescue语句后的代码。 begin number1 = number2 rescue StandardError => ex print "Error running script: " + ex.class.to_s rescue SyntaxError, NameError => ex print "The code doesn't compile: " + ex.class.to_s end 执行结果为: Error running script: NameError 这个例子和第一个例子区别就是rescue语句的顺序,从例子中可以看到,由于StandardError是NameError和SyntaxError的父类,而按顺序StandardError会先被匹配到,所以会放弃继续搜寻,StandardError下面的代码会被执行。上面的例子中,print "The code doesn't compile: " + ex.class.to_s永远不会被执行到。 当抛出一个异常时,可以使用Ruby内置的异常类,也可以自定义自己的异常类。如果使用自定义的异常类,那么这个异常类应该是StandardError类直接或间接的子类。如果不这么做,那么默认情况下,这个异常不能正确地被捕捉。 在rescue处理逻辑之后可以加一个else子句,只有没有任何异常发生的时候程序才会运行到那里,这种用法比较少见。 begin # 正常处理流程代码 rescue ExceptionClass1 => exception1 # 处理当发生ExceptionClass1类型的异常 rescue ExceptionClass2 => exception2 # 处理当发生ExceptionClas21类型的异常 rescue # 处理其他任何异常 else # 没有任何异常 ensure # 这里的代码总会被执行 end 可以使用raise显式抛出一个异常。raise后可以跟异常说明信息,这种情况下抛出的异常为RuntimeError类型: # 抛出一个RuntimeError类型的异常,异常说明信息"FTP Server down" raise "FTP Server down" 可以使用raise抛出特定类型的异常: # 抛出一个FTPServerError类型的异常,异常说明信息" Server not responding” raise FTPServerError, "Server not responding" 还可以附加第三个参数表示调用栈的信息: # 抛出一个FTPServerError类型的异常,异常说明信息" Server not responding” #第三个参数为Kernel#caller方法的处理结果,用来指定调用栈的层次 raise FTPServerError, "Server not responding", caller Kernel#caller方法可以返回一个数组包含了当前调用栈的信息,方法可以携带参数,表示从调用栈内部开始,跳过堆栈的层次,默认值为1。 def meth1(skip) meth2(skip) end def meth2(skip) meth3(skip) end def meth3(skip) caller(skip) end puts meth1(0) puts meth1(1) puts meth1(2) puts meth1(3) 可以看到,puts meth1(0)的结果为: test.rb:10:in `meth3' test.rb:6:in `meth2' test.rb:2:in `meth1' puts meth1(1)的结果为: test.rb:6:in `meth2' test.rb:2:in `meth1' puts meth1(2)的结果为: test.rb:2:in `meth1' puts meth1(3)的结果为空。 通过caller方法,当抛出异常时,我们可以指定调用栈的层次: def meth1 meth2 end def meth2 meth3 end def meth3 begin raise RuntimeError, "Test Error", caller[1..2] rescue RuntimeError => ex puts ex.backtrace end end meth1 执行结果为: test.rb:2:in `meth1' 上面的例子中,通过调用caller方法得到当前调用栈,然后通过caller[1..2]指定异常发生时传递的调用堆栈信息,最后使用异常对象的backtrace方法来得到调用堆栈。 使用raise不加参数表示重新抛出当前异常,如果当前没有异常,那么将抛出一个RuntimeError类型的异常: begin numerator = 10 denominator = 0 value = numerator / denominator rescue ZeroDivisionError => ex puts "Error, divide zero" raise #重新抛出当前异常 ensure puts "Rethow the error!" end 执行结果: Error, divide zero Finish the process! 如果没有任何相应的rescue语句处理异常,那么这个异常会交给运行时环境。运行时环境会输入异常并中止程序的执行。 另外,全局变量“$!”保存了最近抛出的异常对象,而“$@”保存了最近发生的异常发生异常的调用栈信息。 除了循环以外,还可以在rescue部分中使用retry。这时将从begin表达式开始重新执行。使用retry可以在某处理过程成功之前,一直循环该处理过程。 begin do_something # exception raised rescue # handles error retry # restart from beginning end 若在rescue部分、迭代器块或for语句之外使用retry的话会引发LocalJumpError异常。 §9.2 定义异常类 Exception类提供了堆栈和异常的描述信息,在自定义的异常类中,你可以添加额外的信息。自定义的异常类应该是StandardError类直接或间接的子类。否则这个异常可能不会正确地被捕捉。 看下面的例子,以下代码位于MyExceptionTest.rb中。 class MyException < RuntimeError end begin raise MyException, "MyException Testing..." rescue MyException => ex puts ex.to_s puts ex.backtrace.join("\n") ensure puts "Finish the process!" end 执行结果为: MyException Testing... MyException.rb:5 Finish the process! §9.3 catch和throw 使用raise,rescue用来处理出错的情况,同时也可以使处理流程从很深的嵌套结构中跳出。catch和throw也能使处理流程从很深的嵌套结构中跳出。 Catch定义了一个代码块,并给这个代码块一个名字作为标注。当Ruby遇到一个throw时会展开调用堆栈来查找相应的catch块,当找到时,Ruby在相应得catch处展开堆栈并中止块。 def method1(n) puts n throw :done if n <= 0 method1(n-1) end catch(:done) { method1(3) puts "Can not reach here!" } puts "Reach here!" 执行结果: 3 2 1 0 Reach here! 上面的例子中,我们首先用“catch(:done)”标注了一个块,当执行到“throw :done”时,中止当前“catch(:done)”所标注的块,处理流程继续向下处理。 在catch/throw中,当碰到throw时,Ruby会展开调用堆栈来查找匹配的catch,当找到后,堆栈会被展开,catch块剩余的代码不会被执行。 Ruby中没有goto 语句,但你可以使用catch/throw或异常来实现goto的功能。 第十章 多任务处理 当你要同时处理多件任务时Ruby提供了两种基本方式,一种方式是在一个程序协调需要同时执行的任务,使用多线程处理需要同时执行的操作。另一种方式是将需要同时执行的操作分散到多个程序中使用多进程。 按照教科书上的定义,进程是资源管理的最小单位,线程是程序执行的最小单位。 一般来说,我们把正在计算机中执行的程序叫做“进程”(Process),而所谓“线程”(Thread),是“进程”中某个单一顺序的控制流。 线程之间独立运行,每个线程有它自己的堆栈、自己的程序计数器和自己的局部变量。但是与进程相比,线程之间的隔离程度要小。它们彼此共用变量,也有可能会同时操作一片内存。 一个进程至少需要一个线程作为它的指令执行体,进程管理着资源(比如CPU、内存、文件等等),而将线程分配到某个CPU上执行。一个进程可以拥有多个线程,此时,如果进程运行在多处理器的机器上,它就可以同时使用多个CPU来执行各个线程,达到最大程度的并行,以提高效率;同时,即使是在单CPU的机器上,采用多线程模型来设计程序,可以使设计更简洁、功能更完备,程序的执行效率也更高。例如采用多个线程响应多个输入,而此时多线程模型所实现的功能实际上也可以用多进程模型来实现,而与后者相比,线程的上下文切换开销就要小多了。 在操作系统设计上,从进程演化出线程,最主要的目的就是更好的支持多处理器以及减小上下文切换开销。 §10.1 多线程处理 Ruby的多线程是用户级多线程,这样使得Ruby的多线程移植非常容易,你并不需关心具体的操作系统;这样做也使线程容易控制,程序不容易产生死锁这类严重的线程问题。 但是同时,由于Ruby的多线程并不是真正意义上的操作系统级多线程,不管代码使用了多少个Thread类的实例,都只会在启动解释器这一个进程内执行,由Ruby解释器进行具体的线程切换管理,其效率要低于由操作系统管理线程的效率,且不能使用多个CPU。 在Ruby中同时做多件事最简单的方式就是使用Thread类,Thread类提供了一种高效和轻量级的手段来同时处理多件任务。 Thread类由Ruby解释器具体实现,提供了一种同时处理多个任务的方法, Thread类实现的并不是操作系统级多线程。 Ruby多线程的优点和缺点同样明显,缺点是效率不如操作系统级多线程,不能使用多个CPU,但其优点也很明显,即可移植性很高。这就需要设计人员综合考虑。 §10.1.1 线程创建 我们可以使用Thread.new方法去创建一个线程,可以随后代码块中列出线程执行的代码: x = Thread.new { sleep 0.1; print "x"; print "y"; print "z" } a = Thread.new { print "a"; print "b"; sleep 0.2; print "c" } 执行结果为: ab 上面的示例程序中使用Thread.new创建了二个线程,线程随即开始运行。但是运行结果很奇怪,为什么程序运行结果是“ab”呢?我们预期的执行结果应该是 “abxyzc”。 当Ruby程序执行完毕的时候,他会杀掉所有的线程,不管其它的线程的运行状态如何。如果没有使用join方法,那么主程序执行完毕后会把所有没有执行完毕的线程杀掉。 上面的实例程序中,当主程序运行完毕时,两个线程都没有运行结束就被中止掉了。我们可以使用join方法来让主程序等待某个特定线程结束,对每一个线程使用join方法,可以确保在程序结束前所有的线程可以运行完毕。 x = Thread.new { sleep 0.1; print "x"; print "y"; print "z" } a = Thread.new { print "a"; print "b"; sleep 0.2; print "c" } x.join a.join 我们使用Thread.new方法创建两个新的线程并开始运行, 然后使用join方法等待线程结束。执行结果为: abxyzc 可以看到通过使用join方法等待线程结束,程序运行结果和我们预期结果相符。 另一个例子: x = Thread.new { sleep 0.1; print "x"; print "y"; print "z" } a = Thread.new { sleep 0.1; print "a"; print "b"; sleep 0.2; print "c" } 执行没有任何输出,因为主程序执行完毕杀死两个线程的时候这两个线程没有运行到输出语句。 也可以给join方法添加时间用来指明最大等待时间。如果超时join返回nil。 x = Thread.new { sleep 0.1; print "x"; print "y"; print "z" } a = Thread.new { sleep 0.1; print "a"; print "b"; sleep 10; print "c" } x.join(5) a.join(5) 执行结果为: abxyz 上面的例子中对于每一个线程的最大等待时间是5秒,由于a线程需要执行10秒以上,所以a线程没有运行完毕程序就将返回。 §10.1.2 线程操作 可以使用Thread.current方法来访问当前线程,也可以使用Thread.list方法列出所有线程;可以使用Thread#status和Thread#alive?方法得到线程的状态;也可以使用Thread#priority方法得到线程的优先级和使用Thread#priority=方法来调整线程的优先级。 test_thread = Thread.new {} test_thread.join Thread.current.alive? => true Test_thread.alive? => false 上面的例子中,使用alive?方法得线程的状态,由于主线程并没有执行完毕所以状态为true,由于Test_thread已经执行完毕所以返回false。 Thread.new { sleep(200) } Thread.new { 1000000.times {|i| i*i } } Thread.new { Thread.stop } Thread.list.each {|t| p t} 执行结果为: # # # # 上面的例子中创建了三个线程,然后列出所有的线程,可以看到,列出的线程数目是四个而不是三个,因为除了创建的三个线程之外还有主线程。 number1 = 0 number2 = 0 thr1 = Thread.new { loop { number1 += 1 } } print "th1:", thr1.priority, "\n" thr1.priority = -1 thr2 = Thread.new { loop { number2 += 1 } } print "th2:", thr2.priority, "\n" thr2.priority = -2 sleep 1 Thread.critical = 1 puts number1 puts number2 输出结果为: th1:0 th2:0 174100 98 上面的例子中使用priority方法得到线程的优先级,使用priority=方法设置线程的优先级。县城的优先级默认为0。可以看到同样的代码,优先级较高的执行的要快。 我们可以使用critical方法和critical=方法得到和设置全局线程临界区(Thread.critical)的值,如果这个值为true,那么所有存在的线程将停止运行。 一个Thread可以访问自己作用域内的所有数据,但如果有需要在某个线程内访问其他线程的数据应该怎么做呢?Thread类提供了线程数据互相访问的方法,你可以简单的把一个线程作为一个Hash表,可以在任何线程内使用[]=写入数据,使用[]读出数据。 athr = Thread.new { Thread.current["name"] = "Thread A"; Thread.stop } bthr = Thread.new { Thread.current["name"] = "Thread B"; Thread.stop } cthr = Thread.new { Thread.current["name"] = "Thread C"; Thread.stop } Thread.list.each {|x| puts "#{x.inspect}: #{x["name"]}" } 可以看到,把线程作为一个Hash表,使用[]和[]=方法,我们实现了线程之间的数据共享。 §10.1.3 线程和异常 当一个线程运行时如果发生异常会怎样?这和abort_on_exception标志和解释器的debug标志有关,abort_on_exception默认被设置为false,这时候发生异常的那个线程会被杀死,其他线程继续运行。只有当你对发生异常的线程使用join操作时你才会察觉到有异常发生。 a = Thread.new { raise "Error A" } b = Thread.new { puts "Thead B" } c = Thread.new { puts "Thead C" } b.join c.join 执行结果为: Thead B Thead C a = Thread.new { raise "Error A" } b = Thread.new { puts "Thead B" } c = Thread.new { puts "Thead C" } a.join b.join c.join 执行结果为: Thead B Thead C test.rb:1: Error A (RuntimeError) from test.rb:4:in `join' from test.rb:4 我们也可以在使用join时捕获这个异常: threads = [] threads[0] = Thread.new { raise "Error A" } threads[1] = Thread.new { puts "Thead B" } threads[2] = Thread.new { puts "Thead C" } threads.each do |t| begin t.join rescue RuntimeError => e puts "#{e.message}" end end 执行结果为: Thead B Thead C Error A 然而设置了abort_on_exception标志或者使用-d打开了debug开关,如果抛出了未被捕获的异常,程序将结束,所有的线程都将被终止。 Thread.abort_on_exception = true threads = [] threads[0] = Thread.new { raise "Error A" } threads[1] = Thread.new { puts "Thead B" } threads[2] = Thread.new { puts "Thead C" } threads.each do |t| begin t.join rescue RuntimeError => e puts "#{e.message}" end end 执行结果为: test.rb:3: Error A (RuntimeError) from test.rb:3:in `initialize' from test.rb:3:in `new' from test.rb:3 §10.1.4 线程调度 Thread类提供了一系列的方法用来控制线程调度,Thread#run方法用来运行一个线程;Thread#stop方法停止线程的运行。 Thread#pass方法取消运行当前线程,继续其他线程的运行;Thread#join方法将当前线程挂起直到指定线程运行结束;Thread#value方法和Thread#join类似,将线程挂起直到指定线程运行结束并取得返回值。 a = Thread.new { print "a"; Thread.pass; print "b"; Thread.pass; print "c" } b = Thread.new { print "x"; Thread.pass; print "y"; Thread.pass; print "z" } a.join b.join 执行结果为: axbycz 上面的例子中,我们使用pass方法放弃当前的控制权,继续下一个线程的运行。 a = Thread.new { print "a"; Thread.stop; print "c" } Thread.pass print "b" a.run a.join 执行结果为: abc 上面的例子中,线程a开始运行后先输出“a”,然后使用stop方法停止运行;主线程使用pass方法放弃放弃当前的控制权,保证“a”最先输出。然后我们使用run方法重新运行线程a,所以输出结果为“abc”。 a = Thread.new { 4 + 5 } puts a.value 执行结果为: 9 上面的例子中,我们是用value方法等待线程结束并打印出线程执行的返回值,线程执行的返回值就是线程执行的最后一个语句的值。 §10.1.5 线程同步 因为线程共享内存空间,所以可以使用普通的变量完成线程间的数据交换工作。如果有多个线程同时工作,请确保它们之间不存在互相等待以到达某一点或完成的情况。如果操作错误,可能会导致死锁状态,两个线程都无法完成,因为它们都在相互等待。 使用线程的另一个常见问题是竞争状态。如果一个线程正在将数据写入文件,而另一个线程正在从该文件中读取数据,如果读取线程快于写入线程,则将返回无法预料的结果。这种情况称为竞争状态。 有时候在运行程序中,一些你认为不需要同步的地方也会出现资源同步的问题: class Counter attr_reader :number def initialize @number = 0 end def plus @number += 1 end end c = Counter.new t1 = Thread.new { 10000.times { c.plus } } t2 = Thread.new { 10000.times { c.plus } } t1.join t2.join puts c.number 执行结果为: 14228 这里或许大家会奇怪,为什么number的值不是20000呢? 原因在于这一句: @number += 1 这一行在解释器内部会分解为多个操作, 得到number的值 将number的值加一 保存number的值 在两个线程运行过程中,因为时机问题,在一个线程读取number的数值后还没来得及进行赋值的时候,另一个线程有可能已经改变了number的数值。这样多个进程同时访问共享数据造成了数据混乱从而与我们预想的结果不符。 §10.1.5.2 Monitor Monitor是一种资源互斥访问的解决方案。它提供了一种机制,以供不同线程互斥访问指定的共享资源。 我们可以通过使用继承Monitor类解决上一节的问题。 require 'monitor' class Counter < Monitor attr_reader :number def initialize @number = 0 super #初始化父类数据 end def plus synchronize do @number += 1 end end end c = Counter.new t1 = Thread.new { 10000.times { c.plus } } t2 = Thread.new { 10000.times { c.plus } } t1.join t2.join puts c.number 执行结果为: 20000 可以看到,继承Monitor类后,我们可以使用synchronize方法,对于一个Monitor类的实例,同时只能有一个线程执行synchronize代码块的操作。代码的执行结果为“20000”,和我们期望的一致。 如果不想使Monitor成为自己类的父类,也可以使用MonitorMixin模块。 require 'monitor' class Counter include MonitorMixin attr_reader :number def initialize @number = 0 super end def plus synchronize do @number += 1 end end end c = Counter.new t1 = Thread.new { 10000.times { c.plus } } t2 = Thread.new { 10000.times { c.plus } } t1.join t2.join puts c.number 执行结果为: 20000 也可以单独使用Monitor类的实例对象来完成同步操作。 require 'monitor' class Counter attr_reader :number def initialize @number = 0 super end def plus @number += 1 end end c = Counter.new lock = Monitor.new t1 = Thread.new { 10000.times { lock.synchronize{ c.plus } } } t2 = Thread.new { 10000.times { lock.synchronize{ c.plus } } } t1.join t2.join puts c.number 执行结果为: 20000 也可以将使用extend方法将Monitor模块的方法引入完成同步操作。 require 'monitor' class Counter attr_reader :number def initialize @number = 0 super end def plus @number += 1 end end c = Counter.new c.extend(MonitorMixin) t1 = Thread.new { 10000.times { c.synchronize{ c.plus } } } t2 = Thread.new { 10000.times { c.synchronize{ c.plus } } } t1.join t2.join puts c.number §10.1.5.2 Mutex Mutex是mutual-exclusion lock(互斥锁)的简称。它实现了一种简单的信号量机制,用来协调并发线程对共享数据的访问。 在多线程并行访问共享数据时,可以使用下列代码(m是Mutex的实例)。 begin m.lock # 访问受m保护的共享数据 ensure m.unlock end Mutex类有个synchronize方法可以简化这一过程。 m.synchronize { # 访问受m保护的共享数据 } 若对Mutex加锁时发现已经处于锁定状态时,线程会挂起直到解锁为止。 我们修改一下上一节的例子, require 'thread' class Counter attr_reader :number def initialize @number = 0 end def plus @number += 1 end end c = Counter.new m = Mutex.new t1 = Thread.new { m.synchronize { 10000.times { c.plus } } } t2 = Thread.new { m.synchronize { 10000.times { c.plus } } } t1.join t2.join puts c.number 输出结果为: 20000 从上一节我们可以知道,若此程序中不使用Mutex加以保护的话,因为时机问题,在一个线程读取number的数值后还没来得及进行赋值的时候,另一个线程可能已经改变了number的数值。这样,通过Mutex互斥锁,我们实现了对共享数据的正确使用。 Mutex主要方法如下: new 生成新的互斥锁 lock 若已经处于加锁状态则会一直等待下去直到解锁 unlock 执行解锁操作,若有其它等锁的线程则会将他们唤醒 synchronize 执行从获得锁到解锁全过程的迭代器 try_lock 尝试执行加锁操作,若已处于加锁状态,则返回false且不会挂起 locked? 测试Mutex对象是否处于加锁状态 §10.1.5.3 Queue Queue就像一条读写数据的管道。提供数据的线程在一边写入数据,而读取数据的线程则在另一边读出数据。若Queue中没有可供读取的数据时,读取数据的线程会挂起等待数据的到来。 下面是一个使用Queue的简单例子: require "thread" q = Queue.new th = Thread.start { while line = q.pop print line end } while gets q.push $_ end q.push nil # 终止标记 th.join 本程序中,一个线程读入一行之后,另一个线程就输出它。若把第3行改成数组,即“q = []”后,线程间失去同步,则程序无法正确运作。 Queue有下列方法: new 生成新的Queue empty 测试Queue是否为空,若为空则返回真 push 将数据压入Queue pop 尝试从Queue中取数据,如果Queue为空当参数为真则引发一个异常,否则挂起线程 enq 含义与push相同 deq 含义与pop相同 clear 清空Queue length 返回Queue中的元素个数 size 含义与length相同 下面的例子使用Queue来解决生产者-消费者问题。生产者-消费者问题是一个经典的进程同步问题,该问题最早由Dijkstra提出,用以演示他提出的信号量机制。生产者线程生产物品,然后将物品放置在一个空缓冲区中供消费者线程消费。消费者线程从缓冲区中拿走产品。生产者线程在缓冲区满时必须等待,直到缓冲区有空间才继续生产。消费者线程在缓冲区空时必须等待,直到缓冲区中有产品才能继续读取。在这个问题上需要考虑缓冲区满和缓冲区空的情况以及竞争条件。 require 'thread' queue = Queue.new consumers = Thread.new do 5.times do |i| obj = queue.deq print "consumer: #{i}\n" sleep(rand(0.05)) end end producers = Thread.new do 5.times do |i| sleep(0.1) print "producer: #{i}\n" queue.enq("Item #{i}") end end producers.join consumers.join §10.2 多进程处理 Thread类实现的并不是真正意义上的操作系统级多线程,但是有时候,你需要将一个任务分解为几个进程去执行,或者可能你需要运行的进程并不是Ruby语言编写,对于这种情况,Ruby语言也有办法处理,Ruby有几种方法创建真正的操作系统级的进程。 §10.2.1 进程创建 §10.2.1.1 system方法和反引号 可以使用反引号或system去执行一个命令。 system(“tar czf samples.tar.gz .”) result = `date` 使用Kernel.system方法去执行一个外部命令时,这个外部命令将在一个子进程中被执行。如果命令无法执行返回false,否则返回true。命令执行的返回值保存在全局变量$?中。 使用system方法执行外部命令,外部命令的结果输出将和程序的结果输出在一起。使用反引号则不同,执行的返回值为就是外部命令执行的输出结果。 result = `date` result #=> Thu Jan 4 22:45:49 CST 2007 §10.2.1.2 popen方法 很多时候,我们需要将输入数据发送给外部命令,然后取回执行结果。可以使用IO.popen方法完成这样的功能。 IO.popen ("date") { |f| puts f.gets }  执行结果为: Thu Jan 4 22:55:19 CST 2007 另一个例子: command1 = IO.popen("dir", "w+") command1.puts "." command1.close_write puts command1.gets §10.2.1.3 fork方法 也可以使用fork方法产生一个新的子进程。产生子进程后原有进程继续向下执行。 fork do 3.times {|i| puts "Child: #{i}" } end 3.times {|i| puts "Parent: #{i}" } 执行结果为: Child: 0 Parent: 0 Child: 1 Parent: 1 Child: 2 Parent: 2 fork方法调用时返回值如果是nil表示子进程ID,否则表示的是父进程的ID。可以使用Process.wait方法来等待子进程执行结束,Process.wait方法返回所在进程的ID。进程的退出状态保存在$?中。 fork{ sleep(3) } Process.wait 注意,如果要使用fork方法,那么在Ruby解释器所在的操作系统必须支持fork系统调用。例如在Window下fork方法就无法使用。 第十一章 基本I/O操作 I/O是Input/Output的缩写, 类IO是所有IO处理的基类,定义了I/O操作的接口。I/O流可以是单向也可以是双向。 IO类实用Unix文件描述符的概念,使用整数代表打开的文件。0代表标准输入,1代表标准输出,2代表标准错误。 IO端口可以以多种模式打开,以下表中列出了所有模式: 模式 含义 r 只读,从文件头开始(默认模式) r+ 读写,从文件头开始 w 只写,文件不存在则创建文件,文件存在则清空内容 w+ 读写,文件不存在则创建文件,文件存在则清空内容 a 只写,文件不存在则创建文件,文件存在则在尾部添加 a+ 读写,文件不存在则创建文件,文件存在则在尾部添加 b (只用于DOS/Windows)二进制文件,可以和上述模式一同出现 §11.1 使用Kernel模块处理I/O操作 在Ruby中你可以使用多种方法操作I/O。在Kernel模块中已经实现了一系列I/O相关的方法gets, open, print, printf, putc, puts, readline, readlines, 和test方法,使用他们可以方便快捷的写出程序。 §11.2文件处理 我们使用File类来操作文件,可以通过File.new来创建一个File类的实例并打开这个文件。 file = File.new("testfile", "r") # ... process the file file.close testfile是想要操作的文件名,”r”说明了文件的操作模式为读取。可以使用”w”表示写入,”rw”表示读写。 最后记得关闭打开的文件,确保所有被缓冲的数据被写入文件,所有相关的资源被释放。 我们也可以使用File.open来打开文件,open和new的不同是open可以使用其后的代码块而new方法则返回一个File类的实例。 File.open("testfile", "r") do |file| # ... process the file end open操作的另一个优点是处理了异常,如果处理一个文件发生错误抛出了 异常的话,那么open操作会自动关闭这个文件,下面是open操作的大致实现: class File def File.open(*args) result = f = File.new(*args) if block_given? begin result = yield f ensure f.close end end return result end end 对于文件的路径,Ruby会在不同的操作系统间作转换。例如,在Windows下,/ruby/sample/test.rb会被转化为\ruby\sample\test.rb。当你使用字符串表示一个Windows下的文件时,请记住使用反斜线先转义: “c:\\ruby\\sample\\test.rb” 你也可以使用File::SEPARATOR表示不同系操作统的路径分割符。 §11.3 StringIO StringIO是与IO相同接口的字符串类。StringIO操作像其它的I/O对象,但StringIO是对字符串进行读和写。如果打开一个StringIO对象用于读,需要提供给它源字符串,然后在StringIO对象上进行所有读操作并返回操作结果。同样,当对一个StringIO对象执行写操作时,需要传递给它一个要被填充的目的字符串。 #创建一个StringIO对象 io = StringIO.new("abc") io.getc #从io中读取一个字符,返回ASCII码 p io.getc =>98 #当前的位置 p io.pos => 2 p io.size => 3 io << "bar" p io.size => 5 io.rewind p io.gets => "abbar" require 'stringio' ip = StringIO.new("This\nis\na\ndog!") op = StringIO.new("", "w") ip.each_line do |line| op.print "...", line end print op.string 输出结果为: ...This ...is ...a ...dog! §11.4 Socket 使用Ruby处理网络十分灵活,Ruby提供了一系列的库来支持TCP,UDP,HTTP,SOCKS,Unix套接字 和各种各样的网络协议。所有的Socket的都是IO直接或间接的子类。 (to-do 图) require 'socket' socket = UDPSocket.new socket.bind("127.0.0.1", 12345) loop do msg, sender = socket.recvfrom(100) host = sender[3] puts "#{Time.now}: #{host} '#{msg}'" end require 'socket' log = UDPSocket.new log.connect("127.0.0.1", 12121) log.print "Up and Running!" log.print "Done!" 第十二章 反射和对象空间 §12.1 ObjectSpace模块 使用ObjectSpace模块可以察看当前系统中的所有对象,也可以和垃圾回收器交互。 include ObjectSpace a = 123 b = 456.789 each_object(Numeric) { |x| p x } 执行结果如下: 456.789 123.456 2.71828182845905 3.14159265358979 2.22044604925031e-016 1.79769313486232e+308 2.2250738585072e-308 100.0 86400000000 30.6001 30.6001 365.25 365.25 122.1 4.0 36524.25 1867216.25 30.6001 365.25 4.0 100.0 each_object列举系统中的所有指定的对象并返回对象的数目,上述代码列出了系统中所有的数值型数据,除了我们的数据外还有一些系统预定义的常量。输出结果在你的机器上或许会稍有不同。 注意each_ojbect方法并不列举Fixnum, Symbol, true, false和nil型的数据。 a = 102.7 b = 95 # Won't be returned c = 12345678987654321 count = ObjectSpace.each_object(Numeric) {|x| p x } puts "Total count: #{count}" 执行结果为: 12345678987654321 102.7 2.71828182845905 3.14159265358979 2.22044604925031e-016 1.79769313486232e+308 2.2250738585072e-308 100.0 86400000000 30.6001 30.6001 365.25 365.25 122.1 4.0 36524.25 1867216.25 30.6001 365.25 4.0 100.0 Total count: 21 使用ObjectSpace模块也可以给对象提供析构方法,这个方法在对象被垃圾回收器摧毁时被调用。 include ObjectSpace a = "A" b = "B" c = "C" define_finalizer(a, proc {|id| puts "Finalizer one on #{id}" }) define_finalizer(a, proc {|id| puts "Finalizer two on #{id}" }) define_finalizer(b, proc {|id| puts "Finalizer three on #{id}" }) 执行结果为: Finalizer three on 537763470 Finalizer one on 537763480 Finalizer two on 537763480 §12.2 察看类和对象的状态 当找到一个感兴趣的对象,如何察看它的内部状态?毕竟,我们往往对它的内部状态更感兴趣。在静态语言中,对象的类型取决于对象的所属类,察看对象所属的类就可以得到对象支持的方法。和静态语言不同,Ruby支持更为自由的对象。 (to-do 解释Ruby对象的自由性) 可以查看一个对象支持那些方法: str = “123” #创建一个String对象 list = str.methods list[0..3] => ["lstrip!", "to_sym", "scan", "instance_variables"] 可以查看某个对象是否支持特定的方法: str.respond_to?(“==”) => true str.respond_to?(“scan”) => true str.respond_to?(“xyz”) => false 也可以查看对象的ID,对象从属的类,对象和类的关系: num = 6 num.id => 13 num.class => Fixnum num.kind_of? Fixnum => true num.kind_of? Numeric => true num.instance_of? Fixnum => true num.instance_of? Numeric => false 可以查看class之间的从属关系,可以通过class#superclass察看类的父类,可以通过Module#ancestors列出所有的父类和模块。 Fixnum.superclass => Integer Fixnum.ancestors => [Fixnum, Integer, Precision, Numeric, Comparable, Object, PP::ObjectMixin, Fox, Kernel] 我们还可以查看对象中方法的存取属性,可以查看对象中的常量和局部变量。 class InfoTest @@static_variable = 101 CONST = "This is a constant!" private def private_method end protected def protected_method end public def public_method @instance_variable = 100 i = 1 j = 2 local_variables end def InfoTest.class_method end end 我们可以通过以下方法访问InfoTest类的信息: InfoTest.private_instance_methods(false) => private_method InfoTest.protected_instance_methods(false) => protected_method InfoTest.public_instance_methods(false) => public_method InfoTest.singleton_methods(false) => class_method InfoTest.class_variables => @@static_variable InfoTest.constants => CONST InfoTest.constants - InfoTest.superclass.constants => CONST 这里我们可以通过private_instance_methods,protected_instance_methods, public_instance_methods,singleton_methods方法分别访问类的私有方法,保护方法,公有方法和类方法。参数false表示不列出父类的方法,如果参数为true那么父类的相关方法也会被列出。class_variables可以访问类的类变量,constants类出类的包括父类的所有常量,所以InfoTest.constants-InfoTest.superclass.constants表示只列出InfoTest类的常量。这些方法都是在Module类中作的定义,Module类包含许多可以访问类详细信息的方法。 InfoTest是Class类的实例,Class类的继承关系如下, Class -> Module -> Object 所以InfoTest类可以使用这些方法访问类的详细信息。 info = InfoTest.new info.instance_variables => [] info.public_method info.instance_variables => [@instance_variable] instance_variables方法在Object类作的定义,返回当前实例对象的中的变量,从上面的代码可以看出,只有当被使用到才会创建相应的变量,从这里也可以看出Ruby的动态性。 §12.3 动态方法调用 在Ruby中,有多种方法可以实现方法的动态调用。 §12.3.1 使用send方法 第一种实现动态方法调用是使用send方法,send方法在Object类中定义,方法的第一个参数是一个符号用来表示所要调用的方法,后面则是所调用方法需要的参数。 "This is a dog1".send(:length) => 14 上面的代码中通过send方法去对一个字符串执行length操作,返回字符串的长度。 class TestClass def hello(*args) "Hello " + args.join(' ') end end a = TestClass.new puts a.send :hello, "This", "is", "a", "dog!" 执行结果为: Hello This is a dog! §12.3.2 使用Method类和UnboundMethod类 另一种实现动态方法调用是使用Object类的method方法,这个方法返回一个Method类的对象。我们可以使用call方法来执行方法调用。 test1 = "This is a dog1".method(:length) test1.call => 14 class Test def initialize(var) @var = var end def hello() "Hello, @var = #{@var}" end end k = Test.new(10) m = k.method(:hello) m.call #=> "Hello, @iv = 99" l = Test.new('Grant') m = l.method("hello") m.call #=> "Hello, @iv = Fred" 可以在使用对象的任何地方使用method对象,当调用call方法时,参数所指明的方法会被执行,这种行为有些像C语言中的函数指针。你也可以把method对象作为一个迭代器使用。 def square(a) a*a end mObj = method(:square) [1, 2, 3, 4].collect(&mObj) => [1 4 9 16] Method对象都是和某一特定对象绑定的,也就是说你需要通过某一对象使用Method对象。你也可以通过UnboundMethod类创建对象,然后再把它绑定到某个具体的对象中。如果UnboundMethod对象调用时尚未绑定,则会引发异常。 class Double def get_value 2 * @side end def initialize(side) @side = side end end a = Double.instance_method(:get_value) #返回一个UnboundMethod对象 s = Double.new(50) b = a.bind(s) puts b.call 执行结果为: 100 看下面一个更具体的例子: class CommandInterpreter def do_2() print "This is 2\n"; end def do_1() print "This is 1\n"; end def do_4() print "This is 4\n"; end def do_3() print "This is 3\n"; end Dispatcher = { ?2 => instance_method(:do_2), ?1 => instance_method(:do_1), ?4 => instance_method(:do_4), ?3 => instance_method(:do_3) } def interpret(string) string.each_byte {|i| Dispatcher[i].bind(self).call } end end interpreter = CommandInterpreter.new interpreter.interpret('1234') 执行结果为: This is 1 This is 2 This is 3 This is 4 §12.3.3 使用eval方法 我们还可以使用eval方法实现方法动态调用。eval方法在Kernel模块中定义,有多种变体如class_eval,module_eval,instance_eval等。Eval方法将分析其后的字符串参数并把这个字符串参数作为Ruby代码执行。 str = "Hello" eval "str + ' World!'" => Hello World! sentence = %q{"This is a test!".length} eval sentence => 15 当我们在使用eval方法时,我们可以通过eval方法的第二个参数指明eval所运行代码的上下文环境,这个参数可以是Binding类对象或Proc类对象。Binding类封装了代码在某一环境运行的上下文,可以供以后使用。 class BindingTest def initialize(n) @value = n end def getBinding return binding() #使用Kernel#binding方法返回一个Binding对象 end end obj1 = BindingTest.new(10) binding1 = obj1.getBinding obj2 = BindingTest.new("Binding Test") binding2 = obj2.getBinding puts eval("@value", binding1) #=> 10 puts eval("@value", binding2) #=> Binding Test puts eval("@value") #=> nil 可以看到上述代码中,@value在binding1所指明的上下文环境中值为10,在binding2所指明的上下文环境中值为Binding Test。当eval方法不提供binding参数时,在当前上下文环境中@value并未定义,值为nil。 §12.3.4 性能 从上面我们可以看到,我们有多种实现动态方法调用的方式,但是请注意,在这多种方式中,使用eval方法最慢。Ruby提供了一个Benchmark模块可以用来报告代码的执行时间,衡量代码的性能。以下是使用Benchmark模块来显示不同动态方法调用的执行时间: require 'benchmark' test_string = "This is a test!" meth = test_string.method(:length) num = 100000 Benchmark.bm(12) do |x| x.report("call") { num.times { meth.call } } x.report("send") { num.times { test_string.send(:length) } } x.report("eval") { num.times { eval "test_string.length" } } end 执行结果为: all 0.090000 0.000000 0.090000 ( 0.090000) send 0.090000 0.000000 0.090000 ( 0.090000) eval 1.352000 0.000000 1.352000 ( 1.352000) 可以看到,使用eval方法的执行时间远远慢于其他两种。 §12.4 Hook和回调方法 §12.4.1 什么是Hook 最早是在操作系统中出现的Hook概念,在Unix/Linux/Windows中Hook的概念类似,Hook的目的在于允许用户在系统执行原有流程的过程中,插入自己的代码处理一些额外的事情。典型的Hook就是使用实现自己功能的方法替换原有的方法,在所需要的额外处理完成之后,又恢复原有方法的处理流程。 (to-do Hook的图) §12.4.2 Ruby中的Hook 钩子是一种可以跟踪Ruby事件的技术,例如设定一个钩子侦测对象创建,设定好后,钩子在发生对象创建时触发。Ruby是动态语言,可以在运行时改变类的方法和属性,我们可以很容易实现这样的功能。 下面我们来看一个简单的例子, module Mod alias_method :old_exit, :exit def exit(code=0) puts "Exiting with #{code}" old_exit(code) end end include Mod exit(99) 执行结果为: Exiting with code 99 上面的例子中,先使用alias_method给方法添加别名old_new,然后重写exit方法,在exit方法使用方法的别名来调用原有方法来完成原有功能,然后附加额外的信息。 同样,我们利用这种技术可以跟踪对象的创建。 class Class alias_method :old_new, :new def new(*args) result = old_new(*args) puts "The object is: #{result}" result end end class Test def initialize(name) @name = name end def to_s "#{@name}" end end obj1 = Test.new("obj1") obj2 = Test.new("obj2") obj3 = String.new("12345") 执行结果为: The object is: obj1 The object is: obj2 The object is: 12345 §11.4.2 回调方法 Ruby实现了一些方法,当特定的事件发生时,相应的方法会被调用。你可以重新定义这些方法加入额外的处理或者改变系统的默认行为。 Module#method_added Module#method_removed Module#method_undefined Kernel#singleton_method_added Kernel#singleton_method_removed Kernel#singleton_method_ undefined Class#inherited Module#extend_object 默认情况下,这些方法什么也不做。 §12.5 跟踪程序的运行 §12.5.1 set_trace_func 可以使用Kernel#set_trace_func来跟踪程序的运行。set_trace_func的原型为: set_trace_func(proc) => proc set_trace_func(nil) => nil 创建一个Proc作为跟踪的处理器,参数为nil的时候关闭处理。Proc有六个参数,一个事件名称,一个文件名,一个行号,一个对象ID,一个binding(to-do ?),一个类名。只要发生事件,Proc就会被调用。事件有这么几种: c-call 调用C语言过程 c-return 从C语言过程中返回 call 调用一个Ruby方法 class 开始一个类或模块的定义 end 结束一个类或模块的定义 line 执行新的一行代码 raise 发起一个异常 return 从Ruby方法中返回 class Test def test a = 1 end end set_trace_func proc {|event, file, line, id, binding, classname| printf "%10s %10s:%2d %10s %8s\n", event, file, line, id, classname } t = Test.new t.test 执行结果为: line test.rb:11 false c-call test.r b:11 new Class c-call test.rb:11 initialize Object c-return test.rb:11 initialize Object c-return test.rb:11 new Class line test.rb:12 false call test.rb: 2 test Test line test.rb: 3 test Test return test.rb: 3 test Test §12.5.2 trace_var 可以使用Kernel#trace_var方法对全局变量添加钩子,当对一个全局变量执行赋值操作时,这个钩子会被调用。第一个参数是一个符号,用来指明要监视哪一个全局变量,第二个参数是发生赋值操作时执行的Proc对象或块。 trace_var :$_, proc {|v| puts "$_ is now '#{v}'" } $_ = "hello" $_ = "there" 执行结果为: $_ is now 'hello' $_ is now 'there' §12.5.3 caller 可以使用Kernel#caller来得到当前的调用堆栈。 def meth1 puts caller.join("\n") end def meth2 meth1 end def meth3 meth2 end meth3 执行结果为: test.rb:6:in `meth2' test.rb:10:in `meth3' test.rb:13 §12.5.3 __FILE__,__LINE__和SCRIPT_LINES__ __FILE__ 指明当前的原文件名 __LINE__ 当前在源文件中的行号 __FILE__经常和全局变量$0一起使用来判断一个文件是否由用户直接调用。举个例子,库文件的开发者经常在所开发的库中包含一些测试代码,这些测试代码在库被其他文件引用时不会执行。 # library code # ... if __FILE__ == $0 # tests... end SCRIPT_LINES__默认情况下不会被定义。若将它定义为哈希表后,在解释源代码时,它将依次处理使用require或load载入的文件,将源文件名作为哈希表元素的索引,源文件的内容会被按行切分转化为数组作为哈希表的内容。 SCRIPT_LINES__ = {} require 'Benchmark' puts "Files: #{SCRIPT_LINES__.keys.join(', ')}" SCRIPT_LINES__['d:/ruby/lib/ruby/1.8/Benchmark.rb'].each{ |line| puts "Source: #{line}" } 以上代码将输出Benchmark.rb的内容。 第十三章 序列化和YAML §13.1 序列化的概念 序列化是将对象状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它将数据流转换为对象。这两个过程结合起来,就使得数据能够被轻松地存储和传输。 为什么需使用序列化?有两个重要的原因:一个原因是将对象的状态保存在存储介质中,以便在以后重新创建精确的副本;另一个原因是可以将对象从一个应用程序发送到另一个应用程序中,远程处理还可以使用序列化将对象从一台机器上的应用程序传递到另一台机器上的应用程序中。 一般程序在运行时产生对象,这些对象随着程序的停止运行而消失,我们可以通过序列化将对象保存下来,这样在程序终止运行后这些对象仍然存在。以后可以在程序再次运行时通过读取数据重建这些对象,也可以在在其他程序中利用这些保存下来的对象。这种情况下我们通过使用对象的序列化和反序列化来完成这样的功能。 §13.2 使用序列化 §13.2.1 二进制数据保存 在Ruby中,序列化也称为marshaling,使用Marshal类R可以把一个对象转换成字节流,并把它存在应用程序外。这样保存的对象可以在以后被其它的实例或者其它的程序读取使用。 可以使用Marshal类的dump方法保存对象,以后使用load可以读取以保存的这个对象。 class Rectangle def initialize(length, width) @length= length @width = width @area = length * width end def to_s "#@length #@width #@area" end end obj = Rectangle.new(10,20) puts "Before: obj = #{obj}" data = Marshal.dump(obj) obj = Marshal.load(data) puts "After: obj = #{obj}" puts obj.inspect 执行结果为: Before: obj = 10 20 200 After: obj = 10 20 200 # §13.2.2 YAML数据保存 我们也可以使用YAML来实现序列化: require 'yaml' class Rectangle def initialize(length, width) @length= length @width = width @area = length * width end def to_s "#@length #@width #@area" end end obj = Rectangle.new(10,20) puts "Before: obj = #{obj}" data = YAML.dump(obj) obj = YAML.load(data) puts "After: obj = #{obj}" puts obj.inspect 执行结果为: Before: obj = 10 20 200 After: obj = 10 20 200 # §13.3 定制序列化 §13.3.1 二进制数据保存 并不是所有的数据都可以被序列化和适合序列化,有时候你想自己决定哪些东西被序列化,Marshal类提供了二个方法来实现这样的功能,一个是marshal_dump,一个是marshal_load。当使用Marshal.load对一个对象进行序列化的时候,如果这个对象定义了marshal_dump或marshal_load方法,那么这个方法将会被调用。 class Rectangle def initialize(length, width) @length= length @width = width @area = length * width end def marshal_dump [ @length, @width ] end def marshal_load(variables) @length = variables[0] @width = variables[1] @area = "unknown" end def to_s "#@length #@width #@area" end end obj = Rectangle.new(10, 20) puts "Before: obj = #{obj}" data = Marshal.dump(obj) obj = Marshal.load(data) puts "After: obj = #{obj}" 执行结果为: Before: obj = 10 20 200 After: obj = 10 20 unknown 一些对象不能被保存,如果将要被保存的对象包括其它程序调用,方法体,IO实例等,或者单独的一个对象,或者当你保存一个匿名的类或者方法时,一个TypeError错误就会产生。 §13.3.2 YAML数据保存 使用Marshal模块实现序列化将对象保存为二进制数据,这样的做法有一个显著的缺点,由于保存的数据是二进制形式,如果解释器发生了重大改变且Marshal的二进制数据流处理格式和方式也发生了变化,那么就不保证原有已经dump的文件还可以继续使用。 如果使用文本格式的文件来保存序列化数据,那么就不用担心文件是否可读,在Ruby1.8种,可以使用YAML来实现这样的功能。 对于marshaling,除了二进制格式,也可以采用YAML的方法,它是一种纯文字又易懂的格式,很有趣而且比起XML简单多了。 修改一下上一节的例子,我们需要定义to_yaml_properties方法, require 'yaml' class Rectangle def initialize(length, width) @length= length @width = width @area = length * width end def to_yaml_properties %w{ @length @width } end def to_s "#@length #@width #@area" end end obj = Rectangle.new(10,20) puts "Before: obj = #{obj}" data = YAML.dump(obj) obj = YAML.load(data) puts "After: obj = #{obj}" 执行结果为: Before: obj = 10 20 200 After: obj = 10 20 我们可以看看YAML究竟把数据保存成什么样的格式, obj = Rectangle.new(10,20) puts YAML.dump(obj) 执行结果为: -- !ruby/object:Rectangle length: 10 width: 20 §13.3 YAML 看了上一节,大家或许会问,“为什么要命名为 YAML?”已经有许多工具采用了招人喜欢的“YA*”形式的首字母缩略词,来表示“还有另一种 XXX(Yet Another XXX)”。在开放源码这个充满智慧的领域中,YAML 没有使用其名称所暗示的首字母缩略词,而是采用了循环的“YAML 不是标记语言(YAML Ain't Markup Language)”的缩写。然而对此产生的某种感觉是:YAML 确实可以做标记语言所做的工作,却不需要任何标记。 尽管YAML与XML一样普通,但在阅读、编辑、修改和产生方面,它比 XML 简单得多。可以用XML表示的任何东西,都可以用YAML表示,而且几乎总是更紧凑。 XML 是一个典型的由委员会驱动的庞然大物,它试图成为一种文档格式、数据格式、消息包格式、安全的 RPC 通道(SOAP)以及一种对象数据库。而且,XML 为每一类型的访问和操作都提供了大量的 API:DOM、SAX、XSLT、XPATH、JDOM 以及许多不太常见的接口层。非常了不起的是 XML 完成了所有这些工作;令人失望的是没有一项工作是完美无缺的。 YAML的关注面则比较窄,它只是清晰地表示在动态编程语言(如 Perl、Python、Ruby等)中所遇到的数据结构以及数据类型。 YAML的优点: l 适合人们阅读 l 适合与脚本语言交流 l 使用宿主语言的内部数据结构 l 拥有一致性的信息模型 l 使基于流式数据的处理成为可能 l 富于表达和可扩展 l 容易实现 YAML做为一个可移植的对象序列化方案,可以在不同的Ruby进程中将对象以普通文本传递,此外也适合ruby与那些支持YAML的语言之间交换数据用。 YAML中数据主要由序列(sequence),map(有的也叫做hash)和标量(scalar)来表示。语法比较简单,易于人们阅读。 l 注释由#开头 l 序列由"-"开头 l map用key:value的格式 l "---"表示一个yaml文档的开始 l list和hash可以嵌套 l block的概念:一个 block 是一段文本。 l Inline Collections:数据都写在一行 §13.3.1 集合类型 §13.3.1.1 序列 l 基本序列 可以在每一个新行前加“-”构成一个序列。 YAML中, - Henry - Mike - Tom 在Ruby中, [‘Henry’, ‘Mike’, ‘Tom’] l 嵌套序列 使用位于空行上的“-”,你可以在一个序列内包含另一个序列。 - - Henry - Mike - Tom 在Ruby中, [[‘Henry’, ‘Mike’, ‘Tom’]] 使用缩进表示不同的层次,序列也可以表示更深层次的嵌套 - - - Henry - Mike - Tom 在Ruby中, [[[‘Henry’, ‘Mike’, ‘Tom’]]] l 混合嵌套序列 在序列中也可以嵌套其他的YAML数据结构: - Henry - - 13 - male - Mike - Henry 在Ruby中, [‘Henry’, [‘13’, ‘male’], ‘Mike’, ‘Henry’] §13.3.1.2 表 你可以使用Key:value的形式组织数据,每个key:value位于单独的一行,这样的数据结构称为表,也叫做哈希表或字典。 l 基本表 Grant:student Tom:teacher 在Ruby中, {‘Grant’ => ‘student’, ‘Tom’ => ‘teacher’ } l 含有序列的表 Grant:student Tom: - teacher - male 在Ruby中, {‘Grant’ => ‘student’, ‘Tom’ => [‘teacher’, ‘male’ } l 嵌套表 teacher: Mike students: Grant:13 Henry:14 在Ruby中, { ‘teacher’ => ‘Mike’, ‘students’ => { ‘Grant’ => ‘13’, ‘Henry’ => ‘14’ } } l 混合表 grade: 1 teachers: Mike:math Jane:english classes: - Grant:13 Henry:14 - Jack:13 Rose:13 在Ruby中, { ‘grade’ => ‘1’, ‘teachers’ => { ‘Mike’ => ‘math’, ‘Jane’ => ‘English’ } ‘classes’ => [ { ‘Grant’ => ‘13’, ‘Henry’ => ‘14’ }, { ‘Jack’ => ‘13’, ‘Rose’ => ‘13’ } ] } l 序列中嵌套表的缩写 当在序列中添加一个表时,可以 - worked on test.py: - This is a test 在Ruby中, [ {‘worked on test.py’ => [‘This is a test’] } ] l 表中嵌套序列的缩写 当在表中嵌套一个序列时,可以不需要缩进。 Students: - ‘Grant’ - ‘Henry’ 在Ruby中, {‘Students’ => [‘Grant’, ‘Henry’] } l 插入键(Merge key) 可以使用插入键在一个表中插入另一个表 { information: Name: Jane course: English <<: Age: 25 } 在Ruby中, {‘information’ => { ‘name’ => ‘Jane’, ‘course’ => ‘English’, ‘age’ => ‘25’ } } §13.3.2 单行集合类型 l 单行序列 l 单行映射 §13.3.3 基本类型 l String l Scalar l NULL l Boolean l Integer l Float l Time l Date §13.3.4 块 §13.3.5 别名和锚(Aliases and Anchors) §13.3.6 文档 §13.3.7 Ruby中YAML的使用 Ruby中的YAML支持 Ruby1.8已经包含了YAML支持了。只需要require进来就行了。 require 'yaml' class Person attr_accessor :name, :sex, :age def initialize(name, gender, age) @name = name @gender = gender @age = age end end #创建一个对象 person=Person.new("Tom", "male", 15) #打印序列化之后的结果 puts person.to_yaml 结果为: --- !ruby/object:Person age: 15 gender: male name: Tom 假设现在有一个person.xml文件内容为: --- !ruby/object:Person age: 25 gender: male name: Henry 如下代码: #从person.yml创建对象 person2 = YAML::load( File.open('d:\\person.yml') ) puts person2.inspect puts person2.class # 结果是Person puts person2.name 执行结果为: # Person Henry 第十四章 安全控制 Ruby代码可以方便的移植,也可以方便的对原代码进行分发。这样带来了灵活性的同时也带来了巨大的隐患,如何才能防止不安全代码的执行呢? 我们可以利用Ruby语言含有的安全控制功能来实现这一目标。Ruby安全控制可以用来锁住有嫌疑的数据,这样的数据被称作“tainted”。Ruby安全控制系统引入了一种机制,可以由用户决定如何处理有潜在危险的数据。 全局变量$SAFE用来控制安全级别。默认的级别是0。可以通过改变$SAFE的值或使用命令行参数-T来指定安全级别。安全级别数值越低表示数据越可信。数值低的安全级别的约束同样对数值高的安全级别起作用。不能在代码中降低当前安全级别。 $SAFE 约束 0 不对tainted数据作检验,是Ruby的默认安全级别 1 不允许有危险的操作使用tainted数据 2 不允许加载可写路径之外的代码 3 所有新建立的对象都被认为是tainted数据 4 将tainted数据和非tainted数据隔离开,非tainted数据不允许改变 新建立的线程会继承当前$SAFE的值。在新建立的线程中,可以改变$SAFE的值而并不影响其它线程。这样可以容易的实现“沙箱”的功能。例如在一个线程中,当需要从外部加载代码并加以执行时,可以通过降低$SAFE的值来限制代码执行一些有危险的操作,从而降低代码的潜在风险。 什么是tainted数据?简单的说,就是这个数据受到了污染而变得不可信。当一个Ruby对象起源自一些外部资源,例如一个字符串保存了被读取外部文件的内容,这个对象被自动标记为“tainted”。如果使用这个tainted对象而得到其他的对象,那么这个新得到的对象也被标记为“tainted”。可以使用Object#tainted?方法来判断一个对象是否被“tainted”。 str1 = "This is a string" str1.tainted? #=> false str2 = str1 + “!” str2.tainted? #=> false str3 = gets #str1的值从输入流读取 str3.tainted? #=> true str4 = str3 + “!” str4.tainted? #=> true 可以看到,由于str3的内容来自外部,所谓str3被认为是不可信的,对str3作操作的结果同样被认为不可信的。 §14.1 0级 0级是默认的安全级别,不对tainted数据作任何检查。环境变量PATH的值会被检查,只有PATH内包含的目录对所有人都可写PATH才会被置为tainted。 其他任何来自IO的数据,环境变量和ARGV自动被标记为tainted。 §14.1 1级 对tainted数据有潜在危险的操作被禁止。这一级别适合用来处理输入包含一些不可信的数据,例如CGI程序。 §14.2 2级 除了1级的约束之外,对文件有潜在危险的操作被禁止。 §14.3 3级 除了2级的约束之外,所有新产生的对象都被标记为tainted。 §14.4 4级 除了3级的约束之外,不能改变全局变量的值。 第十五章 单元测试 §15.1 什么是单元测试 §15.2 Ruby单元测试框架 第二部分 内置类与模块 这一部分我们将列出Ruby语言标准内置的类与模块,这些类与模块可以在任何Ruby程序中使用而不需要额外使用require语句。 第一章 内置类 §1.1 Array Array类是有序的对象集合。Array类以整数作为索引,索引从0开始,负数索引表示从尾部逆序开始。例如-1表示最后一个元素,-2表示倒数第二个元素,依次类推。 l 父类 Array类的父类为Object类。 l Mix-in Enumerable l 类方法 §1.2 Bignum §1.3 Binding §1.4 Class §1.5 Continuation Continuation对象由Kernel#callcc方法产生,功能有些类似C语言中的setjmp/longjmp,它保存了执行环境,可以供代码以后使用。 Continuation对象只有一个call方法,当执行了call方法后,当前执行语句会转到Continuation对象的块后面,继续执行下面的代码。 obj = callcc{|continuation| continuation} puts "start..." obj.call if obj puts "end..." 执行结果为: start... start... end... 上面的例子中,首先使用callcc生成一个Continuation对象,然后输出“start…”在调用call后,当前执行语句跳转到callcc语句紧接的块后,所以“start…”被输出了两次。在执行了一次call方法后,obj对象会被置为nil。 下面一个例子是在方法内发生跳转: def method callcc {|cont| return cont} puts "in method..." end puts "start..." obj = method() puts "end..." obj.call if obj 执行结果为: start... end... in method... end... §1.6 Dir §1.7 Exception §1.8 FalseClass §1.9 File §1.10 File::Stat §1.11 Fixnum §1.12 Float §1.13 Hash §1.14 Integer §1.15 IO §1.16 MatchData §1.17 Method §1.18 Module §1.19 NilClass §1.20 Numeric §1.21 Object §1.22 Proc §1.23 Process::Status §1.24 Range §1.25 Regexp §1.26 String §1.27 Struct §1.28 Struct::Tms §1.29 Symbol §1.30 Thread §1.31 ThreadGroup §1.32 Time §1.33 TrueClass §1.34 UnboundMethod 第二章 内置模块 §2.1 Comparable §2.2 Enumerable §2.3 Error §2.4 FileTest §2.5 GC §2.6 Kernel §2.7 Marshal §2.8 Math §2.9 ObjectSpace §2.10 Process §2.11 Process::GID §2.12 Process::Sys §2.13 Process::UID §2.14 Signal all?方法 方法原型: enum.all? [ {|obj| block} ] 方法依次将集合内的每个元素传递给块,如果所有的执行结果都为真那么方法返回true,任意一次执行结果为假那么方法返回false。如果忽略了block块,则Ruby会自动添加一个{|obj|obj}块。 %w{ ant bear cat}.all? {|word| word.length >= 3} #=> true %w{ ant bear cat}.all? {|word| word.length >= 4} #=> false [ nil, true, 99 ].all? #=> false Inject方法 方法原型: enum.inject(initial) {| memo, obj | block } => obj enum.inject {| memo, obj | block } => obj memo被设置为块计算后的返回值。首先,用initial初始化memo,并将memo与obj参数传递给块进行计算,然后将上步块计算后的值赋值给 memo后,再继续计算块,以此类推。 若省略了初始值initial,开始时会把第一和第二个元素传递给块。若只有一个元素时,将直接返回首元素,而不会执行块。 若没有元素,则返回nil。 # 求5到10的和 (5..10).inject {|sum, n| sum + n } #=> 45 # 求5到10的积 (5..10).inject(1) {|product, n| product * n } #=> 151200 # 找出最长的单词 longest = %w{ cat sheep bear }.inject do |memo,word| memo.length > word.length ? memo : word end longest #=> "sheep" # 找出最长单词的长度 longest = %w{ cat sheep bear }.inject(0) do |memo,word| memo >= word.length ? memo : word.length end longest #=> 5 这个方法之所以用inject这个名称,是因为这个方法有注射动作,即将initial及后来的块计算结果注入到memo中。 第三部分 Ruby语言总结 l 书写习惯 Ruby是一种面向行的语言,一行可以写多个语句,使用“;”隔开。一条语句也可以写在多行,行之间使用“\”连接。 l 语言注释 Rub中提供两种形式的注释: 1. 单行注释,以“#”开始直到行末尾。 2. 多行注释,在“=begin”和“=end”之间的行会被解释器忽略。 l BEGIN和END块 每一个Ruby源文件可以声明BEGIN块和END块,BEGIN块在文件载入被执行,END块在程序执行完毕被执行。 BEGIN { #Begin code } END { #End code } l 常用分隔符输入 分隔符输入格式以一个“%”开始,紧接着一个字符表明输入类型,随后的字符表示分隔符。分隔符可以是任何的非字母单字节字符。如果分隔符是“(,[,{,<”,则表示结束的分隔符为相应的“),],},>”,而且可以嵌套出现。其它的分隔符则是分隔字符下一次出现的位置。 %Q/This is a string/ %Q a string> 使用分隔符也可以跨越多行: string = %q{ BEGIN { puts "===BEGIN===" } } %q 单引号包围的字符串 % 双引号包围的字符串 %Q 双引号包围的字符串 %w 每一个元素都是字符串的数组 %W 每一个元素都是字符串的数组 %r 正则表达式 %x Shell命令 l 命名规则 Ruby中的对象和常量保存了对实际对象的引用,对象自己并没有类型,所谓的对象的类型是指它引用的实际对象的类型。 常量可以在类或模块中定义,访问时需要使用类名加上“::”。 l 变量 在Ruby中使用不同的前缀来区分变量的不同类型: 类型 示例 解释 全局变量 $foo 全局变量以$开始,未初始化的全局变量值为nil 实例变量 @foo 实例变量以@开始,未初始化的实例变量值为nil 类变量 @@foo 类变量以@@开始,必须初始化 局部变量 foo 局部变量以小写字母或下划线开始 常量 Foo 常量以大写字母开始 l 预定义变量 n 异常相关 名称 类型 解释 $! Exception 最近被抛出的异常对象,可以在rescue语句中使用=>访问 $@ Array 最近被抛出异常相关的堆栈信息,可以使用Exception#backtrace方法访问 n 正则表达式匹配相关 名称 类型 解释 $& String 已经匹配到的字符串 $@ String 最后一个使用括号匹配到的字符串 $` String 前一个匹配到的字符串 $’ String 下一个匹配到的字符串 $= Object (已废弃)如果内容不为nil或failse,那么正则表达式匹配,字符串比较和Hash表的值不区分大小写 $1到$9 String 正则表达式匹配使用圆括号相应的匹配串 $~ MatchData 保存正则表达式匹配结果 n 输入输出相关 名称 类型 解释 $/ String 输入分隔符 $-0 String 和$/同义 $\ String 输出分隔符 $, String 使用Array#join方法输出时的分隔符,默认为nil $. Fixnum $; String 使用String#split方法时的分隔符,默认为nil $< Object ARGF缩写 $> IO $defout缩写 $_ String $defout IO $deferr IO $-F String $stderr IO $stdin IO $stdout IO n 环境相关 名称 类型 解释 $0 String 当前执行应用程序名 $* Array 保存了命令行执行的参数 $” Array $$ Fixnum $? Process::status $: Array $-a Object $-d Object $DEBUG Object __FILE__ String $F Array $FILENAME String $-i String $-I Array $-K String $-l Object __LINE__ String $LOAD_PATH Array $-p Object $SAFE Fixnum $VERBOSE Object $-v Object $-w Object l 标准对象 名称 类型 解释 ARGF Object 和$<同义 ARGV Array 和$*同义 ENV Object false FalseClass nil NilClass self Object true TrueClass l 全局常量 名称 类型 解释 DATA IO 和$<同义 FALSE FalseClass 和$*同义 NIL NilClass RUBY_PLATFORM String RUBY_RELEASE_DATE String RUBY_VERSION String STDERR IO STDIN IO STDOUT IO SCRIPT_LINES__ Hash TOPLEVEL_BINDING Binding TRUE TrueClass 附录 §1 术语对照 Singleton Method 单例方法 Singleton Class 单件类 iterator 迭代器 handler 处理器 callback 回调 class Integer def get_debug_info "This is #{self}" end end puts (0..5).map { |i| i.get_debug_info } 执行结果为: This is 0 This is 1 This is 2 This is 3 This is 4 This is 5

下载文档到电脑,查找使用更方便

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享文档获得金币 ] 3 人已下载

下载文档