Wednesday, October 22, 2008

(3)迭代器和枚举对象

尽管我们前面说到的while,until,for循环是Ruby的核心部分,但是通常情况下我们是用的是另一个更特殊的方式,称为迭代器,迭代器是Ruby中非常值得关注的功能,很多介绍Ruby的文章都会出现类似这样的例子:
3.times { puts "thank you!" }  # 打印3次 感谢
data.each {|x| puts x } # 打印data中的每个元素
[1,2,3].map {|x| x*x } # 计算数组的平方
factorial = 1 # 计算n的阶乘
2.upto(n) {|x| factorial *= x }
times,each,map,upto都是迭代器,他们与紧随其后的进行交互。隐藏在这些复杂操作背后的是yield操作,yield子句会临时从迭代器方法获得一个控制,并把它交给迭代器调用的方法。特别是当控制逻辑贯穿整个代码块的迭代调用,当代模块结束迭代器方法会收回这个控制接着执行yield后的内容。下图是迭代器yield的执行过程:
如你所见,一个代码块是可以有参数的,||中包含的就是参数,就像你在方法定义中()中的参数一样--你可以定义一些列参数,所以yield子句就相当于方法调用。值得注意的是迭代器并不一定全部用于迭代,不如Ruby1.9中定义的tap方法。或者File.open,这个方法是一个迭代器,但是如果其中没有任何参数传入的话,它只代表你打开了一个文件,在open代码块结束的时候文件自动被关闭,这很好的避免了由于忘记关闭程序而带来的问题。
Numeric迭代
Ruby为整数类型定义了3中迭代器,upto方法执行从当前整数对象到传入的整数参数之间的范围内,每一个元素的循环调用。比如
4.upto(6) {|x| print x}   # => 打印"456"

一般来说一个n.upto(m)将执行m-n+1 次循环。downto方法类似于upto不过正好相反,它是从大的数字向小的数字循环。
Integer.times方法执行n次循环,比如:
3.times {|x| print x }    # => 打印"012"

如果你想使用浮点数进行迭代,你可以使用step方法,比如下面的例子中数字从0开始每次增加0.1直到数值达到Math::PI的值,并计算每个迭代只得sin值:
0.step(Math::PI, 0.1) {|x| puts Math.sin(x) }
枚举对象
Array,Range,Hash以及其他的一系列类,定义了一个each方法,这个方法会将集合内的每一个元素传递给代码块。这可能是Ruby中最常用的操作:
[1,2,3].each {|x| print x }   # => 打印"123"
(1..3).each {|x| print x } # => 打印"123" 等同于 1.upto(3)
不仅仅是这些类,Ruby中的IO库定义了each方法,这样你就可以循环的读取文件中的每一行并对其进行处理:
File.open(filename) do |f|       # 打开指定的文件,并将其内容赋值到变量f中
f.each {|line| print line } # 打印f的每一行
end # 结束块并关闭文件
很多类还基于each方法定义了更多专用的方法,比如each_with_index就是其中之一:
File.open(filename) do |f|
f.each_with_index do |line,number|
print "#{number}: #{line}"
end
end
除此之外,枚举对象还定义了一系列其他有用的方法,collect,select,reject和inject。collect方法循环枚举对象中的每个元素然后根据代码块中的计算结果创建一个新的枚举对象
squares = [1,2,3].collect {|x| x*x}   # => [1,4,9]

select 方法用于筛选块中条件判断为true的元素,并返回筛选的结果:
evens = (1..10).select {|x| x%2 == 0} # => [2,4,6,8,10]

reject 方法与select方法正好相反,他筛选块中条件判断为false或nil的元素,并返回筛选的结果:
odds = (1..10).reject {|x| x%2 == 0}  # => [1,3,5,7,9]


inject 方法有点复杂,解释一下,它有两个参数,第一个参数是这个迭代器的计算结果值,第二个参数代表迭代对象的循环时的每一个元素,每次迭代的返回值是第一个参数,在迭代结束的时候返回值就是最终两个参数的迭代计算结果。inject还有一个初始化参数房在括号中,如果这里有一个参数那么它将使枚举对象的第一个元素:
data = [2, 5, 3, 4]
sum = data.inject {|sum, x| sum + x } # => 14 (2+5+3+4)
floatprod = data.inject(1.0) {|p,x| p*x } # => 120.0 (1.0*2*5*3*4)
max = data.inject {|m,x| m>x ? m : x } # => 5 (largest element

Tuesday, October 21, 2008

(2)循环语句

Ruby提供三种简单的循环while,until和for,while循环执行“当”条件表达式为true,until循环执行“直到”条件表达式为true:
x = 10               # 初始化一个循环计数器变量
while x >= 0 do # 如果x大于等于0就之一执行循环
puts x # 打印x的值
x = x - 1 # 递减x的值
end # 循环结束

# 执行这个循环知道x的值大于0就结束循环
x = 0 # 初始化循环计数器
until x > 10 do # 开始循环
puts x
x = x + 1
end # 结束循环
和if语句一样Ruby也提供了一个在一行代码中完成循环的变体:
x = 0                          # 初始化循环计数器
puts x = x + 1 while x < 10 # 执行循环

a = [1,2,3] # 初始化一个数组
puts a.pop until a.empty? # 循环的从数组中移除元素,知道这个数组为空。
这里有一种特殊情况,如果循环执行的代码是一个begin end包含起来的一段代码块,那么代码块的内容会先于循环逻辑判断:
x = 10              # 初始化循环计数器
begin # 在循环表达式判断之前,这段代码会至少执行一次
puts x
x = x - 1
end until x == 0
注意,如果你的代码块不是放在begin end之间,而是放在()中,那么不会在until判断之前执行代码块中的内容:
x = 0               # 初始化
( # 这种方式的代码片段如果条件表达式判断失败不会执行。
puts x
x = x - 1
) until x == 0
最后一个循环是for/in循环,它一般用于数组或者Range,字典对象,用于在这些对象的元素中迭代取值并执行代码,和while until一样,do关键字是可选的,for关键字后跟一个变量,或者用逗号分隔的多个变量,用于代表数组或字典中的元素,所有数组或者字典中的元素全部遍历一遍之后,循环就结束:
# 打印数组中的所有元素
array = [1,2,3,4,5]
for element in array
puts element
end

