第2篇 Ruby基本语法
第3章 Ruby编程基础
本章主要介绍Ruby语言的基本知识,从如何使用Ruby解释器开始介绍,讲解了如何编写、运行Ruby程序。本章将通过介绍Ruby代码的落脚点、Ruby程序的入口等知识,并对比Ruby与Java等静态语言的差别,让读者体验到Ruby语言的简洁性。
本章将详细介绍Ruby语言的基础知识,包括Ruby语言里的变量和常量,因为Ruby是面向对象的程序设计语言,因此它的变量特别丰富,包括了类变量、实例变量、局部变量和全局变量等,本章将详细讲解各种类型的变量。
除了介绍Ruby语言中的变量和常量等知识外,本章将全面介绍Ruby语言所支持的基本数据类型,包括字符串、数值型两种最简单的数据类型。除此之外,还将详细讲解Ruby的两个容器类:数组和Hash对象,以及对这两个容器对象的常用操作,包括迭代。
本章也会介绍Ruby语言所支持的正则表达式、符号对象等。
任何语言都离不开运算符和表达式,但Ruby语言里的运算符是如此独特:它既是运算符,也是一个方法。实际上,说这些运算符是方法更恰当,使用运算符称呼它们仅仅是为了保持和其他语言的兼容。Ruby语言的表达式也是一个很有趣的知识点,Ruby里的绝大部分表达式都有返回值,包括if分支表达式也有返回值。本章会详细介绍这些有趣的知识点。
3.1 Ruby代码基本格式
本节将从Ruby的最基本内容开始,详细介绍Ruby代码的基本格式。
3.1.1 使用Ruby解释器
Ruby语言是解释型的语言,因此无须编译即可运行Ruby程序,但运行Ruby程序需要安装Ruby解释器。在第一章已经详细介绍了如何安装Ruby解释器,成功安装Ruby解释器后就可以在命令窗口运行ruby命令。
运行ruby命令时可以使用如下几个常用选项。
❑ -C directory:执行脚本之前,先进入到directory指定的目录。
❑ -c:只对脚本进行编译,而并不执行。编译后若没发现语法错误,则显示“Syntax OK”。
❑ --copyright:显示当前版本的Ruby解释器的版权信息。
❑ -d或者--debug:以调试模式执行脚本。将$DEBUG设置成true。
❑ -h或--help:显示ruby命令的各种选项的简介。
❑ --version:显示当前Ruby解释器的版本信息。
因此如果我们希望查看是否成功安装了Ruby语言解释器,则可以使用如下命令。
ruby --version
下面是笔者在自己的机器上运行该命令生成的输出。
ruby 1.8.4 (2005-12-24) [i386-mswin32]
除此之外,ruby命令最常用的还是运行Ruby程序,运行Ruby程序的命令格式如下:
ruby RubyFileName
上面的语法格式中,RubyFileName是Ruby源程序的文件名,包括后缀。
假设有如下一个名为hello.rb的程序,该文件的内容只有如下一行。
# 执行输出 puts "Hello World!"
上面的程序也是全世界最出名的、每个程序员开始学习编程时都需要学习的程序:Hello World。编辑好上面的程序后,在命令行运行该程序。采用如下命令:
ruby hello.rb
将可看到如下结果:
Hello World!
在上面的程序中,puts是Ruby语言的输出语句,后面的"Hello World!"是一个字符串常量。上面的程序即表示输出“Hello World!”字符串。
3.1.2 Ruby代码落脚点和程序入口
Ruby代码脚本程序是非常自由的程序,它可以直接放在Ruby源代码中,Ruby源文件的第一行可执行性代码就是Ruby程序的入口。例如:
puts "Hello World"
从上面的代码中可以看出,Ruby代码可以直接放在源程序中。Ruby程序的源文件命名没有太多额外的要求,甚至可以不以*.rb作为后缀,但通常我们建议将Ruby源文件以*.rb作为后缀,且文件名以能代表该源文件的意义为妙,这样可以提供程序的可读性。
Ruby程序的执行语句可以无须任何符号作为结束,Ruby自己把每行代码作为一个语句。当然,我们也可以在每行语句后面添加英文分号(;)作为语句结束,但Ruby并不强制这样做。
如果我们希望在一行书写多个Ruby语句,则应该使用英文分号(;)来作为语句结束符。如下所示:
puts "Hello" ; puts "World"
提示 Ruby语言虽然也是面向对象的程序设计语言,但它并不要求把所有的代码都放在类定义中。因此Ruby程序会把第一行可执行性代码当成Ruby程序的入口,避免了在Ruby类中定义main方法来作为程序入口。
Ruby程序源代码中可以插入任意多个空白字符,空白字符包括空格、Tab、垂直Tab、退格、换行和换页。
换行符比较特殊。如果换行后的内容是继续上一行内容,该换行符就是空字符,除此以外的换行符则被解释为Ruby语言代码的分隔。
当然,Ruby程序也允许只有一个类定义。如下:
# 定义一个Person类 class Person end
上面的代码定义了一个空类,但这个类里没有任何方法,也没有任何变量。而且这个Ruby源文件里没有任何可执行代码,因此,上面Ruby程序不会自己执行,它只能被其他Ruby程序调用。
除此之外,我们还可以直接将可执行性代码放在类定义中。
# 第一条可执行代码作为程序入口 puts "Hello World" class A # 类定义中的可执行代码,将顺序执行 puts "Hello Ruby" end
上面的代码的执行效果如下:
Hello World Hello Ruby
Ruby源文件还允许直接定义一个方法——该方法不属于任何类。看如下代码:
# 定义一个方法 def info puts "Hello" end
这是看起来有点奇怪的语法,因为我们通常认为:在面向对象的语法里,所有的方法都必须属于一个类,而不是凭空存在。而在Ruby语言里,一个单独的方法可以裸露地定义在这里,不属于任何类。因此,有时候我们干脆称这个方法为函数。
提示 根据Ruby官方文档的解释,当我们直接裸露地定义一个方法时,可以理解为在一个“顶层对象”那儿定义了一个方法,至于这个“顶层对象”就是Object类的实例。只要我们书写Ruby程序,我们就已经处于一个“顶层对象”中。
实际上,对于程序员而言,我们可以认为Ruby定义这种裸露方法,提供了与传统结构化程序设计语言的兼容,这个裸露的方法就是一个函数,可以在任意地方直接调用。
综上所述,我们不难发现Ruby源代码中既可以定义一个方法,也可以定义一个类,还可以直接放置可执行代码,可执行代码还可以直接放在类定义中。Ruby解释器将从程序的第一行可执行代码开始执行——只要这些可执行代码放在类定义中,或者直接裸露在顶层对象中。
3.1.3 Ruby区分大小写
Ruby语言是严格区分大小写的语言,因此不要将class写成Class,当我们在程序中定义一个Person类时,并不等于定义了一个person类,这就是说,Person和person是截然不同的两个东西。
3.1.4 Ruby程序的注释
为程序添加注释可以用来解释程序某些部分的作用和功能,提供程序的可读性。除此之外,注释也是调试程序的重要方式。在某些时候,我们不希望编译程序执行程序中的某些代码,我们就可以将这些代码注释掉。
当然添加注释的最大作用还是提高程序的可读性!很多时候,笔者宁愿自己写一个应用,而不愿意去改进别人的应用,没有合理的注释是一个重要原因。
虽然良好的代码可自成文档,但我们永远也不清楚今后读这段代码的人是谁,他是否能和你有相同的思路。或者一段时间以后,你自己也不清楚你当时写这段代码的目的。通常而言,合理的代码注释应该占源代码的1/3左右。
Ruby语言允许在任何地方插入空字符或注释,但不能插入到标识符和字符串中间。
Ruby源代码的注释有两种形式:
❑ 单行注释
❑ 多行注释
Ruby使用井号(#)表示注释的开始,跟在#号后面直到#号在这行结束为止的代码都将被解释器忽略。
例如前面的代码:
# 定义一个方法 def info puts "Hello" end
这就是单行注释的用法。除此之外,单行注释也是Ruby代码的文档注释。所谓文档注释,就是可以通过rdoc命令提取的注释。
看下面的Ruby源文件代码,将该文件保存为Person.rb。
# Author: Yeeku.H.Lee kongyeeku@163.com # # Version 1.0 # # Copyright (C), 2005-2008, yeeku.H.Lee # # This program is protected by copyright laws. # # Program Name: # # <br>Date: # class Person ## # 定义了一个say方法,该方法用于Person实例打招呼 # def say puts "你好" end end
上面的代码中包含了大量注释,这些注释都是定义在类定义和方法定义之前的,因此这些注释可以被rdoc命令提取出来。
在命令窗口进入上面Person.rb文件所在的路径,在命令窗口输入如下命令:
rdoc Person.rb
上面的命令用于生成Ruby类的rdoc文档,运行该命令后,即可在当前路径下看到一个doc文件夹,打开该文件夹,打开index.html文件,看到如图3.1所示的界面。
图3.1 使用rdoc生成文档注释
从图3.1中可以看出,该HTML文件已经包含了Ruby源文件中的注释,熟悉Java API文档的读者可能对这种文档格式非常熟悉。实际上,这就是Ruby的API文档,是程序员进行开发不可缺少的文档。
提示 当我们在进行Ruby编程时,一定要多查阅系统内建类的API文档,这些API文档才是我们参考的权威资料。
除此之外,为了方便写大块的注释,Ruby语言还支持多行注释,多行注释是=begin行和=end行之间的所有代码。这里值得注意的是:=begin和=end必须顶格写,不能缩进。
看如下代码:
=begin 这是多行注释 另外的注释 =end puts "Hello World!"
上面的代码里放在=begin和=end行之间的部分都会被Ruby解释器忽略。
3.1.5 Ruby中的标识符
Ruby使用ASCII字符集来作为标识符,Ruby是区分大小写的语言。
Ruby的标识符用于命名Ruby的变量、类、模块、方法等。Ruby的局部变量、实例变量、类变量、全局变量和常量都有一套固定的命名规则(注意:不是约定,而是规则),这样我们可以通过区分Ruby变量名的首位字符来确定它是局部变量、全局变量、实例变量、类变量还是常量。
通常有如下规则。
❑ 以“$”开头的变量:全局变量。
❑ 以“@”开头的变量:实例变量。
❑ 以“@@”开头的变量:类变量。
❑ 以小写字母或者下划线(_)开头的变量:局部变量。
❑ 以大写字母开头的变量:常量。
通常情况下,标识符的第二位字符以后可以是字母、数字或下划线,但有的内部变量名比较特殊,还可以是“$”或者“?”。通常,变量名长度只受内存大小的限制。
提示 Ruby的类、方法、常量、变量等都有一套严格的命名规则,通过这些命名规则,可以减少标识符命名错误。
注意,保留字不可用做类名和变量名,Ruby语言中的保留字如下。
如果在这些保留字前增加$,@,@@等前缀,则该标识符就不再是保留字,可以作为相应的全局变量、实例变量、类变量名使用。如在def方法定义后、或方法调用操作符“.”后出现这些保留字,则可确定它们是方法名,就可使用Ruby保留字作为方法名。
注意 Ruby的保留字不可作为变量名和类名,但可以作为方法名。
3.1.6 Ruby的垃圾回收
Ruby语言有一个重要特点就是垃圾自动回收,这也是Ruby相对于其他语言的优势之一。
当我们使用Ruby语言创建一个数组或者一个对象时,Ruby语言需要在内存里为该数组和对象分配一块内存,这块内存的分配无须程序员干预,由Ruby解释器负责执行。
一旦Ruby解释器为一个数组、一个对象分配了存储空间后,便开始记录该数组、该对象对所占用的内存区域的使用,一旦该数组、该对象使用完毕,便将其占用的内存回收。
在Ruby语言中,内存区的开辟、后续内存的释放和回收都是由Ruby解释器完成的,也就是说:Ruby语言的内存回收由Ruby解释器的设计者完成,从而将Ruby语言开发者从底层内存处理中释放出来。
通过Ruby语言的垃圾自动回收,避免了程序员自己手动控制内存回收,从而解决了两个最常见的应用程序错误:内存泄漏和无效内存引用。
垃圾回收也是保证Ruby程序性能的重要方面。可能所有读者都有一个经验:Windows系统使用时间久了,速度就有点慢,当我们重启机器,机器速度又快起来——这就是说,Windows运行的程序导致了内存泄漏,有一些程序在运行期间开辟了一些内存区,当程序运行结束后并没有把这些内存收回来。Ruby语言通过自动垃圾回收,避免了这种内存泄漏问题。
3.2 常量
Ruby语言中,常量也有自己的命名规则,因此我们能很方便地从名字上判断谁是常量,谁是变量。
3.2.1 Ruby是弱类型语言
Ruby是一种弱类型的语言,这意味着定义Ruby变量时无须指定该变量的数据类型,而且Ruby的变量可以一会儿是数字类型,一会儿是字符串类型,看如下代码。
# 定义一个数字类型变量 a = 5; # 重新将字符串值赋值给a变量 a = "Hello" puts a
运行上面的程序,没有任何问题,可以看到输出:
Hello
3.2.2 普通常量
因为Ruby是弱类型语言,因此定义常量时无须声明常量数据类型。Ruby语言要求常量以大写字母(A~Z)开头,常量的定义由赋值语句完成,看如下定义常量的代码。
class Constant # 定义一个常量 FOO = 5 puts FOO end
上面的程序定义了一个简单的类Constant。关于类定义的更深入介绍可以参考第5章。
提示:虽然Ruby只要求常量的首字母大写,但为了更好的可读性,我们通常把常量名的全部字母大写。
与其他编程语言不同,Ruby中若对已定义的常量进行赋值,不会出现错误,只是出现警告信息。
看如下代码:
class Constant # 定义一个常量 FOO = 5 # 重新为该常量赋值 FOO = 6 puts FOO end
上面的程序执行没有任何错误,只会引起一些警告。程序执行结果如下:
6 AssignConstant.rb:15: warning: already initialized constant FOO
从执行结果中可以看出,上面的常量被重新赋值成功,但会引起警告!
从上面的代码中可以看出,我们定义常量时,都是在类定义中进行的。事实上,常量只能在类、模块里定义,绝不能在方法里定义。
当然,我们也可以在顶层对象中定义常量。当我们直接在Ruby源文件中定义常量时,就等于在顶层对象中定义常量。如果我们一旦在顶层对象中定义了一个常量,我们就可以在任何地方引用该常量。
看如下代码:
FOO = 6 # 引用顶层常量 puts FOO class Test # 引用顶层常量 puts FOO end
当然,我们也可以在模块中定义常量。看如下代码:
# 定义一个模块 module A # 定义模块中的常量 FOO = "Hello Ruby" puts FOO end
上面的代码定义了一个简单的模块,在该模块内定义了一个名为FOO的常量。关于模块更详细的介绍,请参考第6章的内容。
如果我们声明一个常量,但不给该常量赋值,则会引发NameError异常。
3.2.3 常量的范围
常量的有效范围就是定义常量的类、模块的内部。因此,定义在该类、该模块里的方法、内部类和内部模块都可以访问到该常量,还可以在该类的子类中访问该常量。
看下面代码:
class Fruit # 定义类A里的一个FOO常量 FOO = "Hello" # B模块是类A的一个内部模块 module Vegetable # 内部模块内可以访问FOO常量 puts FOO end # 在类内部可以访问FOO常量 puts FOO # 在类内部的访问内访问FOO常量 def info puts FOO end # B类是类A的一个内部类 class Dog # 内部类内可以访问FOO常量 puts FOO end end # 让类Apple继承类Fruit class Apple < Fruit # 在类D中可以访问类A中常量 puts FOO end
从上面的代码中不难看出,常量的作用范围就是定义它的类的内部,如果我们定义一个顶层常量,则可以在任意地方引用它,这是因为我们定义的任何类、任何方法都已经处于该顶层对象的内部。
注意 当我们定义了一个顶层常量,就可以在任意地方引用该常量。
如果离开上面指出的范围访问常量,则可使用“::”操作符来限定。若想准确地访问顶层对象(Object类的实例)类中的常量,也需要使用“::”操作符,但操作符左边的限定类为空。
看如下代码:
class Test CONST = "Hello" end # 在常量的有效范围内引用常量
puts Test::CONST
如果一个内部类还有一个父类,且父类和外部类中包含了一个同名常量,则内部类将优先使用外部类中的常量,只有在外部类中找不到该常量时才会使用父类中的常量。看如下代码:
# 定义一个Fruit类 class Fruit CONST = 'Foo' end class Dog CONST = 'Bar' # 定义一个内部类:Apple,让Apple继承Fruit class Apple < Fruit # 优先输出外部类中的常量 puts CONST # 显式指定输出父类的常量 puts Fruit::CONST end end
但有一个例外,当我们定义一个顶层常量,或者直接定义一个类时,可以认为该类是该Object类实例的内部类,而顶层常量是顶层对象的常量——但这种情况下,并不优先引用顶层对象的常量,这就是说,顶层对象的优先级是很低的。
看如下代码:
# 定义一个Fruit类 class Fruit # 定义一个常量 CONST = 'Foo' end # 定义个顶层常量 CONST = "ABC" # Fruit类的子类 class Apple < Fruit # 不会输出顶层常量,而是输出Fruit类的CONST常量的值 puts CONST # 显式输出顶层变量 puts :: CONST end
从上面的代码中不难看出,虽然顶层常量可被当成Apple类的外部类的常量(可以认为我们定义的所有类都是Object类的内部类),但这里输出Apple类的父类Fruit类的常量。
提示 为了显式输出顶层常量,我们使用了“::”操作符,且该操作符的左边限定类为空。
当然,内部类、子类可以覆盖外部类、父类的常量,这种覆盖并不是对外部类、父类的常量重新赋值,而是重新定义一个新常量,所以不会引发警告。
下面的代码中,内部类的常量覆盖了外部类的常量。
class Fruit # 外部类常量 CONST = 'Foo' class Apple # 输出外部类常量 puts CONST # 重新定义一个常量,覆盖外部类常量 CONST = "ABC" # 输出新常量 puts CONST # 显式指定输出外部类常量 puts Fruit::CONST end end
上面的代码的执行结果如下:
Foo ABC Foo
从执行结果中可以看出,当在内部类中定义一个与外部类同名的常量时,仅仅是重新定义了一个常量,而不是为外部类的常量重新赋值。
与之类似的是,子类也可覆盖父类常量。看如下代码:
class Fruit # 父类常量 CONST = 'Foo' end class Apple < Fruit # 输出父类常量 puts CONST # 重新定义一个子类常量,该常量覆盖父类常量 CONST = "ABC" # 输出子类定义的新常量 puts CONST # 显式输出父类常量 puts Fruit::CONST end
上面的代码的执行结果与前一个程序的执行结果相同:在子类中定义与父类同名的常量并不是为父类常量重新赋值,而是重新定义一个常量。
3.2.4 类名和模块名常量
当我们定义一个类,或者定义一个模块时,系统在生成类对象、模块对象的时候,还会将该类对象、模块对象赋值给一个与该类、该模块同名的常数。从语法上讲,引用该类名、该模块名也就是引用该常量。
看如下代码:
# 定义一个Test类,会同时生成一个Test常量 class Test end # 输出Test常量,也就是Test类对象 puts Test # 定义一个Bee模块 module Bee # 在模块内部定义一个类,定义类的同时会生成一个Bird常量 class Bird end # 直接输出Bird常量 puts Bird end # 输出Bee常量 puts Bee # 在Bird变量的有效范围内输出Bird变量,使用“::”操作符 puts Bee::Bird
上面的程序的执行结果如下:
Test Bee::Bird Bee Bee::Bird
3.3 变量及变量的作用域
变量也是Ruby语言中一个重要概念,Ruby语言提供了多种变量类型,如局部变量、全局变量和实例变量。下面详细介绍Ruby变量的概念和用法。
3.3.1 变量的概念
在程序运行期间,系统可以为程序的运行分配一块内存区,用于存储程序运行期间的各种数据。系统所分类的内存需要一个名称来标识,如果这个内存区的数据是可以改变的,则该内存区的标识就是变量。
因此,变量实际对应了程序运行中的一块内存,这个内存区的值是可变的。通过使用变量,程序员就可以获取内存中的值,也可以改变该内存区的值,但这个过程对程序员是透明的,无须程序员干预。
当我们向变量对应的内存中放入一个值时,就对应于给该变量赋值;当我们通过变量名来取得该变量值时,就等于取出该变量对应内存里的值。
3.3.2 局部变量
Ruby语言里的局部变量应该以小写字母(a~z)或下划线(_)开头。
注意 除此之外,Ruby里的方法调用也是以小写字母或下划线开头的,而且Ruby调用的方法如果没有参数,则可以省略方法后面的括号,因此读者必须小心分辨到底是方法调用还是局部变量。
局部变量可以被定义在类内部、模块内部或者方法内部,当我们对一个以小写字母开头的变量进行赋值时,就表明声明了一个局部变量。该局部变量的作用域仅仅属于该类、该模块或者该方法,一旦离开了该类、该模块或该方法,则该局部变量自然消失。
# 定义一个类 class Apple # 定义类里的局部变量 local = "属于类的局部变量" puts local end # 定义一个方法 def info # 定义方法里的局部变量 local = "属于方法的局部变量" puts local end # 调用方法 info; # 定义一个模块 module Vegetable # 定义模块里的局部变量 local = "属于模块的局部变量" puts local end
上面的代码定义了三个局部变量。这三个局部变量有相同的名字,但这完全没有任何问题,因为它们都是局部变量,仅仅在定义它们的方法、类和模块内有效。
除此之外,我们还可以定义一个顶层局部变量,该顶层变量则会对整个程序都有效。
提示 根据前面的知识,我们定义类、方法或变量时,等同于在顶层对象中定义,因此当我们定义一个顶层局部变量时,则该变量的作用范围就是该顶层对象——也就是整个程序。
局部变量不可在内部类和子类中访问。看如下代码:
class Fruit # 外部类定义一个局部变量 local = "Hello" class Apple # 在内部类中访问外部类的局部变量 puts local end end
执行上面的代码出现如下结果:
内部类访问外部类的局部变量.rb:16: undefined local variable or method `local' for Fruit::Apple:Class (NameError)
从上面的执行结果中可以看出,内部类不可访问外部类的局部变量;类似地,子类也不可访问父类的局部变量,因此如下程序将出现与前面程序相同的错误。
# 定义一个父类 class Fruit # 父类的局部变量 local = "Hello" end # 定义一个子类 class Apple < Fruit puts local end
不仅如此,类里的方法也不可访问该类里定义的局部变量!因此,即使我们定义一个顶层变量,该顶层变量的作用域是整个程序,但在类定义内、方法定义内或者模块定义内依然无法访问该变量。看如下程序:
# 定义一个顶层局部变量 local = "Hello" class Apple # 类内部无法访问顶层变量 puts local end
上面的程序将出现和前面相同的错误,因为Apple类可以视为顶层对象(Object类的实例)的内部类,而内部类是无法访问外部类的局部变量的。
在声明局部变量的类、模块和方法内,局部变量从声明该变量处开始生效,随该方法定义、类定义或模块定义结束而结束。看如下代码:
class Test # 无法访问到local变量 puts local # 定义一个局部变量 local = "Hello" end
执行上面的程序,将出现如下错误:
先定义后使用.rb:15: undefined local variable or method `local' for Test:Class (NameError)
虽然上面的程序中定义了一个名为local的局部变量,但因为该局部变量必须在定义后才有效,因此如果我们在定义该变量之前访问该变量,则该变量依然无效。
注意 局部变量必须先定义,然后才可使用。
从上面的出错提示中可以看出,系统认为无法识别一个局部变量或方法:local。这表明Ruby解释器也分不清local到底是一个局部变量,还是一个方法!因此,如果引用一个尚未被声明的标识符,Ruby解释器会试图去执行一个没有参数的方法调用。
除此之外,局部变量还可以在块范围内声明,则该变量的作用范围为从变量声明处开始,到该块结束处结束。看如下代码:
2.times { # 判断v变量是否存在——其中defined?是一个运算符,用于判断某个变量是否已经定义 puts defined?(v) # v变量从声明处开始生效 v = 1 # 输出v变量 puts v }
上面的代码两次迭代执行代码块,每次代码块执行时局部变量v都被重新定义,不会互相影响。因此,看到如下执行结果:
nil 1 nil 1
从上面的执行结果中可以看出,代码块每次访问v局部变量时,总是返回nil,这表明该变量还未定义。
但也有例外。如果代码块已经变成过程对象,则局部变量将一直持续到该过程对象终结为止。若多个过程对象引用同一个块,局部变量将被这些对象所共享。关于块的知识,请参考第4章的内容。
3.3.3 全局变量
Ruby中的全局变量是以“$”开头的变量,可以在任何地方访问。通常,我们应该尽量避免使用全局变量,因为使用全局变量会引起各模块之间的高耦合。
看下面的程序:
# 定义一个全局变量 $myGlobal = "Hello" class Test # 在类内部访问全局变量 puts $myGlobal end def info # 在方法内部访问全局变量 puts $myGlobal end info
上面定义了一个顶层的全部变量,该全局变量既可以在类内访问,也可以在方法内访问——这就是全局变量的含义。
全局变量无须变量声明,如果引用尚未初始化的全局变量,其值为nil。
全局变量既可以在类定义中定义,也可以在模块定义中定义,甚至可以在方法中进行定义。
虽然说全局变量的作用域是全局的,但实际上全局变量的作用范围是从全局变量的定义语句开始,直到程序结束。
def info # 在方法内部访问全局变量 puts $myGlobal end info class Test # 在类内部定义一个全局变量 $myGlobal = "Hello" end info puts "直接访问 " + $myGlobal
上面当我们在方法定义中访问全局变量时,系统根本不理会$myGlobal变量是否有值,只有等到执行该方法时才会判断该全局变量是否有值。因此上面的程序的执行结果是:
nil Hello 直接访问Hello
从上面的代码中可以看出,第一次调用info方法时,我们发现$myGlobal为nil,这表明该全局变量还未被声明。当类定义执行结束后,$myGlobal全局变量被赋值了,再次执行info方法时,就可以正常输出$myGlobal变量值。
把全局变量定义在方法中时有点奇怪,只有等到方法被调用后,全局变量的声明才会生效。看如下代码:
def info # 方法内部定义全局变量 $myGlobal = "Hello" # 在类内部访问全局变量 puts "方法内部的输出 " + $myGlobal end class Test1 # 在类内部访问全局变量——因为在调用info方法之前,故此处输出nil puts $myGlobal end # 调用方法 info class Test2 # 在类内部访问全局变量 puts "方法被调用之后输出的全局变量 " + $myGlobal end
执行上面的程序,可以看到如下执行效果:
nil 方法内部的输出 Hello 方法被调用之后输出的全局变量 Hello
从上面的代码中可以看出:当第一次在Test1类里输出$myGlobal全局变量时,还没有调用info方法(定义全局变量的方法),全局变量$myGlobal还未初始化,因此输出nil;当第二次调用在Test2类里输出$myGlobal全局变量时,已经调用了info方法,故可以访问到$myGlobal的值。
提示 上面的效果其实是由于动态语言特征造成的。对于动态语言而言,当我们定义一个方法时,该方法根本不会被解释,只有当真正调用该方法时,该方法才会被解释。
Ruby语言提供了一系列的全局变量,这些全局变量都有其特定的含义,下面是Ruby语言中常用的全局变量,以及对应的含义。
❑ $!:最近一次的错误信息。
❑ $@:错误产生的位置。
❑ $_:gets最近读的字符串。
❑ $.:解释器最近读的行数。
❑ $&:最近一次与正则表达式匹配的字符串。
❑ $~:作为子表达式组的最近一次匹配。
❑ $n:最近匹配的第n个子表达式(和$~[n]一样)。
❑ $=:是否区别大小写的标志。
❑ $/:输入记录分隔符。
❑ $\:输出记录分隔符。
❑ $0:Ruby脚本的文件名。
❑ $*:命令行参数。
❑ $$:解释器进程ID。
❑ $?:最近一次执行的子进程退出状态。
3.3.4 实例变量
Ruby要求实例变量以“@”符号开头。与静态语言不同的是,Ruby语言是一门动态语言,因此Ruby的实例变量无须声明,每个实例变量都是在第一次出现时动态加入对象。
因此,Ruby的实例变量通常在方法中定义类声明——当在方法里声明实例变量时,该实例变量实际上属于该方法所在类的实例,而不是属于该方法。
看下面的程序:
class Apple # 定义第一个方法 def info1 # 输出实例变量@a puts @a end # 定义第二个方法 def info2 # 为实例变量赋值 @a = "Hello"; end end # 创建Apple类实例 apple = Apple.new # 调用方法 apple.info2 apple.info1
从上面的方法中不难看出,虽然定义第一个方法info1时,该方法内输出的@a实例变量还没有被定义,但这不是问题!因为Ruby的方法只有在被调用时才会真正生效,所以当调用apple.info2方法时,执行了@a = "Hello"代码,这行代码动态地为apple实例增加了一个@a实例变量。
注意 Java创建对象时一次为该对象的所有实例变量都分配了相应的内存空间;但Ruby语言里的对象完全是动态的,创建对象时该对象没有任何实例变量,直到执行到为实例变量定义时,该对象才动态增加该实例变量。
类似于全局变量,实例变量无须显式声明即可使用。如果使用一个未定义的实例变量,则该实例变量的值为nil。
与局部变量不同是的:实例变量的生存范围是与该对象的生存范围相同的,只要该类的对象存在,则该对象里的实例变量将一直存在;但局部变量则随方法的消亡而消亡(除非使用闭包)。
实例变量的访问范围总是private,这意味着在类定义内对实例变量的赋值和读取没有限制;如果希望在类外部访问到实例变量的值,则可以通过方法来访问。看如下代码:
class Apple # initialize是一个特殊方法,其实就是一个构造器,在创建Apple实例时自动调用 def initialize(name) # 当执行构造器时添加一个实例变量 @name=name end # 定义一个与name实例变量同名的方法,该方法返回实例变量@name def name return @name end # 定义一个名为name=的方法,该方法用于为@name实例变量赋值 def name=(att) @name=att end end # 创建Apple的实例 a = Apple.new("红富士") # 通过name方法来访问@name实例变量 puts a.name # 通过调用name=方法来为@name实例变量设置值 a.name = "青苹果" puts a.name
执行上面的代码,看到如下执行结果:
红富士 青苹果
从上面的代码中不难看出,Ruby语言里的实例变量可以被定义成一个属性。实际上,Ruby为定义类里的属性提供了更简单的方法:通过Module的方法定义属性。
这种用法并不是Ruby语法的一部分,它只是通过使用Module类里几个方法来自动创建属性。
看如下代码:
class Apple # 使用attr创建了一个@name实例变量,且为该变量创建了读取方法 attr :name # 使用attr创建了一个@color实例变量,且为该变量创建了读取和设值方法 attr :color ,true # 使用attr_reader创建了一个@weight实例变量,且为该变量创建了读取方法 attr_reader :weight # 使用attr_ writer创建了一个@size实例变量,且为该变量创建了设值方法 attr_writer :size # 使用attr_accessor创建了一个@info实例变量,且为该变量创建了读取和设值方法 attr_accessor :info end a = Apple.new # @name属性只可访问 puts a.name # @color属性既可设值,也可访问 a.color = "红色" puts a.color # @weight属性只可访问 puts a.weight # @size属性只可设值 a.size = 0.2; # @info属性既可设值,也可访问 a.info = "可口的水果" puts a.info
上面的代码的执行结果如下:
nil 红色 nil 可口的水果
从上面的代码中不难看出,通过Module里的4个方法attr,attr_reader,attr_writer和attr_accessor来创建实例变量及其对应的访问方法更加简单。
实际上,上面的Apple类相当于如下代码。
class Apple # 定义name的访问方法 def name return @name end # 定义color的访问方法 def color return @color end # 定义color的设值方法 def color=(att) @color = att end # 定义weight的访问方法 def weight return @name end # 定义size的设值方法 def size=(att) @size = att end # 定义info的访问方法 def info return @info end # 定义info的设值方法 def info=(att) @info = att end end
除此之外,我们也可以在模块中定义实例变量,在模块中的定义变量的目的并不是用于创建模块的实例(模块是无法创建实例的),而是用于将该实例变量混入其他类中。
提示 用于混入的知识请参看第6章。
在如下代码中,我们在模块里定义了一个实例变量,并将该实例变量混入其他类中。
# 定义一个Vegetable模块 module Vegetable //在模块里使用attr定义一个@color实例变量 attr :color ,true end # 定义一个Apple类 class Apple # 混入Vegetable的实例变量 include Vegetable end a = Apple.new a.color = "红色" puts a.color
从上面的程序中可以看出,Apple类获得了Vegetable模块中的@color实例变量,这就是在模块中定义实例变量的作用。
从上面的代码中不难看出,虽然实例变量是属于类的,但大部分时候我们都是在该类的方法里定义实例变量,而不是在类中定义实例变量。
在类范围内定义的实例变量属于类对象(Class对象,当我们定义一个类时,该类定义会返回一个Class对象,该对象会保存到一个与该类同名的常量里),类范围内定义的实例变量是Class对象(对象名就是该类的类名)的实例变量,实例变量永远都不能暴露成为public访问控制,只能通过方法暴露,因此类范围的实例变量反而没有太大的意义。
看如下代码:
class Apple # 定义一个类范围的实例变量 @color = "红色" def info # 方法内无法访问类范围的实例变量 puts @color end # 类内才可以访问类范围的实例变量 puts "类范围的实例变量 " + @color end a = Apple.new a.info
上面的代码的执行结果如下:
类范围的实例变量 红色 nil
从上面的执行结果可以看出:在方法内访问类范围的实例变量时结果为nil,这表明方法内无法访问类范围的实例变量。
如果我们希望暴露类范围的实例变量,可以通过定义一个类方法来暴露它。看如下的代码:
class Test # 定义一个类范围的实例变量@name,该实例变量属于Class类的实例(该实例名为Test) @name = "孙悟空"; def name=(value) # 定义属于Test实例的实例变量 @name = value end # 定义一个类方法 def Test.name # 类方法的调用者是Test类本身,也就是Class类的实例 # 因此,此处访问的是类范围的实例变量 @name end end # 调用Test类的name方法 puts Test.name
执行上面的代码,看到如下结果:
孙悟空
从上面的运行结果可以看出,类范围的实例变量可以通过类方法来暴露。
3.3.5 类变量
Ruby中变量名以“@@”开头的变量是类变量,类变量从第一次开始赋值的地方开始生效,它既可以在类定义中定义,也可以在模块中定义,还可以在方法中定义。
看下面的代码,它在类定义中定义一个类变量,并在类、方法中分别访问该类变量。
class Apple # 在类定义中定义一个类变量 @@color = "红色" # 直接访问类变量 puts @@color def info # 在方法中访问类变量 puts "苹果的颜色是 " + @@color end end Apple.new.info
在类的定义中定义类变量,可以在类的特殊方法、实例方法等处对类变量进行引用或赋值。
与实例变量不同的是,类变量必须先定义,然后才可使用。如果引用一个从未定义的类变量,将会引发一个uninitialized class variable异常。
当然,也可在方法中定义类变量,但在方法中定义类变量后,只有当该方法被调用之后,该类变量才会生效。看下面代码:
class Apple def color=(att) # 定义类变量 @@color = att end def color # 访问类变量 @@color end # 此处的@@color依然不可用 puts defined?@@color end # 创建两个Apple实例 a1 = Apple.new; a2 = Apple.new # 依次对两个Apple实例的@@color变量赋值 a1.color = "红色" a2.color = "绿色" # 调用a1的方法来访问@@color变量 puts a1.color
上面的代码的执行结果是:
nil 绿色
从上面的代码中可以看出:当我们在类中直接判断@@color类变量是否存在时,发现该变量并不存在,因为在上面只是定义了两个方法,而且@@color变量是在方法定义中定义的——只有当该方法被调用后,该类变量才有效。
除此之外,读者还可以发现,虽然我们使用a1.color = "红色"为a1实例的@@color赋值为“红色”,但当我们使用puts a1.color来访问@@color类变量时,发现a1实例的@@color类变量是“绿色”!这是为什么呢?
这是因为类变量的特殊性:一个类、其子类以及它们的实例的同名类变量共享了同一块内存区域,也就是说,一个类、其子类以及它们的实例的同名类变量都引用同一个变量。所以当我们使用a2.color = "绿色"代码时,虽然表示为a2的@@color变量赋值,其实也改变了a1的@@color变量。
当然,类变量也可在模块中定义,所有混入该模块的类,共享该模块中定义的类变量(模块变量)。看如下代码:
module Vegetable # 在模块内定义一个类变量 @@foo = 1 end class Apple include Vegetable # 共享Vegetable模块里的@@foo类变量 puts @@foo += 1 end class Banana include Vegetable # 共享Vegetable模块里的@@foo类变量 puts @@foo += 1 end
从上面的代码中可以看出,Apple和Banana两个类都混入了Vegetable模块,故共享该模块的@@foo类变量,因此上面的程序在三个地方访问的@@foo变量引用的是同一个内存区,因此三处@@foo变量的值是连续的。
从某些角度来看,类变量和常量有些相似,但还是存在不少差别。类变量与常量的区别如下:
❑ 类变量可以重复赋值(但对常量赋值会发出警告)。
❑ 类变量默认是protected的,不能在类外部直接引用(在继承类中则可以引用或赋值)。
从某些角度来看,类变量和实例变量也有些相似。类变量与实例变量的区别如下:
❑ 在类范围内定义的类变量,可以在该类的方法中访问,但实例变量则不行。
❑ 类变量可在子类中引用或者赋值,但实例变量则只可在类范围内直接引用或赋值。
3.3.6 伪变量
在Ruby语言中,除了普通的变量之外,还有一种叫做伪变量的特殊变量,这些伪变量可方便地访问Ruby环境信息。Ruby语言包含如下伪变量。
❑ self:当前的执行主体,既可以是方法,也可以是类。
❑ nil:NilClass类的唯一实例。
❑ true:TrueClass类的唯一实例。
❑ false:FalseClass类的唯一实例。nil和false表示“伪”。
❑ __FILE__:当前源文件名。
❑ __LINE__:当前源文件中的行号。
下面的程序输出当前的Ruby文件名,以及执行到的行号。
class Apple # 将输出Apple puts self # 输出当前文件名 puts __FILE__ # 输出当前行数 puts __LINE__ end
注意 伪变量的值是只读的,如果试图对伪变量进行赋值,则会引发语法错误。
3.4 基本数据类型
虽然Ruby语言是一门弱类型的语言,但它依然提供了丰富的基本数据类型。但与Java不一样的是,Ruby里的基本数据类型变量也是一个对象,系统有与之对应的类,基本数据类型变量也可以调用该类里定义的实例方法。
3.4.1 弱类型语言
Ruby是一门弱类型语言,如所有弱类型的语言一样,使用Ruby的变量无须声明类型,当为一个变量赋值时,就是定义了该变量。当我们定义该变量时,该变量的数据类型就是被赋值的数据类型。
下面的代码定义了三个变量,因为三个变量所赋的值不一样,所以三个变量的数据类型也互不相同。
class Apple # 定一个名为a的变量,该变量的值为整数 a = 5 # 定一个名为b的变量,该变量的值为字符串 b = "Hello" # 定一个名为c的变量,该变量的值为浮点数 c = 2.3 puts a.class puts b.class puts c.class end
执行上面的代码,可以看到如下结果:
Fixnum String Float
不仅如此,Ruby作为弱类型的语言,还具有弱类型语言的一个特征:同一个变量的数据类型可以变化。下面代码中变量a的数据类型可以改变。
class Apple # 定一个名为a的变量,该变量的值为整数 a = 5 puts a.class # 重新为a赋值,所赋值为字符串 a = "Hello" puts a.class end
执行上面的代码,可以看出a变量的数据类型开始是整数,后来又是字符串。
3.4.2 数值型
Ruby的数值型包含整数和浮点数。整数包括Fixnum和Bignum两种类型,其中Fixnum可以容纳比一个物理字长少一位的整数。当一个整数超过Fixnum范围时,它会自动转换成Bignum类型,而Bignum类型的范围只能被物理内存所限制。
如果对一个Bignum类型的整数进行算术计算,最后的结果使得它可以用一个Fixnum保存,那么结果就会以一个Fixnum对象返回。
注意 Ruby里的基本数据类型也是一个对象,例如,5就是Fixnum类的对象。因此我们求一个数字的绝对值是这样写的:aNumber.abs,而不是abs(aNumber) ——这也是Ruby面向对象特征的体现。
看下面的代码,代码里两个整数的大小只差1,但已经代表不同数据类型。
class Test # 定一个名为a的变量,值为2的30次方减1 a = 1073741823 puts a.class # 定一个名为a的变量,值为2的30次方 b = 1073741824 puts b.class end
从上面的代码中可以看出,通常当定义一个变量的值为大于或等于2的30次方的整数时,系统自动将该变量设为Bignum类型;如果小于该值,则设置为Fixnum类型。
除此之外,整数前面可以带一个可选的符号标记(用以指定是正数,还是负数)、一个可选的进制标记符(0代表八进制,0x代表十六进制,0b代表二进制),然后跟一串相应进制的数字串。
如果我们在数值中使用下划线,则数值中的下划线将被忽略。看如下代码:
class Apple # 二进制数,其值为12 a = 0b1100 puts a # 八进制数,其值为9 b = 011 puts b # 十六进制数,其值为17 c = 0x1_1 puts c end
上面的代码中分别定义了三个变量a,b和c,分别使用了二进制、八进制和十六进制格式,并在数值中使用了下划线(下划线被忽略)。
前面已经指出,所有整数实际上是Fixnum或Bignum类的实例,因此完全可以执行这两个类的方法,例如支持一些有用的迭代器(iterators),像times,upto,downto和step,分别用于整数的向上迭代、向下迭代以及指定步长迭代(用于取代传统的for循环)。
看如下代码:
class Test # 迭代输出3次 3.times{ print "X " } puts "\n" # 从1向上迭代到5 1.upto(5) { |i| print i, " " } puts "\n" # 从99向下迭代到95 99.downto(95) { |i| print i, " " } puts "\n" # 从50指定步长迭代到80 50.step(80, 5){ |i| print i, " " } end
执行上面的代码,使用整数的迭代器功能,看到如下执行结果。
X X X 1 2 3 4 5 99 98 97 96 95 50 55 60 65 70 75 80
除此之外,我们还可以通过在一个ASCII字符串前添加“?”(英文问号)来取得它对应的整数值,例如?a将返回97。但需要指出的是,“?”后面是一个ASCII字符,不是字符串,因此不需要使用引号,也不能有多个字符。
看如下代码,下面的代码获取了大写A和小写a对应的整数值。
class A puts ?a puts ?A end
上面的代码将依次输出97,65。
不仅如此,如果我们希望获取一些特殊字符的ASCII值,则可以采用转义字符。例如,取得回车符对应的ASCII值,使用“?\n”即可;如果希望取得某个字符与“Ctrl”键或“Alt”键组合后对应的ASCII值,则可以采用“?\C-x”或者“?\M-x”的形式,其中x就是被组合的字符,如果“Ctrl”和“Alt”同时与指定键组合,则可以采用“?\M-\C-x”的形式。
如下代码获取了部分特殊字符对应的ASCII值。
class Apple puts ?a # => 97 puts ?A # => 65 a = ?\\ puts a # => 92 b = ?\n puts b # => 10 c = ?\C-a puts c # => 1 d = ?\M-a puts d # => 225 e = ?\M-\C-a puts e # => 129 end
提示 当键盘上某个键和“Ctrl”或“Alt”进行组合时,实际上有固定的算法:一个字符“ch”和“Ctrl”键组合后的数值为ch&0x9f,一个字符“ch”和“Alt”键组合后的数值为ch|0x80——其中“&”和“|”运算符分别是按位与和按位或,后面有关于这两个运算符的详细介绍。
一个带有小数点的数字串(也可以带指数)被认为是Float对象,大小和物理机器上双精度类型一样。如下形式的数字串都会被当成浮点数处理:
12.34 # => 12.34 -1234e2 # => -12.34 1234e-2 # => 12.34
但值得指出的是,当使用科学计数法来表示浮点数时,如果e的前面部分是一个整数,则要么完全省略小数点,要么在小数点后增加0。如下所示:
5e2 #=> 500 5.0e2 #=> 500
但不可写成如下形式:
5.e2 #=> 出错
对于上面的形式,Ruby会理解为5是Fixnum类的对象,该对象调用e2方法,但e2方法不存在,故出现错误。
3.4.3 字符串
Ruby语言里的字符串是String类的实例,因此任意的字符串变量、常量都可调用String类的实例方法,String可能是Ruby最大的内建类了,它有超过75个标准方法,因此字符串是一个非常重要的类。
字符串通常通过字符串常量来建立,Ruby提供了好几种方法来建立字符串常量,每种方法都产生一个String类的对象,通常通过包括在引号里的字符序列来建立字符串。
Ruby允许使用单引号(' ')或双引号(" ")来创建字符串,这种单引号或双引号就是字符串的分隔符,下面的代码分别采用单引号和双引号来生成字符串。
class Test # 定义一个名为a的字符串变量 a = "Hello" # 定义一个名为b的字符串变量 b = 'Hello' # 判断两个变量是否相等 puts a == b end
上面的代码的执行结果是true。
但单引号和双引号括起来的字符串并不完全相同,不同的引号决定了字符串中的替代程度。一般而言:单引号括起来的字符串里,只进行最少替换:两个连续的反斜杠会被一个反斜杠取代,一个反斜杠后面跟一个单引号会变成一个单引号;而双引号括起来的字符串里,会进行更多替换。
下面显示了双引号所包括的字符串所进行的替换。
❑ \a:替换成Bell声所对应的符号。
❑ \nnn:nnn表示一个八进制数值,替换成该八进制数所对应的字符。
❑ \b:替换成退格键所对应的字符。
❑ \xnn:nn表示一个十六进制数值,替换成该十六进制数所对应的字符。
❑ \e:替换成Escape键所对应的字符。
❑ \cx:替换成“Ctrl + x”组合键所对应的字符,其中x可以是任意键所对应的字符。
❑ \C-x:替换成“Ctrl + x”组合键所对应的字符,其中x可以是任意键所对应的字符。
❑ \n:替换成一个空行。
❑ \M-x:替换成“Alt + x”组合键所对应的字符,其中x可以是任意键所对应的字符。
❑ \r:替换成一个回车。
❑ \M-\C-x:替换成“Ctrl+ Alt + x”组合键所对应的字符,其中x可以是任意键所对应的字符。
❑ \s:替换成一个空白字符。
❑ \t:替换成一个“Tab”键所对应的字符。
❑ \v:替换成一个垂直“Tab”键所对应的字符。
❑ #{expr}:替换成表达式expr所对应的值,这个表达式可以是全局变量、类变量或者实例变量等。
看如下代码,下面的代码分别使用单引号和双引号括起来一些特殊的字符串。
class Test # 对比用双引号和单引号括起来的字符串的差别 puts '\a' puts "\a" # 替换八进制数141所对应的字符 puts "\141" + '\141' # 替换垂直Tab所对应的字符 puts "\v" + '\v' # 替换十六进制数61所对应的字符 puts "\x61" + '\x61' # 替换“Ctrl + a”所对应的字符 puts "\ca" + '\ca' $global = "Ruby" # 替换全局变量 puts "Hello,#{$global}" puts 'Hello,#{$global}' end
上面的代码的执行结果为:
\a a\141 # 此处的空行就是由"\v"替换而出 \v a\x61 \ca Hello,Ruby Hello,#{$global}
从上面的执行结果不难看出,对于用双引号括起来的字符串,Ruby会执行更多替换,对单引号括起来的字符串,则执行较少的替换。
3.4.4 字符串的%表示法
如果我们希望一个字符串是由多行字符组成,则可以使用“%”表示法。“%”表示法一共有三种形式:
%/stuff/——前面形式中的 / 是分隔符,可以是斜线、括号等字符。stuff代表一个多行字符串,这种形式类似于双引号括起的字符串,会进行很多替换。
%Q/stuff/——与%/stuff/用法完全相同。
%q/stuff/——前面形式中的 / 是分隔符,可以是斜线、括号等字符。stuff代表一个多行字符串,这形式类似于单引号括起的字符串,只进行很少替换。
看如下代码,下面的代码使用%Q/stuff/和%q/stuff/来生成多行字符串,多行字符串里都包含了一个#{expr}的形式,只有使用%Q/stuff/才会替换#{expr}为表达式expr的值。
class Test # 定义一个全局变量 $global = "Ruby" # 使用%Q/stuff/来生成多行字符串 b = %Q/ Hello, Ruby Ruby is Simple Language Ruby On Rais #{$global}/ # 使用%q/stuff/来生成多行字符串 a = %q{ Hello, Ruby Ruby is Simple Language Ruby On Rais #{$global}} puts a puts b end
上面的代码的执行结果为:
Hello, Ruby Ruby is Simple Language Ruby On Rais #{$global} Hello, Ruby Ruby is Simple Language Ruby On Rais Ruby
除此之外,Ruby还提供了一种多行字符串的表示方式。
3.4.5 多行字符串的字面值
Ruby还提供了一种多行字符串的表示形式,这种语法也被称为Here Document。Here Document的最常用语法格式如下:
<<标识符 stuff 标识符
在上面的语法格式中,两个左箭头符号是必需的,而且是固定的,标识符可以是任意的。中间的stuff就是需要定义的多行字符串。如下程序使用Here Document定义了一个字符串。
$global = 123 # 使用Here Document定义了一个字符串 puts <<HERE 这是一个多行文本内容 使用here文档语法 Sum = #{$global + 1} HERE
运行上面的程序,看到如下运行结果:
这是一个多行文本内容 使用here文档语法 Sum = 124
一个Here Document由“<<标识符”开始,直到“标识符”结束,这中间的所有多行字符串就是Here Document格式的字符串。通常,我们要求结束的“标识符”必须顶格(结束标识符必须从第一列开始)。但如果在开始标识符的“<<”之后加一个减号,就可以缩进结束标识符。如下面的代码所示:
# 如果要缩进结束标识,则必须使用“<<-标识符”开始 puts <<-HERE 这是一个多行文本内容 使用here文档语法 Sum = #{$global + 1} HERE
除此之外,还可以把“<<”后的标识符用不同的引号(单引号或双引号)括起来,用以确定不同的替换程度。看如下两段代码的对比:
$global = 123 # 将HERE标识符用双引号括起来,表示较大程度的替换 puts <<"HERE" 这是一个多行文本内容 使用here文档语法 Sum = #{$global + 1} HERE # 将HERE标识符用单引号括起来,表示较小程度的替换 puts <<'HERE' 这是一个多行文本内容 使用here文档语法 Sum = #{$global + 1} HERE
上面的两个Here Document分别使用了双引号和单引号来括起“<<”后的标识符,其中双引号指定较大程度的替换,而单引号指定较小程度的替换。运行上面的程序,看到如下运行结果:
这是一个多行文本内容 使用here文档语法 Sum = 124 这是一个多行文本内容 使用here文档语法 Sum = #{$global + 1}
从上面运行结果中可以看出,使用单引号时不会替换#{$global + 1}表达式。
提示 使用双引号将 << 后的标识符括起来和不使用引号的效果是完全相同的。
除此之外,我们还可以在一行中使用多个Here Document。看如下代码:
$global = 123 # 使用Here Document定义了一个字符串 a,b = <<FIRST,<<SECOND 第一个多行字符串的内容 Hello Ruby! #{$global + 1} FIRST 第二个多行字符串的内容 Hello Rails! #{$global + 1} SECOND puts a puts b
执行上面的程序,执行结果如下:
第一个多行字符串的内容 Hello Ruby! 124 第二个多行字符串的内容 Hello Rails! 124
有的读者对XML可能比较熟悉,对上面的语法有点不习惯(因为上面用法不符合XML的合理嵌套法则),但我们必须承认,这是Ruby的Here Document语法,必须结构化文档。
实际上,上面语法自有Ruby的解释:Ruby语言使用Here Document语法时,并不把“<<标识符”和结束标识符当成一个整体!它的语法含义是,“<<标识符”代表一个多行字符串内容,该多行字符串内容是从“<<标识符”的第二行开始,直到结束标识符为止——从某种程度上来看,“<<标识符”就代表了这个多行字符串。
看如下程序:
# 定义一个info方法,该方法有两个参数 def info(arg1 , arg2) puts "这是第一个参数 " + arg1 puts "这是第二个参数 " + arg2 end # 为info方法的第二个参数传入多行字符串 info("Ruby" , <<EOS) 这是一个多行字符串, Hello Ruby EOS
上面调用info方法时,<<EOS就是作为一个参数,该<<EOS的值是从第二行到EOS标识符的全部内容。运行上面的程序,看到如下结果:
这是第一个参数 Ruby 这是第二个参数 这是一个多行字符串, Hello Ruby
下面代码也一样体现了这种语法含义,“<<标识符”就代表一个字符串,而“<<标识符”所实际代表的字符串需要紧跟在“<<标识符”后面定义。看如下程序:
puts <<EOS.insert(4, " 被插入的内容 ") Ruby On Rails FrameWork Hello Ruby EOS
上面的程序看起来就是直接调用<<EOS的insert方法,这是允许的,因为<<EOS就代表一个字符串。上面的程序执行结果如下:
Rub 被插入的内容 y On Rails FrameWork Hello Ruby
但必须注意的是:使用“<<标识符”代表一个字符串时,该字符串定义必须紧随其后,也就是说“<<标识符”第二行的内容就是“<<标识符”所代表的字符串内容。看如下程序:
# 定义一个info方法,该方法有两个参数 def info(arg1 , arg2) puts "这是第一个参数 " + arg1 puts "这是第二个参数 " + arg2 end # 使用Here Document语法来定义第一个参数值 info(<<EOS , "Hello") 这是一个多行字符串, Hello Ruby EOS
上面的程序没有任何问题,当我们使用<<EOS来代表一个字符串后,从该标识的下一行立即开始定义该字符串内容。但如果将函数调用改为如下形式:
info(<<EOS , "Hello") 这是一个多行字符串, Hello Ruby EOS
则程序是错的,因为<<EOS的下一行没有定义<<EOS所代表的字符串。
3.5 范围
范围(Range)是多个枚举值的一种简单表示形式。例如1月到12月,我们可以采用枚举的方法来定义这个概念。但实际上,我们只需要定义2个边界,而中间的2月、3月……则无须定义。通过这种只定义边界的方式来定义枚举的形式就是范围。
创建作为序列的枚举有两种语法。
❑ 值1 .. 值2:定义一个从值1到值2的范围,包括值1和值2,也包括值1和值2中间的值。
❑ 值1 ... 值2:定义一个从值1到值2的范围,包括值1,也包括值1和值2中间的值,但不包括值2。
例如下面的两种形式:
1..10 #=>1,2,3,4,5,6,7,8,9,10
1...10 #=> 1,2,3,4,5,6,7,8,9
Ruby中的范围也是一个对象,是Range类的实例。Ruby不像早期Perl那样,把序列保存在一个内部列表中。比如,1..100000在Ruby中是一个Range对象,包括两个指向Fixnum对象的引用。
因为范围是Range类的实例,因此可以调用Range类的如下常用方法。
❑ to_a():将该范围转换成一个数组。
❑ include?(tagetValue) / ===(tagetValue):判断该范围是否包含了targetValue。
❑ min() / begin() / first():求该范围的最小值(第一个值)。
❑ max() / end() / last():求该范围的最大值(最后一个值)。
看如下程序:
class Test # 使用 ... 定义范围,则该范围不包括终点 a = 2...10 # 将范围转换成数组 print a.to_a puts "\n" # 判断某个值是否在某个范围内 puts a.include?(5) # 求范围的最小值 puts a.min # 求范围的最大值 puts a.max end
上面的程序的执行结果是:
23456789 true 2 9
除此之外,范围还可以使用如下三个常用的迭代器方法。
❑ reject:将该范围转换成一个数组,并剔除掉满足条件的元素。
❑ select:将该范围转换成一个数组,只选择满足条件的元素。
❑ each:迭代范围里的每个元素。
看如下程序:
class Test # 使用 ... 定义范围,则该范围不包括终点 a = 2...10 # 将范围转换成数组,且排除满足后面条件的元素 print a.reject {|i| i < 5 } puts "\n" # 将范围转换成数组,且只选择满足后面条件的元素 print a.select {|i| i < 5 } puts "\n" # 使用each来迭代输出范围里的每个元素 a.each do |ele| puts "当前元素是:" + ele.to_s end end
执行上面的程序,看到如下运行结果:
56789 234 当前元素是:2 当前元素是:3 当前元素是:4 当前元素是:5 当前元素是:6 当前元素是:7 当前元素是:8 当前元素是:9
大部分时候,我们都会使用数字和字符串来创建Range对象,但作为一个面向对象的语言,Ruby也允许我们使用自定义类的对象来创建Range对象。
如果我们要使用自定义对象来创建Range对象,则要求该自定义对象的类必须实现一个succ方法,以返回下一个值;而且这个对象也必须支持 <=> 来与其他对象进行比较。调用 <=> 时,它们比较前后两个对象的大小关系,根据两个对象是小于、等于或大于而返回-1,0或者+1。
下面的程序使用自定义对象来创建了一个Range对象。
class Apple include Comparable attr :weight # 该方法就是一个构造器,用于创建Apple实例时执行 def initialize(att) @weight = att end # 该方法用于返回该Apple实例的基本信息 def info @weight.to_s + "斤" end # 下面是创建Range必须实现的两个方法 # 用于比较两个Apple实例的大小 def <=>(other) self.weight <=> other.weight end # 返回指定Apple实例的下一个Apple实例 def succ Apple.new(@weight.succ) end end # 使用Apple实例创建Range对象 a = Apple.new(1) ... Apple.new(4) # 迭代Range对象 a.each do |apple| puts apple.info end
上面的程序使用自定义类Apple的实例来创建了一个Range对象,并对该Range对象进行迭代。运行上面的程序,看到如下执行结果:
1斤 2斤 3斤
3.6 正则表达式
正则表达式是一种常用的模式匹配技术,如果我们希望匹配某类满足一定规则的文本内容,就可以使用正则表达式来进行匹配。
3.6.1 正则表达式概述
实际上,我们每个人几乎都曾用过正则表达式。请回想一下在硬盘上是如何查找文件的。我们都曾经使用“?”和“*”字符来帮助查找文件。“?”字符匹配文件名中的单个字符,而“*”则匹配一个或多个字符。一个形如“data?.dat”的模式可以找到下述文件:data1.dat,data2.dat,datax.dat,dataN.dat……
上面搜索文件的方法很有用,但功能十分有限,因为“?”和“*”只能满足简单的匹配,对于更复杂的匹配,则需要使用正则表达式来完成。
使用正则表达式,通常可以完成如下工作:
❑ 测试字符串是否满足某个模式。例如,可以对一个输入字符串进行测试,看该字符串是否匹配一个电话号码模式或一个信用卡号码模式,这也被称为数据有效性验证。
❑ 替换文本。可以在文档中使用一个正则表达式来标识特定文字,然后可以将其全部删除,或者替换为别的文字。
❑ 根据模式匹配从字符串中提取一个子字符串。这可以用来在文本或输入字段中查找特定文字。
3.6.2 创建正则表达式
正则表达式可以采用字面值创建。因为正则表达式是Regexp类的实例,因此也可采用创建Regexp类实例的方式来创建正则表达式。这两种创建正则表达式的方式的语法格式如下:
/pattern/ Regexp.new( 'pattern')
下面的代码创建了两个简单的正则表达式。
# 直接采用字面值的方式创建正则表达式 my_reg = /abc/; # 采用创建Regexp实例的方式创建正则表达式 my_reg2 = Regexp.new("abc"); p my_reg == my_reg2
除此之外,还有一种“%表示法”也可以用于创建正则表达式,其语法格式如下:
%r{pattern}
正则表达式可以包含一个或多个选项来修改默认的匹配规则。如果使用字面值来创建正则式,那么一个或多个选项就紧跟在结束符后面。如果使用Regexp.new方法,那么选项就是构造函数的第二个参数。
Ruby正则表达式支持的选项有如下几个。
❑ i:忽略大小写。这个匹配将忽略模式和字符串中字符的大小写。
❑ o:只替换一次。正则式中的多个“#{...}”表达式只会被替换一次,当第一次遇到“#{...}”的时候,Ruby会替换该表达式,其他情况下则不会替换。
❑ m:多行模式。通常情况下,“.”匹配除换行以外的任何字符。有了 /m选项以后,“.”就可以匹配任何字符了。
❑ x:扩展模式。复杂的正则表达式很难阅读。该选项允许我们在模式中加入空格、换行和注释来使它更容易被阅读。
一旦我们有了一个正则表达式对象,就可以用它和字符串进行匹配,通过使用正则表达式的match(aString)方法,或者用匹配操作符“=~”(确定匹配)和“!~”(否定匹配)。字符串和正则表达式对象都可以使用匹配操作符,它们的解释如下。
❑ match(aString):方法返回字符串aString中匹配前面正则表达式的子串。
❑ =~:总是返回正则表达式所匹配的字串在字符串中第一次出现的index(不管是通过正则表达式调用该“=~”,还是通过字符串调用“=~”方法,效果一样)。
❑ !~ :判断字符串和正则表达式是否匹配,当字符串和正则表达式不匹配时返回true。
如果匹配操作符的两个操作数都是字符串,右边那个会转化成正则表达式。
看如下程序,下面的程序示范了正则表达式和字符串的匹配。
# 判断正则表达式在字符串中出现的索引 puts "xxabc" =~ /ABC/ # 判断正则表达式在字符串中出现的索引,使用了i选项 puts "xxabc" =~ /ABC/i # 判断正则表达式在字符串中出现的索引 puts /ABC/i =~ "xxabc" # 截取字符串中匹配正则表达式的子串 puts /ABC/i.match("xxabc") # 截取字符串中匹配正则表达式的子串 puts /ABC/.match("xxabc") puts /ABC/i.match("xxabcyyabc") # 判断正则表达式和字符串是否匹配 puts "xxabc" !~ /ABC/
运行上面的程序,看到如下运行结果:
nil #=>当字符串中不曾出现匹配正则表达式的子串时,返回nil 2 #=>返回字符串中匹配正则表达式的子串的索引 2 #=>返回字符串中匹配正则表达式的子串的索引 abc #=>搜索字符串中匹配正则表达式的子串 nil #=>搜索字符串中匹配正则表达式的子串,找不到时返回nil abc #=>搜索字符串中匹配正则表达式的子串,找到多个也只返回一个 true #=> 当字符串和正则表达式不匹配时返回true
下面详细介绍正则表达式的语法细节。
3.6.3 匹配基本字符
在正则表达式中,英文句号(.)、竖线(|)、英文括号(包括 (,[,{,},],))、反斜线(\)、^、加号(+)、美元符号($)、星号(*)和问号(?)都有特定的含义。除此之外,其他任何字符都匹配它自己。如果需要匹配上面的特殊字符,则应该使用反斜线来转义。
如果我们希望匹配某个范围内的所有字符,则可以使用如下格式匹配从字符c1到字符c2的任一字符:
[c1-c2]
下面是这些特殊字符的含义。
❑ ^ :匹配一行的开始。
❑ $ :匹配一行的末尾。
❑ \A :匹配字符串的开始。
❑ \z :匹配字符串的末尾。
❑ \Z :匹配字符串的末尾,如果字符串以“\n”结束,那么它从“\n”前面开始匹配。
❑ \b,\B :分别匹配单词边界和非单词的边界。
❑ \d :数字字符。
❑ \D : [^0-9] 非数字。
❑ \s : [\s \t \r \n \f] 空格字符。
❑ \S : [^ \s \t \r \n \f] 非空格字符。
❑ \w : [A-Z a-z 0-9_] 单词符号。
❑ \W : [^ A-Z a-z 0-9_] 非单词符号。
❑ 英文句号(.):出现在方括号外面,它代表除了换行符以外的任何字符(如果/m选项被设置了,那么它也匹配换行符)。
3.6.4 重复
我们可以在上面的任意特殊字符后加上特殊的限定来表明重复次数。例如 \s匹配一个空白,则 \s*可以匹配任意多个空白。
实际上,用于控制重复次数的标识符有如下几个。如果 r 代表一个模式里面的前置字符,那么 r * 匹配0个或多个r。下面详细介绍了这种用法。
❑ r* :配置0个或多个r。
❑ r+ :匹配1个或多个 r。
❑ r? :匹配0个或1个 r。
❑ r{m} :匹配m个r。
❑ r{m,n} :匹配最少m个,最多n个 r。
❑ r{m,} :匹配最少m个 r,最多无限个r。
这些重复修饰符有很高的优先权,在正则表达式中,它们和它们的前置字符紧密绑定。
例如:/ab+/ 匹配一个a后面跟一个或多个b而不是一个ab组成的序列。如果我们希望匹配任意多个ab组成的序列,则可以使用 (ab)+,使用一个括号将ab成组即可。
前面介绍的重复模式串被称为“贪婪匹配”,因为它们默认匹配尽量多的字符。我们可以改变这种行为,让它们匹配最少的,只要加一个问号后缀就可以了。
3.6.5 成组
我们可以使用英文圆括号(())来将多个模式字符组成一个整体。在这种情况下,括号里的多个模式字符不可拆分。例如前面介绍的an* 用于匹配所有以a开头的字符串(n出现0次或者无限多次),而 (an)* 则匹配an出现0次或无限多次的情形。
成组主要有如下两种用法(假设r1,r2是正则表达式的一个模式字符)。
❑ (r1r2):将r1和r2组成一个整体使用,将r1r2捆绑在一起。
❑ (r1|r2):选择r1和r2中的任意一个,r1和r2不可同时出现。
3.7 数组
与其他所有编程语言一样,Ruby也提供了数组。如同所有弱类型的语言一样,Ruby数组里的数据元素类型可以互不相同,而且数组长度也是可变的。
3.7.1 定义数组
Ruby中的数组可以使用一系列在方括号中用逗号分隔的对象引用来定义,方括号中的最后一个逗号会被忽略。如下代码所示:
# 定义了一个数组,该数组名为a a = [4, 5,"hello"]
对于Ruby的数组而言,每个数组元素都处于数组的一个位置上,用一个整数(既可是正整数,也可以是负整数)来索引。数组指定索引的下标用“[ ]”操作符,这实际上也是一个方法,可以在子类中被重载。当我们从左向右访问数组元素时,最左边数组元素的索引从0开始,然后依次是1,2,3,……如果我们希望从右向左访问数组元素,则最右边的数组索引从-1开始,然后依次是-2,-3,-4,……
看如下程序,下面的程序直接创建一个数组之后,又为该数组增加了一个数组元素。
# 定义一个数组 my_array = [3, 4 , "Hello"] # 输出数组长度 puts "my_array的数组的长度为" + my_array.size.to_s # 增加一个数组元素 my_array[4] = 3..5 # 输出数组长度 puts "my_array的数组的长度为" + my_array.size.to_s # 输出所有数组元素 puts my_array # 从左向右访问数组元素 puts "第三个数组元素是 " + my_array[2] print "倒数第一个数组元素是 " # 从右向左访问数组元素 puts my_array[-1]
运行上面的程序,看到如下的运行结果:
my_array的数组的长度为3 my_array的数组的长度为5 3 4 Hello nil 3..5 第三个数组元素是Hello 倒数第一个数组元素是 3..5
从上面的运行结果可以看出:既可以通过正整数索引来访问数组元素,也可以通过负整数索引来访问数组元素。不仅如此,还可以发现Ruby语言里的数组长度是变化的,数组长度总是等于最后一个数组元素的索引加1。
如果我们只指定了数组中某个数组元素的值,则该元素前面没有指定值的数组元素的值都是nil。
提示 Ruby语言里数组的长度是可变的,当我们指定中间某个数组元素的值后,会导致该元素前面没有指定值的数组元素的值都是nil。
Ruby里的数组是Array类的实例,因此,我们还可以通过创建Array类的实例来创建数组。看如下程序,我们通过创建Array类的实例来创建数组。
# 定义一个空数组 my_array = Array.new puts "数组的长度为 " + my_array.size.to_s # 为该数组的数组元素指定一个值 my_array[3] = " Hello" puts "数组的长度为 " + my_array.size.to_s my_array[5] = 4...7 puts "数组的长度为 " + my_array.size.to_s # 输出所有数组元素 p my_array
运行上面的程序,看到如下运行结果:
数组的长度为 0 数组的长度为 4 数组的长度为 6 [nil, nil, nil, " Hello", nil, 4...7]
上面的代码示范了通过创建Array的实例来创建数组。直接使用Array.new创建一个空数组,此时该空数组的长度为0,不包含任何数组元素。随着程序为该数组元素指定值,数组长度在不断变化中。
再看如下程序:
# 定义一个空数组 my_array = Array.new # 当指定正数索引时,数组长度自动加长 my_array[4] = 5; # 指定负数索引时,该索引必须对应已经存在的数组元素 my_array[-4] = 7; p my_array # 下面代码出错,当负数索引超出数组长度时,数组长度不会自动加长 my_array[-7] = "Hello"
当我们使用my_array[-7] = "Hello"代码为数组元素赋值时,如果该负数索引所对应的数组元素不存在,数组长度不会自动加长。
注意 当我们使用正数索引来为数组元素赋值时,如果该数组元素不存在,则该数组自动加长,保证该数组元素有效;但负数就达不到这种效果,使用负数索引来访问数组元素时,负数索引必须引用一个已经存在的数组元素。
3.7.2 数组的%表示方法
除此之外,Ruby语言还可以把一个包含多个空格的字符串转换成数组。Ruby会把空格当成数组分隔符,每个空格分隔的子串都会被当成一个数组元素。
如果希望将包含多个空格的字符串转换成数组,则需要使用“%w{...}”或者“%W{...}”表示法,这两种表示法的功能大致相同,但“%W{...}”表示法会进行更多额外的替换。看如下程序:
$global = 6 my_array = %w(Hello Ruby On Rails, Ruby\ on\ Rails Is Funny! #{$global}) p my_array my_array2 = %W(Hello Ruby On Rails, Ruby\ on\ Rails Is Funny! #{$global}) p my_array2
运行上面的程序,看到如下运行结果:
["Hello", "Ruby", "On", "Rails,", "Ruby on Rails", "Is", "Funny!", "\#{$global}"] ["Hello", "Ruby", "On", "Rails,", "Ruby on Rails", "Is", "Funny!", "6"]
从上面运行结果中可以看出,当使用“%W{...}”表示法时,Ruby会把该字符串中的#{$global}替换成表达式的值,但使用“%w{...}”表示法则不会。
因为Ruby会把字符串中的空格当成数组元素分隔符,如果希望某个数组元素中包含空格,则可以使用反斜线(\)来进行转义,正如上面的程序中看到的效果。
3.7.3 数组的常用方法
Ruby的数组是Array类的实例,因此可以使用Array类的实例方法,使用这些方法可以用数组实现队列、堆栈、列表等各种数据结构。
除此之外,数组还包含如下两个常用方法:截取数组的子数组,以及同时为多个数组元素赋值。
下面先看截取数组的子数组的用法,它有三种用法。
❑ array[n , m]:截取数组array的子数组,从索引n开始截取,截取长度为m个元素组成新数组。
❑ array[n .. m]:截取数组array的子数组,从索引n开始截取,截到索引为m的元素(包括索引为m的数组元素)。
❑ array[n ... m]:截取数组array的子数组,从索引n开始截取,截到索引为m的元素(不包括索引为m的数组元素)。
# 定义一个Range对象 my_range = 1..9 # 把Range对象转换成数组对象 my_array = my_range.to_a p my_array # 从数组的第3个元素开始截取,共截取2个元素 p my_array[2,2] # 从数组的第3个元素开始截取,截取到第5个元素,包含第5个元素 p my_array[2..4] # 从数组的第3个元素开始截取,截取到第5个元素,不包含第5个元素 p my_array[2...4]
运行上面的程序,得到如下运行结果:
[1, 2, 3, 4, 5, 6, 7, 8, 9] [3, 4] [3, 4, 5] [3, 4]
除此之外,还可以同时为多个数组元素赋值,其用法与上面基本类似,一样是通过array[n , m ],array[n .. m ]或者array[n ... m ]三种形式来指定多个数组元素。赋值运算符(=)指定多个值,如果在左边指定的数组元素个数和右边指定的值个数不等,则原数组会自动调整。
看如下程序,下面程序示范了同时为多个数组进行赋值的情况。
# 定义一个Range对象 my_range = 1..9 # 把Range对象转换成数组对象 my_array = my_range.to_a # 从第3个元素开始,一共将2个元素替换成一个字符串元素 my_array[2,2] = "Ruby" p my_array # 从第3个元素开始,一共将1个元素替换成2个元素 my_array[2,1] = ["Hello" , "World"] p my_array # 将第3个元素、第4个元素替换成7,8,9三个元素 my_array[2..3] = [7 , 8 , 9] p my_array # 从第2个元素开始,一共将6个元素替换成空 my_array[1, 6] = [] p my_array
运行上面的程序,看到如下运行结果:
[1, 2, "Ruby", 5, 6, 7, 8, 9] [1, 2, "Hello", "World", 5, 6, 7, 8, 9] [1, 2, 7, 8, 9, 5, 6, 7, 8, 9] [1, 7, 8, 9]
3.8 Hash对象
Ruby提供两种容器对象(类似于Java里的集合对象,一个复合对象里可以包含多个其他对象):数组和Hash对象。从某种意义上来看,数组和Hash对象是统一的,数组只是Hash对象的一种特殊形式:Hash对象里包含的元素的key是任意的,而数组里所包含元素的key是连续的,而且只能是整数。因此,有时候我们也把Hash对象称为关联数组对象或者字典对象。
3.8.1 定义Hash对象
定义Hash对象不仅需要定义该Hash对象里包含的元素,还应该定义该元素对应的key。也可以换一个角度来看Hash对象——Hash对象里的元素都由两个部分组成:key和value。无论如何,我们定义Hash对象时,都需要同时定义Hash对象的key和value两个部分。
Hash对象是由一系列在花括号里面的key-value对组成,多个key-value对之间以英文逗号(,)隔开,key和value之间由“=>”隔开,最后一个逗号会被忽略。
看如下定义Hash对象的程序。
# 定义一个Hash对象 score = { "Java" => 70, "Java" => 67, "Ruby" => 77, "Perl" => 65, 5=>"Hello" } # 输出Hash对象 p score
运行上面的程序,看到如下输出结果:
{5=>"Hello", "Ruby"=>77, "Java"=>67, "Perl"=>65}
从上面的程序中可以看出,所谓哈希对象就是把任意对象当做索引来构成的数组。在一个指定的哈希对象中,key和value的类型不一定要一致。如果我们为同一个key指定了两次值,语法上并不会出现问题,只是后面指定的值会覆盖前面指定的值。
注意 如果为同一个索引的元素两次指定元素值,则后面指定的元素值会覆盖前面指定的元素值。
从理论上讲,Ruby里的Hash对象可以使用任何数据类型作为key。但如果希望使用某个对象作为Hash对象的key,则该对象应该具有hash方法,而且该hash方法的返回值不应该随该对象的改变而改变。
因此,Ruby里的Array和Hash等不适合作为Hash对象的key,因为它们的值会随其内容的改变而改变。
实际上,Ruby经常使用字符串作为key值,但字符串值又会经常改变。为此,Ruby对字符串key值做了特殊处理。当你把一个String对象当做Hash对象的key时,Hash对象会复制一个字符串,然后把这个字符串当做键值,这样,程序对原字符串的改变不会影响Hash对象。
因此,如果你想使用自定义类的实例来当做Hash对象的key值,你必须确定Hash对象的key值在创建以后不再改变,或者在key值改变的时候记得调用Hash#rehash方法来重新索引Hash对象。
因为Hash对象就是Hash类的实例,因此我们完全可以采用创建Hash类实例的方式来创建Hash对象。完全可以先创建一个空的Hash对象,然后动态地增加Hash对象里的元素。看如下代码:
# 创建一个空的Hash对象 my_hash = Hash.new # 输出Hash对象里元素的个数 puts "Hash对象里元素的个数为: " + my_hash.size.to_s # 动态增加一个元素 my_hash["Java"] = 5; # 输出Hash对象里元素的个数 puts "Hash对象里元素的个数为: " + my_hash.size.to_s # 动态增加一个元素 my_hash["Ruby"] = "Simple"; # 输出Hash对象里元素的个数 puts "Hash对象里元素的个数为: " + my_hash.size.to_s
运行上面的程序,看到如下运行结果:
Hash对象里元素的个数为: 0 Hash对象里元素的个数为: 1 Hash对象里元素的个数为: 2
通过上面的程序的运行结果可以看出,Hash对象的元素可以动态增加,当Hash对象的元素动态增加时,该Hash对象的size(Hash对象的长度)也在动态改变——实际上,每当Hash对象增加一项(一个key-value对)时,该Hash对象的长度加1。
3.8.2 操作Hash对象
前面已经介绍了为Hash对象动态增加项,除此之外,还可以为Hash对象删除指定项,判断Hash对象是否包含某个特定key,获取Hash对象所有key和所有value等操作。
下面是Hash对象所支持的常用操作。
❑ length() / size():返回Hash对象的长度。
❑ delete(key):删除指定Hash对象中指定key的对应项。
❑ include?(key) \ has_key?(key) \ key? \ member?:判断指定的Hash对象是否包含特定key。
❑ keys():返回该Hash对象的全部key组成的数组。
❑ values():返回该Hash对象的全部value组成的数组。
看如下程序,下面程序分别示范了上述方法的用法。
score = { "Java" => 67, "Ruby" => 77, "Perl" => 65, 5=>"Hello" } p score # 删除指定Hash对象中指定key的对应项 p score.delete("Perl") # 判断指定Hash对象是否包含特定key p score.include?("Perl") # 判断指定Hash对象是否包含特定key p score.include?("Java") # 返回该Hash对象的全部key p score.keys() # 返回该Hash对象的全部value p score.values()
运行上面的程序,看到如下运行效果。
{5=>"Hello", "Ruby"=>77, "Java"=>67, "Perl"=>65} 65 false true [5, "Ruby", "Java"] ["Hello", 77, 67]
从上面的程序中可以看出,当调用keys()和values()方法后,即可返回该Hash对象的全部key和全部value所组成的数组,从而可以迭代输出该Hash对象的全部key和全部value。
实际上,如果仅仅是希望迭代输出Hash对象的全部key和全部value,完全可以使用Hash对象的迭代器方法。Hash对象支持如下4个常用的迭代器。
❑ each:迭代输出Hash对象里的全部项(包括key和value)。
❑ each_key:迭代输出Hash对象里的全部key。
❑ each_pair:迭代输出Hash对象里的全部key-value对(包括key和value)。
❑ each_value:迭代输出Hash对象里的全部value。
下面程序示范了如何使用each和each_key迭代器来对Hash对象进行迭代。
score = { "Java" => 67, "Ruby" => 77, "Perl" => 65, 5=>"Hello" } # 同时对score里的key和value进行迭代 score.each do |key , value| puts "score中的key-value对为: " + key.to_s + " - " + value.to_s end # 仅仅迭代score里的key score.each_key do |key| puts "score中的key-value对为: " + key.to_s + " - " + score[key].to_s end
运行上面的程序,看到如下运行结果:
score中的key-value对为: 5- Hello score中的key-value对为: Ruby -77 score中的key-value对为: Java -67 score中的key-value对为: Perl -65 score中的key-value对为: 5- Hello score中的key-value对为: Ruby -77 score中的key-value对为: Java -67 score中的key-value对为: Perl -65
上面示范了each和each_key两个迭代器的用法,当然,该Hash对象也可使用其他迭代器方法,用法大致相同。
3.9 符号对象
符号(Symbol)对象是一个字符串、一个方法、一个类、或者一个标识符的内部表示。
我们可以通过如下方式来产生符号:
:标识符 :变量名 :操作符
从上面的代码中可以看出,我们可以在对象名前面加一个冒号来产生符号对象。
Ruby的符号很像一个字符串,因为它的内部表现形式是一个字符序列。因此,字符串与Symbol对象是一一对应的。与字符串不同的是:每个符号的实例只有一个。一个特定的对象名总是产生相同的符号对象,不论对象名在程序中是如何被使用的(不论它的值如何变化,不论它是一个方法,还是一个标识符)。看如下程序:
# 定义一个变量a a = "Hello" # 将变量a对应的符号对象赋给b b = :a # 重定义变量a的值 a = 6 # 将变量a对应的符号对象赋给c c = :a # 判断b是否等于c p b == c # 判断b是否等于字符串"a"对应的符号对象 p b == :"a" # 定义一个名为abc的方法 def abc p "Hello" end # 判断abc方法的符号对象与字符串"abc"对应的符号对象是否相等 p :abc == :"abc"
运行上面的程序,得到三个true的运行结果,这表明只要对象名相同,该对象总是产生相同的符号对象。
因为字符串和符号对象之间是一一对应的,因此字符串和符号对象之间可以互相转化。将一个符号对象转换成字符串,需要使用to_s方法;把一个字符串转换成符号对象,则应该使用to_sym方法。
除此之外,每个符号对象都有唯一与之对应的整数值,这个整数值可以使用to_i方法来获得。看如下程序:
# 定义一个字符串 a = "apple" # 定义apple标识符对应的符号对象 b = :apple # 判断"apple"字符串与符号对象的to_s返回值是否相同 p a == b.to_s # 判断"apple"字符串转换的符号与b对象是否相同 p b == a.to_sym # 取得b符号对象的整数值 p b.to_i # 取得b对象object_id p b.object_id # 取得符号对象的hash码 p b.hash
运行上面的程序,看到如下运行结果:
true true 9833 2517262 2517262
从运行结果可以看出符号和字符串互相转换的结果。
因为Ruby把系统中所有Symbol对象都放在运行时维护的一个符号表里,而这个符号表是一个原子数据结构,其中存储着当前所有的全局标识符,并且保证相同标识符不会出现多个对象。几乎每一个语言和系统都会有这样一个符号表,只不过对C/C++那样的语言,这个符号表只在编译时存在,运行时就没有了,而Python,Ruby则在运行时也保留这张符号表。
实际上,Ruby的符号表中保存的不仅有我们自己主动生成的符号对象,还有Ruby解释器对当前程序进行词法分析、语法分析后存在其中的、当前程序的所有名字。
因此,我们只要加上一个冒号,就让自己的对象跟Ruby引擎内部使用的对象成邻居了。所以,有的编程语言称其为interning(内部化或者驻留)。
注意 不要把符号对象和object_id方法返回值混淆。二者也有一定的相似之处,符号对象和object_id方法返回值都是程序级的(全局)。但object_id是每个对象的唯一标识,根据该对象在内存的存放地址计算得到;但符号对象则仅与该对象的标识有关,不管哪个对象,只要其具有相同的标识符,则它们对应的符号对象就完全相同。
一个符号对象最常用的地方是用它来代表变量名或者方法名。看如下定义存储器的代码示例:
class Apple attr_accessor :color end
与上面的程序对等的程序如下:
class Apple def color @color end def color=(val) @color = val end end
除此之外,还有一种创建符号对象的方式,称为“%表示法”。我们可以通过如下语法格式来创建一个符号对象:
%r{...}
看如下一行代码:
# 通过另一种方式来创建符号对象 p :a == %s{a}
从上面的代码中可以看出,通过两种方式创建的符号对象完全一样。
至此,我们所用的“%表示法”一共有如下几种。
❑ %!STRING!:用于创建一个使用双引号括起来的字符串。
❑ %Q!STRING!:用于创建一个使用双引号括起来的字符串。
❑ %q!STRING!:用于创建一个使用单引号括起来的字符串。
❑ %r!STRING!:用于创建一个正则表达式字面值。
❑ %w!STRING!:用于将一个字符串以空白字符切分成一个字符串数组,进行较少替换。
❑ %W!STRING!:用于将一个字符串以空白字符切分成一个字符串数组,进行较多替换。
❑ %s!STRING!:生成一个符号对象。
在上面的表示法中,感叹号(!)只是一个代表,可以使用任何非字母数字的字符替换它,包括换行符,但要保证起始字符和结束字符相同。但若起始字符是括号((,[,{,<),则终止切分字符是与其对应的括号。
除此之外,还有一种百分号表示法。
%x!STRING! :用于执行STRING所代表的命令。
例如,如果我们在Windows下运行如下代码:
%x{notepad.exe}
将可以启动Windows下的记事本程序。
3.10 运算符
Ruby语言提供了相当丰富的运算符,运算符也是Ruby语言的基础。通过运算符,可以将变量连接成语句,语句是Ruby代码的执行单位。
Ruby语言提供了算术运算符、逻辑运算符、位运算符等。
3.10.1 算术运算符
Ruby支持所有的基本算术运算符,这些算术运算符用于执行基本的数学运算:加、减、乘、除、求余和乘方等。下面是6个基本的算术运算符。
(1)+ :加法运算符。例如代码:
a = 5.2 b = 3.1 sum = a + b # sum的值为8.3 puts sum
(2)- :减法运算符。例如代码:
a = 5.2 b = 3.1 sub = a - b # sub的值为2.1 puts sub
(3)* :乘法运算符。例如代码:
a = 5.2 b = 3.1 product= a * b #product的值为16.12 puts product
(4)/ :除法运算符。例如代码:
a = 36 b = 9 div = a / b # div的值为4 puts div
(5)% :求余运算符。例如代码:
a = 5.2 b = 3.1 mod = a - b # mod的值为2.1 puts mod
(6)** :乘方运算符号,例如代码:
a = 3 b = 3 power = a ** b # power的值为27 puts power
在上面的6个运算符中,“+”除了可作为数学的加法运算符外,还可作为字符串的连接运算符。“-”除了可以作为减法运算符之外,还可以作为求负的运算符号。例如代码:
# 定义变量x,其值为-5 x = -5 # 将x求负,其值变成5 x = -x
3.10.2 赋值运算符
赋值运算符用于为变量指定变量值,Ruby使用“=”作为赋值运算符号。通常,使用赋值运算符将一个字面值赋给变量。如下面的代码所示:
# 将一个字面值赋给一个变量 a = 5
除此之外,也可使用赋值运算符将一个变量的值赋给另一个变量。即如下代码也是正确的:
# 将a变量的值赋给b变量 b = a
赋值运算符也支持连续赋值,通过使用多个赋值运算,可以一次为多个变量赋值。如下代码也是正确的:
# 通过为a, b , c ,d赋值,4个变量的值都是7 var a = b = c = d = 7
赋值运算符还可用于将表达式的值赋给变量。如下代码也是正确的:
# 为变量a赋值12.34 a = 12.34; # 将表达式的值赋给b b = a + 5 # 输出b的值 puts b
赋值运算符还可与其他运算符结合后,成为功能更加强大的赋值运算符,如下所示。
❑ +=:对于x += y,即对应于x = x + y。
❑ -=:对于x -= y,即对应于x = x - y。
❑ *=:对于x *= y,即对应于x = x * y。
❑ /=:对于x /= y,即对应于 x = x / y。
❑ %=:对于x %= y,即对应于x = x % y。
❑ **=:对于x **= y,即对应于x = x ** y。
实际上,这种加强型的赋值运算符由赋值运算符(=)和其他二目运算符组成,赋值运算符几乎可以与所有的二目运算符组成这种加强的运算符。
3.10.3 位运算符
Ruby也提供了丰富的位运算符,大致包含如下6个。
❑ & :按位与。
❑ | :按位或。
❑ ~ :按位非。
❑ ^ :按位异或。
❑ << :左位移运算符,右边空出的位补0。
❑ >> :右位移运算符,左边空出的位补最高位。
上面的位运算符包含了“&”,“|”,“~” 和“ ^”4个位运算符。其中“~”是一个单目运算符,它执行按位取反,即把运算数转换成二进制数,然后依次对每个位取反(0取成1,1取成0),然后再换成对应的结果。
其他三个位运算符的运算法则如表3.1所列。
表3.1 逻辑位运算符的运算结果表
看如下代码:
# 执行按位与 puts 5 & 9; # 执行按位或 puts 5 | 9;
程序执行5 & 9的结果是1,5 | 9的结果是13。下面介绍运算的原理。
5的二进制码是00000101,9的二进制码是00001001,运算过程如图3.2所示。
图3.2 位运算的过程
左位移运算符是将二进制码向左移动,右边以0补齐。看如下代码:
a = 5; # 执行左位移 puts a << 2 # 执行右位移 puts a >> 2
上面的程序执行左位移后得到20,执行右位移后得到1,即5<<2的结果是20,而5>>2的结果是1。下面介绍运算的原理。
5的二进制码为00000101,左移2位成为00010100,是20的二进制码。5右移两位成为00000001(右边补符号位,即0),该二进制数对应1。
按位非(~)就是先将操作数转化成二进制数,然后将二进制数按位取反,再转换回十进制数。看如下程序:
# 定义一个整型变量 a = 5 # 设置按位取反 puts ~5
上面的程序运行得到-6,即~5的结果为-6。
下面介绍运算过程:5的二进制码为00000101,按位取反后得到11111010,这是一个负数(负数在计算机里都是以补码形式存放),补码为11111010的数对应的反码为11111001,该反码对应的原码为10000110,也就是-6。
提示 计算机里数字的存放形式有3种:原码、反码和补码。实际上,所有数字在计算机里都是以补码形式存放的。反码和原码的关系为:反码是对原码直接取反(符号位除外)。补码和原码的关系为:正数的原码和补码完全相同,负数的补码为反码-1。
3.10.4 比较运算符
比较运算符用于判断两个变量或常量的大小,运算的结果是一个布尔值。Ruby支持的比较运算符如下。
❑ > :大于。如果前面变量的值大于后面变量的值,返回true。
❑ >= :大于等于。如果前面变量的值大于或等于后面变量的值,返回true。
❑ < :小于。如果前面变量的值小于后面变量的值,返回true。
❑ <= :小于等于。如果前面变量的值小于或等于后面变量的值,返回true。
❑ != :不等于。如果前后两个变量的值不相等,返回true。
❑ == :等于。如果前后两个变量的值相等,返回true。
❑ <=> :比较两个对象,根据大于、等于、小于的情况,分别返回1,0,-1。
❑ === :当普通对象处于运算符左边时,该运算符与==功能相同;当左边对象是Range实例,且右边的对象在左边Range之内时,返回值true,否则返回false。
❑ =~ :正则表达式匹配,判断正则表达式所匹配的字串在目标字符串中出现的索引。
❑ !~ :判断字符串是否匹配一个正则表达式,当不匹配时返回true。
上面的比较运算符中,前面6个比较常见。后面4个运算符才比较特殊,它们分别有各自不同的用法,甚至不一定都返回true或false。看如下代码:
# 当前一个Range中包含8时,返回true puts (7..9) === (8) # 当前一个Range中包含8时,返回true puts 5 <=> 7 # 返回后面正则表达式在前面字符串中出现的索引 puts "abc123" =~ /c1/ # 当前面字符串和后面正则表达式不匹配时返回true puts "abc123" !~ /dd/
运行上面的程序,看到如下运行结果:
true -1 2 true
值得注意的是,比较运算符不仅可以在数字之间进行,也可以在字符串之间进行。字符串的比较规则是按字母的ASCII值进行比较。对于两个字符串,先比较它们的第一个字母,其ASCII值大的字符串大;如果它们的第一个字母相同,则比较第二个字母……依次类推。看如下代码:
# 因为b的ASCII值大于a的ASCII值,故下面的表达式返回true puts "b" > "a" # 因为Z的ASCII值小于b的ASCII值,故下面表达式返回false puts "ZAD" > "b"
3.10.5 逻辑运算符
逻辑运算符用于操作两个布尔型的变量或常量。逻辑运算符主要有如下5个。
❑ && / and :与。必须前后两个操作数都是true才返回真,否则返回false。
❑ & :不短路与。作用与&&相同,但不会短路。
❑ || :或。只要两个操作数中有一个为true,就返回true,否则返回false。
❑ | / or :不短路或。作用与 || 相同,但不会短路。
❑ ! / not :非。这是一个单目逻辑运算符。如果操作数为true,返回false;如果操作数为false,返回true。
❑ ^ :异或,如果前后两个操作数的值相同(都是true,或者都是false),则返回false;否则(一个为true,一个非false,顺序无所谓)返回true。
下面程序简单测试了Ruby中的逻辑运算符。
# 定义了一个名为a的变量,变量值为6 a = 6 # 使用 ! 运算符求非 puts !(a > 5) puts (not ( a < 5)) # 使用异或 puts true ^ false puts false ^ false
运行上面的程序,看到如下运行结果:
false true true false
从上面运行结果中可以看出:“!”对原来的true或false进行求非,原来是true,得到false;原来是false,得到true。
对于&,&&和and的区别,看如下代码:
a = 6 # 使用&&求逻辑与 puts (a < 5) && ((b = a + 1) < 7) # 使用and求逻辑与 puts (a < 5) and ((c = a + 1) < 7) # 使用&求逻辑与 puts (a < 5) & ((d = a + 1) < 7) # 依次输出b,c,d的值 puts "b的值" + b.to_s puts "c的值" + c.to_s puts "d的值" + d.to_s
上面的程序的运行结果如下:
false false false b的值 c的值 d的值7
逻辑与要求两个运算数都是true,才会返回true。对于上面的程序,因为a<5总是返回false,故上面三个逻辑表达式的结果都是false。关键是三个逻辑运算符后面的运算数:它们分别是三个赋值语句,分别定义了b,c,d三个变量——但我们只看到了d变量存在,这是因为&&和and都是短路与,如果第一个运算数已经是false,则整个逻辑表达式直接返回false,根本不会计算第二个表达式。
对于 |,|| 和or的区别,看如下代码:
a = 6 # 使用 || 求逻辑或 puts (a > 5) || ((e = a + 1) < 7) # 使用or求逻辑或 puts (a > 5) or ((f = a + 1) < 7) # 使用 | 求逻辑或 puts (a > 5) | ((g = a + 1) < 7) # 依次输出e,f,g的值 puts "e的值" + e.to_s puts "f的值" + f.to_s puts "g的值" + g.to_s
运行上面的程序,看到如下运行结果:
true true true e的值 f的值7 g的值7
上面的程序分别使用了三个逻辑或来对逻辑表达式进行求值。因为逻辑或只要有一个运算数为true,整个逻辑表达式的值就是true,因此我们看到上面三个逻辑表达式的值都是true。
除此之外,上面三个逻辑表达式的后一个运算数都包含了一个额外变量的定义,只有使用 | 或or作为逻辑或时,后一个运算数里定义的变量才有效。这是因为,对于短路的逻辑或(||)而言,如果第一个操作数返回true,|| 将不再对第二个操作数求值,直接返回true。它不会计算(e = a + 1)< 7这个逻辑表达式,因而e = a + 1没有获得执行的机会,因此e变量不存在。
3.10.6 三目运算符
三目运算符只有一个:“?:”。三目运算符的语法格式如下:
(expression) ? if-true-statement : if-false-statement;
三目运算符的规则是:先对逻辑表达式expression求值,如果逻辑表达式返回true,则返回第二个操作数的值;如果逻辑表达式返回false,则返回第三个操作数的值。看如下代码:
# 使用三目运算符 a = 6 > 5 ? "Hello" : 6 p a
执行上面的程序,得到如下输出结果:
"Hello"
因为6 > 5返回true,因此a等于“?”后面的第一个运算数,也就是“Hello”。
大部分时候,三目运算符都是作为if else的精简写法。上面的程序等于如下的代码形式:
if 6 > 5 then a = "Hello" else a = 6 end
这两种代码写法的效果是完全相同的。三目运算符甚至支持多个语句。看如下代码:
# 如果5大于3,执行下面代码块 if 5 > 3 then puts "多行语句" puts "5大于3" # 否则,执行下面代码块 else puts "多行语句" puts "5小3" end
对于上面的代码块,可以转换成如下三目运算符。换成如下语句是无法正常运行的:
5 > 3 ? (puts "多行语句";puts "5大于3") : (puts "多行语句";puts "5小3")
注意 上面使用了英文分号(;)作为语句结束。实际上,在Ruby的语言中,可以使用英文分号来作为语句的结束,但并不强制使用。
3.10.7 Ruby运算符的含义
虽然上面介绍了Ruby里包含的运算符,但必须指出的是:Ruby里的运算符和C/C++,Java里的运算符不一样,Ruby里的运算符实际上不是运算符,而是方法!
因此,当我们执行5 + 5的运算时,“+”实际上不是一个运算符号,而是一个方法名。因此我们可以有如下代码:
# 通过调用方法的形式来执行加法运算 p 5.+(5)
上面的代码明显是使用了调用方法的形式来进行加法运算,这是Ruby语言为了保证自己是纯粹的面向对象特性所做出的努力。通过这种方式,可保证任何对象后都是调用方法,没有所谓的运算符;另一方面,Ruby为了保证和其他语言里运算符的兼容性,让这些特殊方法的方法名与其他语言里的运算符相同。
3.11 本章小结
本章详细介绍了Ruby语言的基本语法。本章从Ruby语言的解释器介绍起,详细介绍了如何编写、运行Ruby程序,并详细介绍了Ruby程序的落脚点,Ruby程序的入口等知识。本章详细介绍了Ruby的常量、变量等细节,还重点介绍了Ruby语言的各种基本数据类型,包括字符串、数值、正则表达式、数组、Hash对象、符号对象等常用数据类型,并细致介绍了Ruby的各种运算符和各种表达式用法。