Showing posts with label Ruby中的数据类型与对象. Show all posts
Showing posts with label Ruby中的数据类型与对象. Show all posts

Monday, October 20, 2008

(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 "

(12)字符串迭代

Ruby1.8中定义了each方法来在字符串中一行行的迭代,String属于Enumable模块,你可以使用each_byte来字符串中的每一个字符编码,而且通过each_type结合下标[]的方式可以带来更好的性能。
Ruby1.9中String不再是Enumable的了,它明确的定义了3个方法:each_line用于迭代字符串中的每一行,each_char用于迭代字符串中的每一个字符,each_byte用于迭代每一个字节:
s = "¥1000"
s.each_char {|x| print "#{x} " } # 打印"¥ 1 0 0 0". Ruby 1.9
0.upto(s.size-1) {|i| print "#{s[i]} "} # 对于多字节字符串来说,性能更好

(11)字符串截取和查询操作

也许String对象最重要的操作就是通过[]下标访问操作了,你可以通过这种方式提取字符串内容或者更改字符串内容。在Ruby1.8中字符串被当作一个字节或者8位的字符码数组,你可以通过length或者size方法获得这个数组的长度,你通过在[]设置索引数字来设置或者得到那个位置的值:
s = 'hello';   # Ruby 1.8
s[0] # 返回值为104,第一个字母'h'的ASCII码
s[s.length-1] # 返回值为111,最后一个字母'o'的ASCII码
s[-1] # 返回值为111,你可以使用负数索引
s[-2] # 返回值为108,倒数第二个字母'l'的ASCII码
s[-s.length] # 104: 另一种访问第一个字符的方式
s[s.length] # nil: 如果超出索引范围就返回nil

注意的一点是,索引是正向从0开始的,如果你使用负数索引那么最后一个字母是从-1开始索引的。而且,在Ruby中如果你给出了一个超出索引范围的下标,Ruby不会抛出异常,而是返回一个nil。
在Ruby1.9中返回值为一个字符串,而不是字符的编码:
s = 'hello';   # Ruby 1.9
s[0] # 返回'h'
s[s.length-1] # 返回'o'
s[-1] # 返回'o'
s[-2] # 返回'l'
s[-s.length] # 返回'h'
s[s.length] # nil:如果超出索引范围就返回nil

如果要修改字符串内容,只要给相应位置赋值就可以了,Ruby1.8中你可以使用字符编码或者字符串作为赋值内容,在Ruby1.9中你应该使用字符串:
s[0] = ?H        # 将第一个字符替换为大写的H
s[-1] = ?O # 将最后一个字符替换为大写的O
s[s.length] = ?! # 这里会出现异常,因为你的赋值超出了索引的访问

等号右边可以是任意的字符串,多字节字符串也是允许的,或者是一个空的字符串,一下代码在Ruby1.8和1.9都可以正常运行:
s = "hello"      # 初始化字符串
s[-1] = "" # 删除最后一个字符,现在s的值为hell
s[-1] = "p!" # 修改最后一个字符现在s的值为 "help!"

大多数情况下,你需要取得的是字符串中的一段内容,而不是一个单独的字符。要截取字符串中的一段内容你可以在[]中给出两个用逗号隔开的数字,通过这种方式你就可以得到一段文本了。其中第一个数字指出了我们要截取的文本从哪个位置开始索引,第二个字符指出了我们要截取的内容的长度:
s = "hello"
s[0,1] = "H" # 将第一个字符替换为一个大写的H
s[s.length,0] = " world" # 在字符我末尾增加其他的字符串
s[5,0] = "," # 在字符串中插入一个逗号
s[5,6] = "" # 通过给一段文本赋值空文本的方式来删除那段文本,现在s == "Hellod"

另一种截取或者插入、删除、修改字符串的方式是使用Range(范围)对象,Range对象通过两个数字之间插入两个联系的点号表示,比如1..3表示范围在1到3之间。

s = "hello"
s[2..3] # "ll": 位于索引2到3的字符
s[-3..-1] # "llo": 负数索引页可以使用
s[0..0] # "h": Range只包含一个字符
s[0...0] # "": 空的Range对象
s[2..1] # "": 这也是空的Range
s[7..10] # nil: 这个Range超出了索引范围,所以返回nil
s[-2..-1] = "p!" # 替换值,现在s=="help!"
s[0...0] = "Please " # 在起始位置插入新值,现在s== "Please help!"
s[6..10] = "" # 删除值,现在s== "Please!"

注意的一点是,用逗号分隔的方式,分别指出了索引的开始位置和截取字符的长度,而使用Range对象是制定了一段索引的范围,两者是不同的。
另外,你还可以使用字符串作为索引,当你这样做的时候,你得到的是字符串中包含这个字符的第一个,如果查找不到你给出的字符,就返回一个nil对象:

Created with colorer-take5 library. Type 'ruby'

s = "hello"
while(s["l"]) # 循环判断是否包含字符 "l"
s["l"] = "L"; # 将找到的字符从 "l" 替换为 "L"
end # 现在s=="heLLo"

最后要说的是,你还可以在[]中包含正则表达式,这样会得到一个符合正则表达式的内容:


s[/[aeiou]/] = '*'
# 将找到的第一个原音替换为星号


(10)字符串操作

String对象定义了一些列方法来操作字符串,使用+可以连接两个字符串:
planet = "Earth"
"Hello" + " " + planet # 输出 "Hello Earth"

Java 程序员需要注意Ruby并不会把要连接的对象自动转换为字符串,你必须调用to_s方法,或者可以使用字符串表达式#{}:
plant_number=2
print "hello "+plant_number.to_s()
print "hello ##{plant_number}"

<<操作符与+操作符类似,他会链接多个字符串,但是和+操作的一个重要区别是,他会改变链接的左边的字符串的值,比如:
greeting = "Hello"
greeting << " " << "World"
puts greeting # 输出"Hello World"

和+操作一样的是<<操作也不会自动转换对象为字符串,那么如果你<<一个整数,那么他会被当作一个字符的ASCII码来使用,除非你明确的调用了to_s方法:

alphabet = "A"
alphabet << ?B # Alphabet 值为 "AB"
alphabet << 67 # 现在的值为"ABC"
alphabet << 256 # 在Ruby 1.8这是不允许的,Ruby1.8只支持0~255之间的整数Ruby1.9没有限制,除非他超出了可表示的字符ASCII的范围

字符串右边*一个整数操作,代表重复输出n次当前的字符串。
ellipsis = '.'*3    # 结果为 '...'

如果*左边的字符串是一个表达式,那么规则是先计算表达式然后对结果进行*操作:
a = 0;
"#{a=a+1} " * 3 # 结果为"1 1 1 ", 而非 "1 2 3 "

String对象定义了所有的比较操作符:==,!=,<, <=, >, 和 >=。
==和!=比较两个文本是否完全相同,这和Java的对象比较是不同的,在Java中你必须调用object.equals(other)方法。其它的比较是按照字符所代表的编码符号的大小进行比较的,短字符串总是小于长字符串。字符串比较是大小写敏感的,因为他们代表的ACSII码是不同的。

(9)here documents

有时候我们的字符串会很长,使用分界符号的方式可能也行不通,比如我们有时候可能忘了什么地方需要进行转义(\)。Ruby提供了另一个方便的方法,在你使用长文本内容的时候非常有用。
你可以以<<或者<<-符号开始,然后紧跟一个你自定义的字符串,长文本内容的结束部分使用相同的自定义字符串告诉Ruby这个长文本已经结束了:
document = <<HERE      #以<<定义HERE为文本开始的关键字 
This is a string literal.
It has two lines and abruptly ends...
HERE
#以相同的HERE关键字结尾,注意这里的HERE结束关键字必须在单独的一行,并且其后不能有任何字符,空格也不行

如果有多个here documents的时候,Ruby会顺序的读取其中的内容,读完一个然后返回,从头读另一个,比如下面的文档输出结果为:Hello there world:
greeting = <<HERE + <<THERE + "World"
Hello
HERE
There
THERE

如果你希望你的文本非常的“文本化”,比如给他加上某些说明,你甚至可以定义一段字符串为开始和结束标记:
document = <<'THIS IS THE END, MY ONLY FRIEND, THE END'
.
. lots and lots of text goes here
. with no escaping at all.
.
THIS IS THE END, MY ONLY FRIEND, THE END

注意,结束标记处,没有单引号。
同样你还可以使用双引号为标记的文本。

最后再提一点,字符串对象是可变的,在Ruby中你不能用同一个对象表示两个相同的文本(Java程序员会觉得这很奇怪),每当Ruby解析到一个字符串文本,他就会创建一个新的对象,也就是说如果你在一个循环中使用了字符串文本,那么每个循环都会创建一个新的对象,你可以试试下面的代码:
10.times { puts "test".object_id }

基于性能的考虑,你应该避免这样作。
另外,你可以使用String.new的方式创建一个空的字符串对象,这和声明一个空的文本是一样的效果。

(8)文本的任意分隔符

我们在处理文本中的引号('或者")比较麻烦,如前所述我们使用了\ 操作,Ruby还提供了一个通用的解决方案,使用%q,%Q的方式,用%q和%Q加上分解符号<>,{},[],(),--或者||,那么你可以在其中输入任意内容而不必担心引号的问题,关键的一点是这些符号必须成对出现,你甚至可以使用-- \\
%q用于表示单引号的文本。
%Q用于表示双引号的文本。
而且q和Q也是可以省略的你可以直接用%

%q(Don't worry about escaping ' characters!)
%Q|"How are you?", he said|
%-This string literal ends with a newline\n- # 这里的Q被省略了

如果你的字符串中包含分界符号,你可以使用\来标明它们:
%q_This string literal contains \_underscores\__
%Q!Just use a _different_ delimiter\!!

如果字符串中存在成队的分界符号,他们会直接被输出,但是如果只有一个你就需要用\进行转义:
<

Created with colorer-take5 library. Type 'ruby'

# XML格式使用了成对的括号:
%<<book><title>Ruby in a Nutshell</title></book>> # 这可以正常运行
# 计算表达式使用{}:
%((1+(2*3)) = #{(1+(2*3))}) # 这也没问题
%(A mismatched paren \( must be escaped) # 其中的第二个(需要被转义

(7)在Ruby中使用Unicode

Ruby1.9中对双引号字符提供了Unicode支持\u,\u关键字后跟随的是4位16进制的字符(字母不区分大小写):

"\u00D7" # => "x": 前面的0可以被省略
"\u20ac" # => "€": 小写字母也是可以的

第二种方式是使用\u{}的形式,括号中的十六进制字符可以是1到6个:
"\u{A5}"      # => "¥": 等同于 "\u00A5"
"\u{3C0}" #返回希腊字母 派 : 等同于 "\u03C0"
"\u{10ffff}" # 最大的 Unicode 字符

使用\u{}的另一个优势是,可以在一个{}中嵌入多个十六进制字符,它们之间用空格分开:
money = "\u{20AC A3 A5}"  # => "€£¥"

如你所见,{}之间的空格被忽略了,如果你确实需要空格可以使用ASCII 20:
money = "\u{20AC 20 A3 20 A5}"  # => "€ £ ¥"

值得注意的是,如果你想使\u有效,那么你的源文件必须是utf-8的,如果你使用其它编码格式,那么\u是无效的。而且\u模式不仅仅可以用于双引号字符串中,还可以用于正则表达式,但是如果你是Java程序员,你可能注意到了,Ruby中的\u是不能用于标识符的。

Wednesday, October 15, 2008

(6)Ruby中的文本

Ruby通过String类对象来描述文本,Ruby为String定义了一些列非常有用的方法来操作文本:截取字符串,插入,替换,删除等等操作。提到字符串我第一个想到了正则表达式,Ruby提供了Regexp类对象来实现这一功能。比如:/[a-z]\d+/ 表示查找所有以小写字母开头后面跟1到多个数字的文本。值得一提的是Ruby1.9比起1.8提供了对Unicode和其他多字节文本的支持,我在后面的部分会予以介绍。
单引号字符
你可以使用单引号('')包含一段文本,单引号之间的内容就是字符串的值:
'This is a simple Ruby string literal'

那么如果我们的文本中本身就包含单引号怎么办呢?答案是使用反斜杠(\),Ruby解析器在遇到单引号前面有一个反斜杠的时候,不会认为这是一段字符串的终止,而且单引号之前的第一个反斜杠也会被忽略:

'Won\'t you read O\'Reilly\'s book?'


说一下面的例子中,如果你答应这个字符串,你只会看到一个反斜杠:
'This string literal ends with a single backslash: \\'

实际上如果一段文本中存在一个\而且其后跟有一个字符那么他总是被Ruby解析器忽略,比如下面的等式是成立的:
'a\b' == 'a\\b'

最后在来说话文本的换行,你知道,我们的文本有时候会很长,如果把它写在一行里,天哪,看起来多臭啊,利用反斜杠你就可以解决这个文本,下面的例子中Ruby会把反斜杠当作多个文本的链接,你最终得到的是一行文本而不是多行:
message =
'These three literals are '\
'concatenated into one by the interpreter. '\
'The resulting string contains no newlines.'

双引号文本
双引号文本提供了比单引号方式更好的功能,你可以用\n表示换行,\t表示制表符,\'表示文本中的一个单引号。
puts "\t\"这段文本以制表符开始以换行符号结束\"\n"
puts "\\" # 输出一个反斜杠

Ruby1.9中提供了\u 方式在双引号中描述一段Unicode文本,这里还有其他很多\开头的应用,随后我会给出一个列表来说明他们的用法。
Ruby字符串中另一的强大的功能是你可以在一个双引号字符串中嵌入任意的Ruby表达式。这种表达式以#开头,其后是一对{}你的表达式就包含在括号中。
"360 degrees=#{2*Math::PI} radians" # "弧度与角度的计算公式"
如果嵌入的表达式引用的是一个全局变量、实例变量或者类变量,通常{}是可以省略的,比如:

Created with colorer-take5 library. Type 'ruby'

$salutation = 'hello' # 定义一个全局变量
"#$salutation world" # 在双引号字符串中应用

一个特殊的情况下,比如你的字符串中包含#且其后出现{,$或者@,但是这又不是一个表达式,你当然不希望Ruby按表达式来解析它们,怎么办呢,方法是使用反斜杠,


Created with colorer-take5 library. Type 'ruby'

"My phone #: 555-1234" # 这种情况下没有文本
"Use \#{ to interpolate expressions" # 用反斜杠来告诉Ruby#{后面不是表达式

C语言程序员知道Ruby中支持sprintf和printf:
sprintf("pi is about %.4f", Math::PI) # Returns "pi is about 3.1416"
只有做的好处是你可以知道你的文本输出格式.
Ruby 还提供了另一种方式,使用%符号:

"pi is about %.4f" % Math::PI # 想过与上面的方式相同
"%s: %f" % ["pi", Math::PI] # 左边是参数,右边是一个数组包含了每个参数的值

双引号之间的字符可以直接换行,你可以使用一个反斜杠来连接字符:
print "This string literal
has two lines \
but is written on three"

打印结果是:
This string literal
has two lines but is written on three.
下列了所有反斜杠操作的含义:
  • \ x :它与的含义和x本身一样,x可以是任何字符,它的作用是告诉Ruby\后面的内容就是它自己,没有任何特殊含义,尤其是遇到某些特殊符号的时候。
  • \a:如果在控制台运行PC会发出Bi的一声。
  • \b:代表退格键
  • \e:代表Ese键
  • \f:换页符号
  • \n:换行符
  • \r:回车符号
  • \s:空格符号
  • \t:制表符号
  • \u nnnn:Unicode代码,一种n代表一个十六进制的代码。
  • \u{ hexdigits }:Unicode代码的特殊用法。
  • \v:垂直线符号
  • \ nnn:nnn代表八进制符号(000-377)
  • \ nn:nn代表八进制符号(00-77)
  • \ n:n代表八进制符号(0-7)
  • \x nn:nn是两个16进制符号(00-ff),注意这里的字母代表所有的大小写字母。
  • \x n::n是一个16进制符号(0-f),注意这里的字母代表所有的大小写字母。
  • \ eol:终止符号

Tuesday, October 14, 2008

(5)二进制浮点运算中的四舍五入错误

大多数硬件平台和程序语言都提供了浮点数运算,比如Ruby中的Float类。由于硬件的原因,大部分浮点运算都采用二进制浮点运算来提高精度,他可以精确的描述类似1/10,1/100,1/1024等的小数部分,但是不幸的是并不像我们想的那样1/10的结果是0.1。
Ruby的Float类的计算结果可以无限接近0.1但是这还是无法满足我们的要求,比如看看下面这个例子:


0.4 - 0.3 == 0.1 # 两边是不等的

是不是很奇怪,原因是因为0.4-0.3的结果取近似值后与0.1不同。这个问题不是只存在于Ruby中所有基于IEEE-754 浮点运算规范的语言比如C,Java,JavaScript都存在这样的问题。
一个解决的办法是使用10进制计算代替二进制计算,Ruby标准库中提供了BigDecimal 类可以很好的解决我们的问题,尽管BigDecimal 的性能比Float类要差,但是应付我们平常的计算已经足够了(只要你不是编写科学计算程序)。