# 打印字典对象的键/值
hash = {:a=>1, :b=>2, :c=>3}
for key,value in hash
puts "#{key} => #{value}"
end
实际上for /in 循环是基于for语句内容对each方法的实现,比如上面的代码可以写成这样:

hash = {:a=>1, :b=>2, :c=>3}
hash.each do |key,value|
puts "#{key} => #{value}"
end

(1)条件判断语句

这一节要说的是条件控制语句,if, else ,elsif,unless,end。是Ruby中所有的对象都是有值的即使是一个条件判断语句。和大多数语句一样Ruby中条件判断语句用前面提到的关键字实现。比如:
if 条件表达式
要执行的代码
end


如果条件表达式的值为nil和false以外的任何值:if 和end之间的代码都会被执行,条件表达式之后必须换行后再写入要执行的代码,否则的话你需要一个额外的关键字then来分隔出哪部分是条件表达式,哪部分是要执行的代码。
# 如果x小于10 就给它加1
if x < 10 # 通过换行分隔
x += 1
end
if x < 10 then x += 1 end # 通过then关键字分隔

当然,在then关键字之后换行也是允许的:
if x < 10 then
x += 1
end

else关键字用于条件的跳转,如果if的条件不成立那么就执行else 部分的代码。
if data         # 如果data不为空
data << x # 那么在data中增加元素.
else # 否则...
data = [x] # 创建一个新的数组来保存这个元素.
end # 条件判断结束.

elsif关键字相当于else if ,解释一下就是“否则的话,那么如果。。。”
if x == 1
name = "one"
elsif x == 2
name = "two"
elsif x == 3 then name = "three" #elsif后也可以加then关键字
elsif x == 4; name = "four"#可以用分号替带换行
else
name = "many"
end

我们刚才说过Ruby中所有的对象都是有值得包括条件判断语句,条件判断语句返回的值,就是满足条件的代码块最后一行的值:
name = if    x == 1 then "one"
elsif x == 2 then "two"
elsif x == 3 then "three"
elsif x == 4 then "four"
else "many"
end
刚才我们提到,if表达式必须以换行或者then 加end的方式出现,Ruby中还提供了另外一个变体,你可以把要执行的代码放在if前面,条件表达式放在if的后面,then和end都可以不写,不过这种写法要求整个语句只能位于一行中:
puts message if message    # Output message, if it is defined

除了if语句Ruby还提供了一个unless语句,它的结构和if语句相似,但是执行与if语句相反的判断操作,也就是说如果unless后的表达式为false或nil那么就执行其后的代码:
s = unless o.nil?                        # 换行分隔
o.to_s
end
s = unless o.nil? then o.to_s end # then 关键字分隔

不过,unless没有类似elsif这样的语句。
最后在来提提Ruby中的case语句,如果你是Java程序员,你一定用过switch语句,他们的功能是类似的,不过case语句更强的,因为基于case语句的条件表达式可以是任何对象类型。
case
when x == 1
"one"
when x == 2
"two"
when x == 3
"three"
end

你也可以在when 后的条件表达式 后加then关键字来省去换行。或者更简洁的写法(如果你只判断一个变量的值):
name = case x
when 1 # 只比较x的值
"one"
when 2 then "two" # then关键字替代了换行
when 3; "three" # 分号替代了换行
else "many" # else用于提供缺省值
end

case语句中还可以包含多个条件表达式,表示满足其中任何一个条件都可以执行其后的代码:
case
when x == 1, y == 0 then "x is one or y is zero" # 用逗号分隔多个条件
when x == 2 || y == 1 then "x is two or y is one" # ||看起来更容易理解
end
我们前面讲过===在Range中可以判断一个值是否包含在其中,还可以判断一个对象是否是一个类的实例,在case语句中也可以这样使用:
x=1

case
when Numeric===x
print "yes"
else
print "no"
end

Ruby中同样支持?:的简写方式来完成一个条件判断,其中?相当于then,:相当于else:
def how_many_messages(n) # Handle singular/plural 
"You have " + n.to_s + (n==1 ? " message." : " messages.")
end

(5)赋值操作

在Ruby中复制操作通过=实现,这个我们前面一再提到。比如:
x+=1 #等同于x=x+
x*=1 #等同于x=x*1
x,y,z=1,2,3 #Ruby中的平行赋值,x=1,y=2,z=3
x,y,z=[1,2,3] #Ruby中的平行赋值,等号右边可以是一个数组,x=1,y=2,z=3
这些都很好理解,我主要想说说两个特殊情况。局部变量和平行赋值:
对于局部变量,因为没有类似全局变量的符号前缀,所以Ruby无法区分你现在用的x是一个局部变量还是一个方法,在这种情况下,如果你给x赋值那么Ruby认为这是一个局部变量。看下面的代码:
class Ambiguous
  def x; 1; end # 定义一个方法x,返回值为 1

  def test
    puts x      # 没有找到变量名为x,所以调用上面定义的方法:打印 1

    # 因为 "if false" 下面的第一行的赋值不会被执行. 但是
# Ruby解析器已经把他视为一个局部变量了.
    x = 0 if false

    puts x    # 现在x是一个变量但是没有被赋值: 打印nil

    x = 2     # 赋值为2
    puts x    # 所以现在打印 2
  end
end 
 
说到平行赋值,考虑下面的情况,如果等号左边只有一个变量,而右边有多个值,这种情况Ruby会将右边的值转换为一个数组:
x = 1, 2, 3      # x = [1,2,3]
但是如果左边的变量后有一个逗号,结果就不同了:
x, = 1, 2, 3   
  # x = 1;
 其他的值被忽略了
如果等号两边的变量和值不同,结果也不同:
Created with colorer-take5 library. Type 'ruby'

