您需要注意的第一件事是类和类型之间的区别。
非常不幸的是,Java 混淆了这种区别,因为类总是类型(尽管 Java 中还有其他类型不是类,即接口、原语和泛型类型参数)。事实上,几乎每一本关于 Java 风格的书都会告诉你不要将类用作类型。此外,在他的开创性论文On Understanding Data Abstraction, Revisited中,William R. Cook 指出,在 Java 中,类描述的是抽象数据类型,而不是对象。接口描述了对象,所以如果你在 Java 中使用类作为类型,你就不是在做 OO;如果您想在 Java 中实现 OO,则唯一可以用作类型的就是接口,而唯一可以使用类的就是工厂。
在 Ruby 中,类型更像网络协议:类型描述了对象理解的消息以及它如何对它们做出反应。(这种相似性并非偶然:Ruby 的远祖 Smalltalk 的灵感来自后来成为 Internet 的东西。在 Smalltalk 用语中,“协议”是非正式用来描述对象类型的术语。在 Objective-C 中,这种非正式的协议的概念成为语言的一部分,主要受Objective-C影响的Java直接复制了这个概念,但将其重命名为“接口”。)
所以,在 Ruby 中,我们有:
module
(一种语言特性):代码共享和差异化实现的载体;不是类型
class
(语言特性):对象工厂,也是 IS-A module
,不是类型
- 协议(一种非正式的东西):对象的类型,以消息为特征,响应以及它如何响应它们
还要注意,一个对象可以有多个类型。例如,一个字符串对象同时具有“ Appendable ”(它响应<<
)和“ Indexable ”(它响应[]
)类型。
因此,回顾一下要点:
- Ruby 语言中不存在类型,只存在于程序员的头脑中
- 类和模块不是类型
- 类型是协议,以对象如何响应消息为特征
显然,协议不能在语言中指定,因此通常在文档中指定。尽管通常情况下,它们根本没有指定。这实际上并不像听起来那么糟糕:例如,对消息发送的参数施加的要求通常从名称或方法的预期用途中“显而易见”。此外,在某些项目中,预计面向用户的验收测试会起到这个作用。(例如,在不再存在的 Merb Web 框架中就是这种情况。在验收测试中对 API 进行了全面描述。)传递错误类型时得到的错误消息和异常通常也足以弄清楚该方法是什么需要。最后但并非最不重要的一点是,总是有源代码。
有几个众所周知的协议,例如each
混合所需的协议Enumerable
(对象必须each
通过一个接一个地产生其元素并self
在传递块时返回并Enumerator
在没有传递块时返回),Range
如果一个对象想要成为 a 的端点所需的协议Range
(它必须succ
用它的后继响应并且它必须响应<=
),或者<=>
混合所需的协议Comparable
(对象必须<=>
用-1
, 0
, 1
,响应或nil
)。这些也没有写在任何地方,或者只写在片段中,它们只是被现有的 Ruby 专家所熟知并被教导给新人。
一个很好的例子是StringIO
:它具有相同的协议,IO
但不继承自它,也不继承自一个共同的祖先(除了明显的Object
)。所以,当有人检查 时IO
,我不能传入 a StringIO
(对测试非常有用),但如果他们只是使用对象 AS-IF 它是 a IO
,我可以传入 a StringIO
,他们永远不会知道其中的区别。
当然,这并不理想,但与 Java 相比:许多重要的要求和保证也在散文中指定!例如,它在类型签名中的List.sort
什么地方说结果列表将被排序?无处!这仅在 JavaDoc 中提及。函数式接口的类型是什么?同样,仅在英文散文中指定。Stream API 有一个完整的概念库,这些概念没有在类型系统中捕获,例如不干扰和可变性。
对于这篇长篇文章,我深表歉意,但理解类和类型之间的区别以及在像 Ruby 这样的面向对象语言中什么是类型是非常重要的。
处理类型的最佳方式是简单地使用对象并记录协议。如果您想调用某些东西,只需调用call
; 不要求它是一个Proc
. (一方面,这意味着我不能通过 a Method
,这将是一个烦人的限制。)如果你想添加一些东西,只需调用+
,如果你想附加一些东西,只需调用<<
,如果你想打印一些东西,只需调用print
或puts
(后一个很有用,例如,在测试中,当我可以传入 aStringIO
而不是 aFile
)。不要试图以编程方式确定一个对象是否满足某个协议,这是徒劳的:它相当于解决了停机问题。YARD 文档系统有一个用于描述类型的标签。它是完全自由格式的文本。但是,有一种建议的类型语言(我不是特别喜欢,因为我认为它过于关注类而不是协议)。
如果您确实必须拥有一个特定类的实例(而不是满足某个协议的对象),那么您可以使用许多类型转换方法。但是请注意,一旦您需要某个类而不是依赖于协议,您就离开了面向对象编程的领域。
您应该知道的最重要的类型转换方法是单字母和多字母to_X
方法。这是两者之间的重要区别:
- 如果一个对象可以“稍微合理地”表示为一个数组、一个字符串、一个整数、一个浮点数等,它将响应
to_a
, to_s
, to_i
,to_f
等。
- 如果一个对象与 , , , 等的实例属于同一类型。它将响应, , ,等。
Array
String
Integer
Float
to_ary
to_str
to_int
to_float
对于这两种方法,可以保证它们永远不会引发异常。(如果它们完全存在,当然,否则NoMethodError
会引发 a。)对于这两种方法,都保证返回值将是相应核心类的实例。对于多字母方法,转换应该是语义无损的。(注意,当我说“有保证”时,我说的是已经存在的方法。如果你自己写,这不是保证,而是你必须满足的要求,这样它就成为其他人使用你的保证方法。)
多字母方法通常要严格得多,而且数量要少得多。例如,说nil
“可以表示为”空字符串是完全合理的,但是说nil
IS-AN 空字符串因此nil
响应to_s
而不响应 是可笑的to_str
。同样,浮点数to_i
通过返回其截断来响应,但它不响应to_int
,因为您不能无损地将浮点数转换为整数。
下面是一个来自 Ruby API 的示例:Array
s 实际上不是使用 OO 原则实现的。出于性能原因,Ruby 作弊。结果,您实际上只能使用类Array
的实际实例来索引Integer
,而不是任意的“类整数”对象。但是Integer
, Ruby 将首先调用,而不是要求您传入to_int
,以便您有机会仍然使用自己的类似整数的对象。但是,它不会调用to_i
,因为用非整数的东西索引数组是没有意义的。只能“在某种程度上合理地表示”为一个。OTOH, Kernel#print
, Kernel#puts
, IO#print
, IO#puts
, 和朋友打电话to_s
根据他们的论点,让您可以合理地打印任何对象。并Array#join
调用to_str
它的参数,但to_s
在数组元素上;一旦你理解了为什么这是有意义的,你就更接近于理解 Ruby 中的类型了。
以下是一些经验法则:
- 不要测试类型,只需使用它们并记录它们
- 如果你绝对肯定 必须有一个特定类的实例,你可能应该使用多字母类型转换;不要只测试类,给对象一个转换自己的机会
- 单字母类型转换几乎总是错误的,除了
to_s
打印;你能想象有多少种情况在你甚至没有意识到有一个或一个字符串的情况下静默转换nil
或"one hundred"
转换是正确的做法?0
nil