赞 | 0 |
VIP | 0 |
好人卡 | 0 |
积分 | 1 |
经验 | 163587 |
最后登录 | 2020-5-5 |
在线时间 | 41 小时 |
Lv1.梦旅人
- 梦石
- 0
- 星屑
- 60
- 在线时间
- 41 小时
- 注册时间
- 2008-3-5
- 帖子
- 2072
|
- 尽管技术上不属于Ruby语言的一部分,但解释器和标准库会使用各种各样的协议来处理其他语言在使用类型时遇到的一些问题。
- 一些对象会有多种自然表示。比如,你可能会编写一个类来表示罗马数字(I, II, III, IV, V等等)。这个类并不一定是Integer的子类,因为其对象是数字的表示,自己未必就是数字。同时它们真的有点类似整数(interger)的特点。无论何时当Ruby预期见到整数时,如果能够使用我们罗马数字类的对象,就太好了。
- 为此,Ruby提供了转换协议(conversion protocols)的概念——对象可以选择把自己转换成另外一个类的对象。Rudy有三种标准的方式来实现它。
- 我们已经遇到了第一种方式。诸如to_s和to_i方法分别把它们的接收者转换成字符串和整数。这些转换方法不是非常严格的:比如,如果对象有某种得体的字符串表示,它可能会有to_s方法。为了返回数字的字符串表示(比如VII),我们的Roman类可能会实现to_s方法。
- 第二种形式的转换函数使用名字如to_str和to_int的方法。它们是严格的转换函数:只有当对象能够很自然地用在字符串或者整数能够使用的任何地方,才应该实现它。比如,罗马数字的对象可以清楚地表示一个整数,因此应该实现to_int方法。但是涉及字符串的严格转换时,我们得好好想一想。
- 罗马数字显然可以用字符串来表示,但是这些数字是字符串吗?我们能在任何使用字符串的地方使用这些数字吗?不,可能不。从逻辑上讲,它们是数字的表示。你可以把它们表示成字符串,但是它们和字符串并非是插入兼容(plug-compatible)。因为这个原因,罗马数字不应实现to_str——它不是真正的字符串。总结一下:使用to_s,罗马数字可以转换成字符串,但是它本质上不是字符串,因此没有实现to_str。
- 为了看看这实际上是如何工作的,我们考察一下打开的文件。 File.new的第一个参数可能是已有的文件描述符(用数字表示)或是要打开的文件名。当然,Ruby不是只看第一个参数还检查类型是否是Fixnum 或String。相反,它给被传递进来的对象一个机会,让这个对象以数字或字符串来表示自己。如果用Ruby编写,它会写成这样:
- class File
- def File.new(file, *args)
- if file.respond_to?(:to_int)
- IO.new(file.to_int, *args)
- else
- name = file.to_str
- # call operating system to open file 'name'
- end
- end
- end
- 如果想传递保存在罗马数字中的文件描述符整数到File.new中,我们看看到底发生了什么。因为我们的类实现了to_int,第一个 respond_to?测试会成功、我们把数字的整数表示传递到IO.open,同时返回文件描述符,所有这一切都被包装到新的IO对象中。
- 少数几个严格的转换函数被内建到标准库中。
- to_ary→Array
- 用在当解释器需要把对象转换成数组以传递参数或进行多个赋值时。
- class OneTwo
- def to_ary
- [ 1, 2 ]
- end
- end
- ot = OneTwo.new
- a, b = ot
- puts "a = #{a}, b = #{b}"
- printf("%d-­%d\n", *ot)
- 输出结果:
- a = 1, b = 2
- 1 ­- 2
- to_hash→ Hash
- 用在当解释器期待看到散列表时(唯一已知的用法是Hash#replace的第二个参数)。
- to_int → Integer
- 用在当解释器期待看到整数值时(如文件描述符或Kernel.Integer的参数)。
- to_io → IO
- 用在当解释器期待IO对象时(比如,IO#reopen或IO.select的参数)。
- to_proc → Proc
- 用在转换方法调用中的前缀有&符号的对象。
- class OneTwo
- def to_proc
- proc { "one-two"}
- end
- end
- def silly
- yield
- end
- ot = OneTwo.new
- silly(&ot) → "one-two"
- to_str → String
- 普遍用在任何解释器寻找String值的地方。
- class OneTwo
- def to_str
- "one-two"
- end
- end
- ot = OneTwo.new
- puts("count: " + ot)
- File.open(ot) rescue puts $!.message
- 输出结果:
- count: one-two
- No such file or directory ­one-two
- 请注意,当然to_str使用的不是很普遍——一些想要字符串参数的方法不会调用to_str。
- File.join("/user", ot) → "/user/#<OneTwo:0x1c974c>"
- to_sym→Symbol
- 表示接收者是符号。解释器没有使用它进行转换并且可能在用户代码中是无用的。
- 最后要说明的是:诸如Integer和Fixnum的类实现了to_int方法,String类实现了to_str方法。这样可以多态地调用这些严格的转换函数:
- # it doesn't matter if obj is a Fixnum or a
- # Roman number, the conversion still succeeds
- num = obj.to_int
- 23.3.1 数字强制转换
- Numeric Coercion
- 在第372页讲过解释器可以执行三种类型的转换。已经讨论了宽松的和严格的转换。第三种是数字强制转换(numeric coercion)。
- 问题是这样的。当写“1+2”时,Ruby知道在对象1(Fixnum)上调用+,把Fixnum 2作为参数传递给它。但是当写“1+2.3”时,同样的+方法现在接受Float参数。它如何知道该做些什么呢(尤其详细检查参数的类型违背了duck typing的精神)?
- 答案在Ruby中的基于coerce方法的强制(coercion)协议。coerce的基本操作很简单。它接受两个数字(一个是接受者,另外一个是参数)。它返回包含两个元素的数组,分别是两个数字的表示(但是参数在前,后面跟着接受者)。coerce方法保证两个
- 对象会有相同的类,因此可以对它们进行相加(或乘,或比较,或任何别的什么操作)。
- 1.coerce(2) → [2, 1]
- 1.coerce(2.3) → [2.3, 1.0]
- (4.5).coerce(2.3) → [2.3, 4.5]
- (4.5).coerce(2) → [2.0, 4.5]
- 技巧在于接受者调用其参数的coerce方法来生成数组。这种技术被称为两次分发(double dispatch),它允许方法根据它的类并且还有参数的类来改变自身的行为。在这个例子中,我们让参数决定到底应该对哪个类的对象进行相加(或乘,除等等)。
- 假设正在编写新的类来进行算术计算。为了使用强制转换,需要实现coerce方法。它接受一些其他类型的数字作为参数,并返回一个数组,其中包含同一类的两个对象,它们的值与其参数及其自身相等。
- 对于罗马数字类来说这相当简单。每个罗马数字对象在内部把它的实际值以Fixnum方式保存在实例变量@value中。coerce方法检查看参数的类是否也是Integer。如果是,则返回参数和这个内部值。如果不是,则优先把两者都转换成浮点数。
- class Roman
- def initialize(value)
- @value = value
- end
- def coerce(other)
- if Integer === other
- [ other, @value ]
- else
- [ Float(other), Float(@value) ]
- end
- end
- # .. other Roman stuff
- end
- iv = Roman.new(4)
- xi = Roman.new(11)
- 3 * iv → 12
- 1.1 * xi → 12.1
- 当然,以此种方式实现的Roman类还不知道如何相加:无法在先前例子中编写“xi + 3”,因为Roman类没有“plus”方法。也许它就应当这样。但是不管这些,让我们为罗马数字实现相加方法吧。
- class Roman
- MAX_ROMAN = 4999
- attr_reader :value
- protected :value
- def initialize(value)
- if value <= 0 || value > MAX_ROMAN
- fail "Roman values must be > 0 and <= #{MAX_ROMAN}"
- end
- @value = value
- end
- def coerce(other)
- if Integer === other
- [ other, @value ]
- else
- [ Float(other), Float(@value) ]
- end
- end
- def +(other)
- if Roman === other
- other = other.value
- end
- if Fixnum === other && (other + @value) < MAX_ROMAN
- Roman.new(@value + other)
- else
- x, y = other.coerce(@value)
- x + y
- end
- end
- FACTORS = [["m", 1000], ["cm", 900], ["d", 500], ["cd", 400],
- ["c", 100], ["xc", 90], ["l", 50], ["xl", 40],
- ["x", 10], ["ix", 9], ["v", 5], ["iv", 4],
- ["i", 1]]
- def to_s
- value = @value
- roman = ""
- for code, factor in FACTORS
- count, value = value.divmod(factor)
- roman << (code * count)
- end
- roman
- end
- end
- iv = Roman.new(4)
- xi = Roman.new(11)
- iv + 3 → vii
- iv + 3 + 4 → xi
- iv + 3.14159 → 7.14159
- xi + 4900 → mmmmcmxi
- xi + 4990 → 5001
- 最后,应当小心使用coerce——尝试总是强制转换到更通用的类型,否则可能最终造成强制转换循环,在那里A试图强制转换到B,而B试图强制转换回到A。
复制代码 |
|