x, y = 1, 2, 3 # x=1; y=2; 3 is not assigned anywhere
#*有特殊的含义,表示后面是一个单独的数组
x, y, z = 1, *[2,3]  # Same as x,y,z = 1,2,3
x,*y = 1, 2, 3  # x=1; y=[2,3]
x,*y = 1, 2     # x=1; y=[2]
x,*y = 1        # x=1; y=[]
#如果存在括号,那么括号内的变量对应等号右边的相应值
x,y,z = 1,[2,3]             #  没有括号:x=1;y=[2,3];z=nil
x,(y,z) = 1,[2,3]           #  加上了括号:x=1;y=2;z=3
a,b,c,d = [1,[2,[3,4]]]     #  没有括号:a=1;b=[2,[3,4]];c=d=nil
a,(b,(c,d)) = [1,[2,[3,4]]] #  加上了括号:a=1;b=2;c=3;d=4
另外需要注意的一点是,如果平行赋值出现在一个方法的后面,Ruby会把逗号分隔的部分当作是方法的参数:
puts x,y=1,2 #Ruby会认为你给puts方法赋值x , y=1 和2 这3个参数。

(4)方法调用

以下四点说明了什么是方法调用:
  1. 方法的调用通过.或者::触发。如果省略了.和::那么说明是在调用自己的方法。
  2. 方法通过方法名调用。
  3. 方法的参数放在一对括号内并用逗号分隔,调用方法是在括号内传递参数,当然括号也不是必须的,你只要顺序给出参数值,并用逗号隔开就可以了。
  4. 另外,一个块(用花括号或者do/end关键字定义的)可以在其中通过yield关键字调用。块和迭代是Ruby提供的强大功能,我们将在后面详细介绍。

puts "hello world"  # "puts" 方法有一个字符串参数
Math.sqrt(2) # "sqrt" Math.sqrt方法有一个参数
message.length # "length" 方法返回message的长度,没有参数
a.each {|x| p x } # "each" 方法在块内部自动执行。
我们看看前面代码中的puts 方法,它是一个全局方法定义在Ruby的kernel中,它可以在任何地方使用。

(3)常量

常量和变量类似,只不过它的值一直保持不变,当然Ruby没有强制规定你不能修改常量的值,只不过如果你修改一个常量的值时会得到一个警告信息。表面上看起来常量和变量没什么区别,不过常量一般以大写字母开头,通常我们会把常量的所有字母都大写LIKE_THIS,并且在每个单词之间用下划线分隔。Ruby中的类目和模块名也是常量,不过它们通常使用每个单词首字母大写的驼峰式写法LikeThis。
常量的特性有些像全局变量,它可以在程序任何地方被引用,但和全局变量不同他可以在类或者模块中定义。
CM_PER_INCH = 2.54  # 定义一个常量.
CM_PER_INCH # 使用这个常量.
如果你在一个类或者模块中定义了一个常量,那么在引用这个常量的时候需要加上类或者模块前缀。
class Master
PP=2.00
end
puts Master::PP

有时候,模块是嵌入到另一个模块中的,这中情况下对常量的引用可以这样写:
Conversions::Area::HECTARES_PER_ACRE

如果一个常量存在于全局范围,那么::前面的内容可以省略:
::ARGV      # 全局常量 ARGV
需要注意的是我们这里说到的“全局”,实际上就是Object类内,也就是说上面的常量实际上是Object::ARGV。
而且如果没有给常量赋值,那么在调用这个常量的时候你会得到NameError异常。


Monday, October 20, 2008

(2)变量

变量就是一个值得名字,创建一个变量的过程就是给这个变量赋值,如果变量出现在等号的左边就是把这个变量的引用赋值给另一个变量。在Ruby中有4中不同类型的变量,以$开头的变量是全局变量,它在整个应用程序中都是有效的;以@和@@开头的变量代表实例变量和类变量;如果变量以下划线_或者消息字母开头,则说明这个变量是一个局部变量。如果在变量中有::说明你要定义一个静态变量比如Math::PI。如果出现点号. 说明你要调用变量所代表的对象的一个方法。Ruby在开始运行的时候还会初始化一系列的全局变量,有关这部分我们后面会讲到。
通常情况下你应该在使用变量前先给变量赋值,或者说初始化这个变量。在某些情况下,你也可以不初始化变量,这在Ruby中是允许的,不过必须遵循一定的规则:
  • 类变量:对于类变量,在使用之前你必须给它赋值,否则你会得到一个NameError的异常。
  • 实例变量:如果你使用了一个未初始化的实例变量,你会得到一个nil值,通常这不是好的编程习惯,如果你打开Ruby的-w选项你会得到一个警告信息。
  • 全局变量:和实例变量一样,如果你使用了未初始化的实例变量,你会得到一个nil值。
  • 局部变量:局部变量比较复杂,因为和其他变量不同,它没有一个明确的符号标记,标明它是一个局部变量还是一个方法调用。如果Ruby解析器识别到一个变量被赋值了,那么它知道这是一个局部变量而不是方法,如果没有赋值操作,Ruby会把它当作是一个方法调用,如果没有这样一个方法,哦,你会得到一个NameError异常。通常,如果一个局部变量没有明确的赋值,Ruby会认为它的值是nil。比如:

a = 0.0 if false    # 这个赋值操作永远不会执行。
print a # 打印nil,即使存在这样一个变量,但是它的值为nil。
print b # NameError: 不存在的变量或者方法。

(1)文字内容与关键字

Ruby中的表达式就是一段代码块,Ruby解析器可以通过它计算并得到一个值。下面是一些例子:
2                  # 一个数字
x # 一个本地变量
Math.sqrt(2) # 一个方法调用
x = Math.sqrt(2) # 赋值操作
x*x # 乘法操作
很多语言在表达式语句(比如条件控制语句和循环语句)之间作了明确的区分。语句只负责执行程序的流程,它们本身是没有值得,它们用于执行而不是计算。在Ruby中没有这样明确的区分,任何语句,包括类、方法都可以被看作是表达式并且都会返回一个值。
值得注意的是很多表达式属于初级表达式,它们是最简单的表达式类型,它们不可能由其他的表达式组成,比如数字。其它的表达式诸如数组、字典对象等不属于初级表达式。一些Ruby中的初级表达式有其特殊的含义,我们可以把它们成为关键字:
  • nil:代表空对象。
  • true
  • false
  • self:代表对象本身,相当于java中的this
  • __FILE__:是一个字符串,它表示一个Ruby可以操作的文件名,这对于处理异常信息非常有用。
  • __LINE__:结合__FILE__对象使用,它是一个整数,代表文件中的某一行。
  • __ENCODING__:在Ruby1.9中它用于指定编码格式,比如utf-8。


(24)“冻结”和“污染”的对象

出于某些需求你可能不希望你的对象状态被修改,那么在Ruby中你可以通过调用freeze方法实现,如果一个对象被freeze那么任何试图修改对象状态的行为都会抛出异常,这是对象变成了一个不可变对象:
s = "ice"      # 字符串是一个可变对象
s.freeze # 标记这个对象为不可变
s.frozen? # true: 用frozen?方法检查这个对象是否被冻结了
s.upcase! # TypeError: 不能修改一个冻结的对象
s[0] = "ni" # TypeError: 不能修改一个冻结的对象
说到这里我们再来说说对象的复制。Ruby定义了clone和dup方法来复制对象,当你执行这个两个方法的时候你会得到这个对象的拷贝,如果这个对象中存在对另一个对象的内部引用,你只能复制另一个对象的引用而不是它本身。如果你的对象中定义了initialize_copy方法,那么在调用clone和dup方法的时候会触发这个initialize_copy方法来初始化你刚刚复制的对象拷贝。那么这里clone和dup方法对于已经冻结的对象,复制拷贝的结果就不同了,clone得到的是一个冻结的对象,而dup方法得到的是一个“解冻”的对象。
我们的应用程序经常要面对很对安全问题,比如未信任的用户SQL注入操作。Ruby提供了一个方便的方案来处理这一问题。我们可以通过tainted方法来标明一个对象被“污染了”。一旦一个对象被污染了,那么所有派生自他的对象都是被污染的对象,你可以通过tainted?方法检测一个对象是否被污染了:
s = "untrusted"   # 缺省情况下,一个对象是没有被污染的。
s.taint # 把这个对象标记为被污染了
s.tainted? # true: 检查对象是否被污染了
s.upcase.tainted? # true: 被污染对象的派生对象也是被污染的
s[3,4].tainted? # true
通过gets方法读入的用户输入内容:比如命令行参数,环境变量,字符串都自动被标记为感染的。通过clone和dup复制的对象都是“污染的”对象,你可以通过untainted方法标记一个对象没有被污染。
当你设置了全局变量$SAFE时,这种机制非常有用,如果你设置$SAFE为任何非0的值,那么Ruby会限制所有的方法不能使用被污染的对象。

(23)对象的“顺序”

==用于判断对象是否相同,一些类还定义了“顺序”。比如两个对象可能相等,也可能一个对象“小于”另一个对象。数字对象就是一个最好的例子,比如5大于4说明5在4的后面,字符串也存在顺序,字符串的比较通过比较字符串的ASCII码来实现。在Ruby中类的顺序可以通过实现<=>操作符,如果说明操作符左边的值小于右边的,如果返回0说明两边的值是相等的,如果返回1说明,左边的值大于右边的。如果<=>两边的对象是不同的则返回nil。
1 <=> 5     # -1
5 <=> 5 # 0
9 <=> 5 # 1
"1" <=> 5 # nil: 字符串和数字无法比较。

<=> 用于比较已经足够了,但是这个符号看起来不够“直白”。在Ruby的minxin模块中基于<=> 定义了一系列其他的操作符,这样操作符意思更加明确:
  • < 小于
  • > 大于
  • <= 小于等于
  • >= 大于等于
  • > 大于
  • == 等于
如果你所见,这里没有定义!=符号。此外我们还将看到一个方法between?,它用于判断一个对象是否位于两个对象之间:
1.between?(0,10)  # true: 0 <= 1 <= 10
注意如果你个<=>的比较结果是nil,那么所有基于他的比较都返回false,对象Float他的返回结果是NaN.

(22)对象的比较

我们经常要比较两个对象,Ruby中提供了多种比较对象的方法。我们可以通过equal?方法比较两个对象是否相同,通过这种方式是比较两个值是否指向同一个对象的引用。比如:
a = "Ruby"       # 一个字符串对象。
b = c = "Ruby" # 两个字符串对象指向动一个引用。
a.equal?(b) # false: a和b是不同的对象。
b.equal?(c) # true: b和c指向同一个引用。
这种比较方式实际上是比较两个对象的ID是否相同,显然a是一个对象,而b和c指向另一个对象,他们的对象ID是不同的:
a.object_id == b.object_id   # 等同于 a.equal?(b)
还有另一种简单的方式,你可以使用==来比较对象是否相等,他的作用和equal?方法是不一样,它比较的是两个对象的内容是否相同:
a = "Ruby" # 定义一个字符串对象
b = "Ruby"    # 虽然和a的内容相同,但是他们是不同的对象
a.equal?(b) # false: a和b指向不同的对象
a == b # true: 他们的内容是相同的
除了字符串外,数组和字典类也定义了==操作符,如果两个数组或两个字典对象中元素个数相同,且每个元素都相同,那么==返回true.
Numerics对象在比较的时候会做一个简单的最新转换,所以Fixnum类型的1和Float类型的1.0的比较结果是相等。
同样,你可以使用!=来判断两个对象是否不等。
另外,Ruby还提供了eql?方法,它与equal?的功能相同,是一种更精炼的写法。
再来说说有三个等号的比较操作===。通常情况下这中方式与==是一样的,但是在某些特定情况下,===有特殊的含义:在Range中===用于判断等号右边的对象是否包含于等号左边的Range;正则表达式中用于判断一个字符串是否匹配模式,Class定义===来判断一个对象是否为类的实例,Symbol定义===来判断等号两边的符号对象是否相同。
(1..10) === 5 # true: 5属于range 1..10
/\d+/ === "123"  # true: 字符串匹配这个模式
String === "s" # true: "s" 是一个字符串类的实例
:s === "s" # true


===在大多数情况下用于case语句。
你还可以用=~来判断正则表达式的模式是否匹配。

(21)对象类和对象类型

有很多种方法可以获得一个对象的类是什么:
o = "test"  # 定义一个字符串值
o.class # 得到这个对象对于的类String



如果你想对类的集成结构感兴趣,你可以通过调用superclass方法获得:
o.class                       # String:o是一个字符串对象
o.class.superclass # Object: 字符串对象的父类是Object
o.class.superclass.superclass # nil: Object没有父类。

在Ruby1.9中Object不再是跟类:
Object.superclass             # BasicObject:在 1.9中Object的父类是BaseObject
BasicObject.superclass # nil: BasicObject 没有父类

那么,如果想比较一个对象的类是否为某个类,可以直接使用==操作:
o.class == String       # true 

instance_of? 可以完成相同的操作,这种做法看起来更简洁:
o.instance_of? String   # true

通常你可能希望判断一个对象是否是某个类的子类,你可以通过is_a?方法或者kind_of?方法:
x = 1                    # 定义一个对象
x.instance_of? Fixnum # true: 判断x是否是一个Fixnum
x.instance_of? Numeric # false: instance_of? 不能判断x是否为Numeric的子类
x.is_a? Fixnum # true
x.is_a? Integer # true
x.is_a? Numeric # true
x.is_a? Comparable # true: 是否实现了Comparable
x.is_a? Object # true 任何对象都是Object的子类

Ruby还定义了=== 操作符来完成is_a?相同的操作:
Numeric === x            # true: x is_a Numeric

在Ruby中我们通过不关心我们使用的对象的类型,我们只关心我们可以调用对象的那些方法,比如<<操作,在数组、字符串或者文件对象中,他意味着向对象增加内容,而对于整数对象,<<意味着对象的左位移操作。我们可以使用respond_to?方法来判断一个对象是否存在某个我们想要的方法。
o.respond_to? :"<<"  # 返回true,如果存在 << 操作。


但是这个方法的缺点是,你只知道有这样一个方法,但是你知道这个方法接受什么参数。比如对于整型<<期望的参数是一个整数,但是如果你给出的是一个字符串,那么这里会抛出异常。不幸的是,对此没有一个通用的解决方法,一个临时的方法是结合is_a?方法:
o.respond_to? :"<<" and not o.is_a? Numeric

(20)对象的唯一性

Ruby中所有的对象都有一个唯一的ID,你可以通过object_id方法获得对象的ID,在一个对象生命周期中,每一个对象ID都是不同的,唯一的。原来的id方法已经被废弃,如果你在Ruby1.8中调用这个方法你会得到一个警告信息,在Ruby1.9中这个方法被完全的删除了。
还有另外一个__id__方法与object_id实现同样的功能,如果你的object_id方法被重载了你可以通过这个方法获得对象ID

(19)Ruby中的对象

Ruby中一切都是对象,这里没有类似其他语言的原生类型和对象类型的区别,在Ruby中所有的对象都集成自Object。实际上当我们操作一个对象的时候我们实际上操作的是这个对象的引用,而不是对象本身,换句话说,当我们给一个变量赋值的时候我们并没有把值拷贝到这个变量“里边”,我们只是包存了一个引用到对象。如果你熟悉C或者C++你可以把对象引用理解为指针,但是Ruby其实没有实现指针。这里给出一些例子帮助我们理解对象引用:

s = "Ruby" # 创建一个字符串对象.  将一个引用存储到s.
t = s # 将这个引用拷贝给 t. 现在s和指向的是同一个引用.
t[-1] = "" # 通过对象t的引用修改这个对象.
print s # 通过 s访问这个已经更改的对象.打印值为 "Rub".
t = "Java" # 现在t指向另一个不同的引用.
print s,t # 打印结果为:"RubJava".


如果一个对象作为参数传递给一个方法,实际是传递的是对象的引用,而不是对象本身。更确切的说方法参数的传递是值传递而不是引用传递,但是这个是对象的引用。这句话说起来比较拗口,再举个例子,比如你传递一个对象x给方法,而你在方法内容对传入的参数就行了修改,那么对象x的引用也被修改了:


def modify(x) #参数是一个对象引用的值
x[0]="H"
end
z="hello"
y=modify(z)
puts z #z:Hello,值被改为了

因为你传递的是对象的引用,那么方法就可以利用这个引用修改底层的对象,这个修改操作发生在方法返回的时候。
我们说Ruby中所有的对象操作都是通过引用实现的,但是Fixnum和Symbol型的对象是“不可变对象”,而且这些对象中也没有定义“改变其自身”的方法,所以我们没法说着两个对象是值传递还是引用传递。
创建一个类的实例需要通过new方法,new方法分配一个实际的内存空间来存储这个对象。当你执行new操作的时候会调用对象的initialize方法,这个方法负责初始化对象。
myObject = myClass.new

Ruby中不存在类似C和C++中的析构函数,Ruby会在必要的时候自动进行垃圾对象的回收,垃圾回收是由Ruby内部控制的,当一个对象不再使用,或者没有被其他对象引用,它就会被Ruby作为垃圾对象回收。自动垃圾回收机制可以有效的避免由于忘记释放内存而造成的内存泄漏,但这也不能保证你的程序永远没哟内存泄漏,假如你对字典对象时候了缓存机制,而由没有实现类似“最近访问”算法,那么这个对象会一直存在,或者你把一个很长的自动对象定义为全局变量,那么这个对象会一直存在,知道你的Ruby解析器退出。

(18)True, False, and Nil

就像我们前面章节中说到的那样Ruby中用true,false来表示布尔值,用nil表示空值。true是TrueClass类的实例,同样false是FalseClass的实例,nil是NilClass的实例,注意Ruby中没有Boolean这个类,TrueClass和FalseClass的超类就是Object。如果你想比较一个对象是否为空,你可以通过下面的方式:
o == nil # 对象o是空的吗?
o.nil?     # 另一种检测方式
在Ruby中没有1为true,0为false的说法,所有的对象如果为nil或者false那么就是false,其它情况都是true。

(17)Ruby中的Symbol

Symbol称为符号对象,它是由Ruby解析器维护的一个符号表,这个符号表中包含了所有已知的类、方法变量等,这主要是为了避免更多的内存开销。因为在Ruby中的字符串是可变对象,所以每一个字符串都是一个单独的对象即使是有相同内容的字符串他们在内存中的id也是不同的,比如:
s="hello"
puts s.object_id
s2="hello"
puts s2.object_id
你会发现他们的object_id是不同的,这和类似Java这样的语言处理方式是不同的,Java中的字符串是不可变对象,所以如果你在Java中定义两个相同内容的字符串,他们在内容中实际上是占用同一块内存地址。
那么Symbol的作用就是唯一标识一个名字,它占用唯一的一个内存地址。Symbol通过在一个唯一的标识符或者字符串前加冒号(:)来定义:
:symbol                   #定义一个Symbol
:"symbol" # 另一种方式
:'another long symbol' # 如果Symbol对象包含空格,可以用引号包含
s = "string"
sym = :"#{s}" # Symbol对象:string

就像String中使用%q和%Q定义字符串的方式,你也可以用%s加上任意的边界符号来定义一个Symbol。
%s[hello]     # 等同于 :hello

Symbol经常用于方法的反射机制,比如我们经常需要判断一个对象是否有each方法:
o.respond_to? :each

下面这个例子用于判断一个对象是否包含一个特定的方法,如果有,就调用它:
name = :size
if o.respond_to? name
o.send(name)
end

你可以通过intern或者to_sym将一个字符串转换为Symbol,反过来你可以使用to_s或者id2name方法将Symbol转换为字符串:
str = "string"     # 定义一个字符串
sym = str.intern # 转换为 symbol
sym = str.to_sym # 另一种转换方法
str = sym.to_s # 在转换回字符串
str = sym.id2name # 另一个转换成字符串的方法
最后我们来总结一下,拥有相同内容的字符串是不同的对象,但是拥有相同内容的字符串转换成的Symbol是一样的,不同的Symbol内容总是不同的。
如果你的程序中用到了字符串,而这个字符串可能表示的是一个唯一的内容,你应该考虑使用Symbol,所以如果你使用字典对象,那么最好用Symbol作为元素的“键”。
在Ruby1.9中增加了一系列String对象中的方法,比如length,size等等,你可以使用这种方式,实现类似不可变字符串的功能。

Sunday, October 19, 2008

(16)Ruby中的范围对象Rnage

Range对象描述了一段开始和结束的值,值与值之间用两个或三个点号隔开,如果用两个点号表明范围的值包括开始和结束点的所有值,如果是三个点号,表示范围的值包括开始到结束,但是不包括最后一个结束值:
1..10      # 表示1到10之间的值,包括10
1.0...10.0 # 表示1.0到10.0之间的值,但是不包括10.0

可以通过include?方法判断Range是否包含一个指定的值:
cold_war = 1945..1989
cold_war.include? birthdate.year

Rnage对象的另一个用途是迭代,比如step,each,以及Enumerable方法:
r = 'a'..'c'
r.each {|l| print "[#{l}]"} # 打印"[a][b][c]"
r.step(2) { |l| print "[#{l}]"} # 打印"[a][c]"
r.to_a # => ['a','b','c']:to_a方法将Range对象转换为数组

succ方法用于返回当前字符的下一个字符,比如:'a'.succ是'b','b'.succ是'c'以此类推,Range对象的内容排序就是依据这个规则。注意如果你想直接在一个Range对象上调用方法,你必须用()包含他们:(1..3).step{#..some method}:
1..3.to_a    # 这样是错误的
(1..3).to_a # => [1,2,3],必须用()包含。

在Ruby1.8中定义了include?和member?用于测试一个值是否包含在Range对象中,Ruby1.9中定义了另一个方法cover?它实现了相同的功能,但是这个方法的效率更高:
triples = "AAA".."ZZZ"
triples.include? "ABC" # true; 在1.8中执行效率要高于1.9
triples.include? "ABCD" # true 在1.8中可以运行,在1.9中返回false
triples.cover? "ABCD" # true 1.9中效率更高
triples.to_a.include? "ABCD" # 返回false在 1.8 和 1.9中效率头很低

(15)Ruby 中的字典类

一个字典对象就是包含一系列键/值对映射的数据结构。它和数组类似,不过,数组只能通过整数索引来操作数组内的元素,而字典对象中的“索引”可以是任何对象,下面是一个例子:
numbers = Hash.new     # 创建一个新的、空字典对象
numbers["one"] = 1 # 将字符串"one"映射到Fixnum 1上
numbers["two"] = 2 # 这里类似于数组的下标赋值方式,但是这里的下标可以是任何对象
numbers["three"] = 3

sum = numbers["one"] + numbers["two"] # 通过下标的方式取得字典对象中的元素

一个字典对象中的元素包含在一对{}中,其中的元素是由逗号分隔的键/值对,键和值之间通过一个=>分隔。我们上面例子中的字典对象还可以写成这样:
numbers={"one"=>1,"two"=>2,"three"=>3}

通常我们使用符号对象作为字典元素的
numbers={:one=>1,:two=>2,:three=>3}

Ruby1.8还支持另一种全部用逗号分隔的写法,但是Ruby1.9中将不再支持这种方式:
numbers = { :one, 1, :two, 2, :three, 3 } # 可以实现相同的功能但是可读性不好

Ruby 1.9中提供了另一种方便而精炼的定义方式,直接用符号对象,但是冒号放到了符号对象的右边,而且取消了=>的使用:
numbers = { one: 1, two: 2, three: 3 }

注意冒号的两边不能有空格。
Ruby 中的字典对象其实是实现了哈希表,这意味着字典元素中的”键“必须有一个hash方法,这个方法返回”键“的Fixnum型的哈希代码。那么,在比较字典元素的键是否相等时,实际上比较的是他们的哈希代码。一个字典类通过eql?方法来比较元素的键是否相等,所以如果你用到了自定义对象作为”键“,那么你的自定义对象必须重载hash方法,如果你不重载这个方法那么即使你的对象内容是一样的,那么在调用eql?方法进行比较的时候,结果还是false,因为缺省情况下,hash方法返回的是对象的ID,而每一个对象的ID都是唯一。
注意,使用可变对象作为”键“可能会带来麻烦,因为如果你更改了可变对象的值,你实际上也更改了他的哈希代码。
字符串对象是一个可变对象,但是它经常作为字典对象的键,所以Ruby对它进行了特殊的处理,如果一个字符串对象作为键的时候,Ruby会创建一个”私有的对象拷贝“。但是这只是一个特例,在你使用其他可变对象作为键的时候,你应该考虑也”创建一个私有的对象拷贝“或者调用freeze方法,防止这个对象被更改。如果你不得不修改这个可变的”键“,那么记住在你修改这个键之后调用Hash类的rehash方法。否则字典对象中的元素会出现错误。

Thursday, October 16, 2008

(14)Ruby中的数组Arrays

Ruby中的数字就是一系列的对象,你可以通过位置或者说索引来访问数组中的元素。在Ruby中数组的第一个元素的索引是0,数组的长度是数组中元素的个数,Ruby数组允许负数索引,最后一个元素的索引是-1,倒数第二个的索引是-2,一次类推,如果你试图使用一个超出范围的索引方法数组你会得到一个nil值,而不会出现异常。
Ruby的数组是可变的,而且无类型的,也就是说你可以在任何时候改变数组内的元素,数组内的元素与不必全部是相同的类型。另外,数字的长度是动态可变的,如果你向其中增加一个元素,数组的长度就会自动扩展,如果你用一个越界索引给数组赋值,它会用一个nil值来扩展数组。
声明一个数组要使用[],其中的元素用逗号分隔:

[1, 2, 3]         #  一个包含3个Fixnum对象的数组。
[-10...0, 0..10,] # 这个数组包含两个Range对象,结尾的逗号是允许的。
[[1,2],[3,4],[5]] # 包含数组的数组
[x+y, x-y, x*y] # 数组元素还可以是表达式
[] # 空数组的长度为0

用%w方式你可以把用分割符号包含的;用空格隔开的不同字符串定义为一个数组:
words = %w[this is a test]  # 等同于: ['this', 'is', 'a', 'test']
open = %w| ( [ { < | # 等同于: ['(', '[', '{', '<']
white = %W(\s \t \r \n) # 等同于: ["\s", "\t", "\r", "\n"]

用%w或%W定义数组的方式类似于用%q和%Q定义字符串,分界符号的规则也一样,在分界符号中的元素无需分号('',“”)包含,无需逗号分隔,不同元素之间用空格隔开。
你还可以通过Array.new 来定义数组,这种方式可以让你通过程序化的手段还初始化数组:
empty = Array.new       # []: 创建一个空的数组
nils = Array.new(3) # [nil, nil, nil]: 包含3个nil的数组
zeros = Array.new(4, 0) # [0, 0, 0, 0]: 创建包含4个0的数组
copy = Array.new(nils) # 创建另一个数组对象的拷贝
count = Array.new(3) {|i| i+1} # [1,2,3]: 通过计算得到的数组,你可以在数组声明后加上一个块,来填充数组

可以通过[一个整数索引] 的方式获得数组元素的值。
a = [0, 1, 4, 9, 16]   # 创建一个数组
a[0] # 第一个元素是 0
a[-1] # 最后一个元素是 16
a[-2] # 倒数第二个元素是 9
a[a.size-1] # 另一种获得最后一个元素的方法
a[-a.size] # 另一种获得第一个元素的方法
a[8] # 越界访问会得到一个 nil值
a[-8] # 越界访问会得到一个 nil值

可以通过赋值的方法修改数字内的元素:
a[0] = "zero"   # a 变成 ["zero", 1, 4, 9, 16]
a[-1] = 1..16 # a 变成 ["zero", 1, 4, 9, 1..16]
a[8] = 64 # a 变成 ["zero", 1, 4, 9, 1..16, nil, nil, nil, 64],增加了新的元素。
a[-9] = 81 # 这里会抛出异常,你不能向数组第一个元素之前赋值

和String对象的操作一样,你同样可以使用两个整数下标或者一个Range对象下标的方式获得一个子数组:
a = ('a'..'e').to_a   # to_a方法将一个Range对象转换为数组 ['a', 'b', 'c', 'd', 'e']
a[0,0] # []: 得到一个空的数组
a[1,1] # ['b']: 得到包含一个元素的数组
a[-2,2] # ['d','e']: 得到包含最后两个元素的数组
a[0..2] # ['a', 'b', 'c']: 包含前面3个元素的数组
a[-2..-1] # ['d','e']: 通过Range的方式获得最后两个元素
a[0...-1] # ['a', 'b', 'c', 'd']: 除最后一个元素外所以元素的数组

可以通过赋值的操作将数组中的子数组修改成其他的内容:
a[0,2] = ['A', 'B']      # a 变成了 ['A', 'B', 'c', 'd', 'e']
a[2...5]=['C', 'D', 'E'] # a 变成了['A', 'B', 'C', 'D', 'E']
a[0,0] = [1,2,3] # 在数组 a之前增加3个元素
a[0..2] = [] # 删除这些元素
a[-1,1] = ['Z'] # 用一个新值替换最后一个元素
a[-1,1] = 'Z' # 如果只修改一个元素,赋值的对象不必一定是数组
a[-2,2] = nil # 在Ruby 1.8中会删除最后两个元素; 在Ruby 1.9中会将最后两个元素替换为nil对象。

你还可以用+符号链接两个数组:
a = [1, 2, 3] + [4, 5]    # [1, 2, 3, 4, 5]
a = a + [[6, 7, 8]] # [1, 2, 3, 4, 5, [6, 7, 8]]
a = a + 9 # 这里会抛出异常,因为只有都是数组的情况下才可以“相加

你还可以使用-符号来从数组中移除指定的元素:


a=['a', 'b', 'c', 'd', 'e']
b=['b', 'c', 'b']
c=a-b

puts "a:#{a},b:#{b},c:#{c}"

输出结果是:
a:["a", "b", "c", "d", "e"],b:["b", "c", "b"],c:["a", "d", "e"]

包含在b中的元素被从a中移除了,但是这个操作不会改变数组a和b本身,只是返回计算结果的拷贝。
我们前面提到的+操作创建了一个链接后的新的数组但是不改变数组本身,而<<会向数组中追加元素,数组本身的内容也发生了改变
a = []        #创建空的数组
a << 1 # a 变成 [1]
a << 2 << 3 # a 变成 [1, 2, 3]
a << [4,5,6] # a 变成 [1, 2, 3, [4, 5, 6]]

和String一样数组也提供了乘法操作:
a = [0] * 8    # [0, 0, 0, 0, 0, 0, 0, 0]

数组对象还引入了布尔操作符:|和&,用他们来取两个数组的交集和并集。|操作将两个数组的重复元素移除并返回两个数组的并集,&操作返回两个数组的交集。
a = [1, 1, 2, 2, 3, 3, 4]
b = [5, 5, 4, 4, 3, 3, 2]
a | b # [1, 2, 3, 4, 5]: 重复元素被移除
b | a # [5, 4, 3, 2, 1]: 元素内容相同,但是顺序变了
a & b # [2, 3, 4]
b & a # [4, 3, 2]

Ruby为数组定义了一系列非常有用的方法,我们这里只介绍一下each方法:
a = ('A'..'Z').to_a    # 创建一个数组
a.each {|x| print x } # 迭代打印出数组中的每个元素

还有很多其他有用的方法诸如:clear, compact!, delete_if, each_index, empty?, fill, flatten!, include?, index, join, pop, push, reverse, reverse_each, rindex, shift, sort, sort!, uniq!, 和 unshift.,我们将在以后的章节进行介绍。

(13)字符串编码和多字节字符

较之Ruby1.8,Ruby1.9中的String对象有了跟不上的改变。
  • 在Ruby1.8中字符串对象是一个字节序列,当字符串用于描述文本的时候,每一个字节都被当作一个单独的ASCII字符。在Ruby1.8中字符串中的一个元素不是字符而是一个数字,这个数字是字符编码的实际的值。
  • 在Ruby1.9中字符串对象是一个真正的字符序列,并且这些字符并不一定是ACSII字符集内的。在Ruby1.9中字符串中的一个元素是一个字符而不再是一个ACSII码数字,所以字符串是有其自己的编码方式的。
Ruby1.9重写了String对象以便对多自己编码提供更好的支持。如果一个字符串对象包含多字节字符,那么字符串的字节的长度和其字符长度就不同了。在Ruby1.9中length和size返回的是字符串中字符的个数,并且提供了一个新的方法bytesize返回字符串的字节长度:


# -*- coding: utf-8 -*- # 指定Unicode UTF-8

# 这个字符串中包含一个多字节字符
s = "2x2=4"

# 这个字符串的字符长度是5,但是字节长度为6
s.length # => 5: 字符: '2' 'x' '2' '=' '4'
s.bytesize # => 6: 字节(hex): 32 c3 97 32 3d 34

注意,你必须在第一行指定字符的编码,比如utf-8,否则Ruby解析器不知道该如何解码你的多字节字符。
在包含多字节字符的文本中如果你使用[]方式来查找文本中的内容的话,实际上程序会从头到尾的读取这个文本,这样会带来效率问题,一个好的做法是你自己通过each_char的方式来查找字符串的内容,当然如果你没有这样使用,问题也不大,Ruby会自动调优你的程序。
Ruby1.9的字符串定义了一个encoding 方法,你可以通过它得到字符串的编码格式,比如:
s="你好"
s2="Heelo"
puts s.encoding
puts s2.encoding

打印结果为:
UTF-8
ASCII-8BIT

String对象的操作,比如连接两个字符串或者匹配查找,需要两个字符串的编码是兼容的,比如你将一个UTF-8的字符串与一个ACSII编码的字符串相链接,这是可以的,因为UTF-8和ASCII是兼容的,但是如果你将一个UTF-8和一个SJIS的字符串连接或出现异常。你可以通过Encoding.compatible?方法来判断两个字符串是否是编码兼容的,如果是,那么这个方法的返回值为兼容性更好的字符编码:
#-- encoding: utf-8 --
s="你好"
s2="Heelo"
puts Encoding.compatible?(s,s2)

返回值为:UTF-8

你还可以指定用什么编码来处理一个文本,比如有的时候你可以要读取一个文本,并且告诉Ruby用那种编码格式来处理它:
text = stream.readline.force_encoding("utf-8")
bytes = text.dup.force_encoding(nil) # nil 代表字节编码

force_encoding方法返回的不是一个对象拷贝,它会直接改变对象本身的内容。force_encoding 不会校验文本是否符合你给出的编码规格,如果你要校验它必须调用valid_encoding?:
= "\xa4".force_encoding("utf-8")  # 这不是一个UTF-8字符串
s.valid_encoding? # => false

encode 方法和force_encoding不同,他会返回一个字符串拷贝,原有的字符串不会变化:
# -*- coding: utf-8 -*-
euro1 = "\u20AC" # 定义一个unicode字符串
puts euro1 # 打印"€"
euro1.encoding # => <Encoding:UTF-8>
euro1.bytesize # => 3

euro2 = euro1.encode("iso-8859-15") # 转换为Latin-15
puts euro2.inspect # 打印"\xA4"
euro2.encoding # => <Encoding:iso-8859-15>
euro2.bytesize # => 1

euro3 = euro2.encode("utf-8") # 转换为 to UTF-8
euro1 == euro3 # => true

通常你很少直接使用encode方法,我们大多数的应用是将文本写入文件或者网络,这是Ruby的I/O库会自动转换编码。当你指定一个转换的编码但是Ruby无法转换的时候会抛出一个异常。
如果你在Ruby1.8中遇到多字节编码,你可以使用jcode库,这是一个Ruby的标准库,你只要在代码的开头指定你要使用的编码就可以使用类似Ruby1.9中的each_char功能。
$KCODE = "u"        # 指定 Unicode UTF-8, 或者在Ruby命令行中开启 -Ku 选项
require "jcode" # 加载多字节字符支持库

mb = "2\303\2272=4" # 相当于"2x2=4" 不过使用了多字节字符编码
mb.length # => 6: length返回的长度是6,是字节长度
mb.jlength # => 5: jlength返回的长度是5,是字符长度
mb.mbchar? # => 1: 第一个出现多字节字符的位置,如果没有就返回 nil
mb.each_byte do |c| # 在字节码中迭代.
print c, " " # 变量c 是一个Fixnum的整数
end # 输出 "50 195 151 50 61 52 "
mb.each_char do |c| # 在字符中迭代
print c, " " # 变量c是一个jlength 为1 的字符串
end # 输出 "2 x 2 = 4 "