Ruby中类型的约定是什么?
由于Ruby是一种纯动态类型的语言,我无法确定传递给我的方法的类型应该具有什么样的期望。 例如,如果我的方法仅在传递整数时起作用,我应该主动检查以确保是这种情况还是应该在这种情况下允许类型exception?
另外,在围绕Ruby代码编写设计文档时,指定方法应该在哪些类型上运行的正确方法是什么? 例如,Javadocs(虽然通常不用于设计文档)可以准确地指定方法将对哪些类型进行操作,因为语言本身是静态类型的,但似乎Ruby文档对于方法的前后条件一直非常不精确。 在Ruby中指定这种格式是否有标准做法?
IMO这是非常基于意见的。 并且在很大程度上取决于环境和您的要求。 问问你自己:我在乎吗? 可以提出错误吗? 谁是用户(我的代码与外部客户)? 我可以处理修复输入吗?
我认为一切都很好, 从不关心 (可能引起奇怪的例外)
def add(a, b) a + b # raise NoMethodError if a does not respond_to + end
过度使用鸭子类型检查
def add(a, b) if a.respond_to?(:+) a + b else "#{a} #{b}" # might makes sense? end end
或者只是将其翻译为例外类型
def add(a, b) a.to_i + b.to_i end
预先检查类型 (并引发一个有用的exception):
def integers(a, b) raise ArgumentError, "args must be integers" unless a.is_a?(Integer) and b.is_a?(Integer) a + b end
这实际上取决于您的需求以及您所需的安全性和安全性。
您需要注意的第一件事是类和类型之间的区别。
非常不幸的是,Java通过让类始终是类型来混淆这种区别(尽管Java中有其他类型不是类,即接口,基元和generics类型参数)。 实际上,几乎所有关于Java风格的书都会告诉你不要将类用作类型。 此外,在他的开创性论文“理解数据抽象”中 ,William R. Cook指出,在Java中,类描述的是抽象数据类型,而不是对象。 接口描述了对象,所以如果你在Java中使用类作为类型,你就不会做OO; 如果你想在Java中使用OO,你唯一可以用作类型的是接口,你唯一可以使用类的是工厂。
在Ruby中,类型更像是网络协议:类型描述对象理解的消息以及它对它们的反应。 (这种相似性并非偶然:Smalltalk,Ruby的远古祖先受到后来成为互联网的启发。在Smalltalk的说法中,“协议”是非正式用于描述对象类型的术语。在Objective-C中,这是非正式的协议的概念已成为语言的一部分,主要受Objective-C影响的Java直接复制了这个概念,但将其重命名为“interface”。)
所以,在Ruby中,我们有:
-
module
(语言特征):用于代码共享和差异实现的工具; 不是一种类型 -
class
(语言特性):对象的工厂,也是IS-Amodule
, 不是类型 - 协议 ( 非正式的东西):以消息为特征的对象类型响应以及它如何响应它们
另请注意,对象可以有多种类型。 例如,字符串对象具有类型“ Appendable ”(它响应<<
)和“ Indexable ”(它响应[]
)。
那么,回顾一下重点:
- Ruby语言中不存在类型,只有程序员才能使用
- 类和模块不是类型
- 类型是协议,其特征在于对象如何响应消息
显然,协议不能在语言中指定,因此它们通常在文档中指定。 虽然通常没有指定它们。 这实际上没有它听起来那么糟糕:例如,通常情况下,对消息的参数施加的要求从名称或方法的预期用法“显而易见”。 此外,在某些项目中,预计面向用户的验收测试将发挥作用。 (例如,在不再存在的Merb Web框架中就是这种情况。在验收测试中完全描述了API。)传递错误类型时得到的错误消息和exception通常足以弄清楚方法是什么需要。 最后但并非最不重要的是,始终存在源代码。
有两个众所周知的协议,例如在Enumerable
混合所需的each
协议(对象必须通过逐个产生其元素来响应each
协议,并且如果传递块并返回Enumerator
则返回self
如果没有传递块,则,如果一个对象想要成为一个Range
的端点(它必须响应succ
及其后继并且它必须响应<=
),或者所需的<=>
协议,则需要Range
协议。在Comparable
混合(对象必须以-1
或nil
响应<=>
)。 这些也不是在任何地方写下来的,或者只是在片段中写下来,它们只是被现有的Rubyists所熟知,并且很好地教给了新的。
一个很好的例子是StringIO
:它具有与IO
相同的协议,但不从它inheritance,也不从共同的祖先inheritance(除了明显的Object
)。 因此,当有人检查IO
,我无法传入StringIO
(对测试非常有用),但如果他们只是使用对象AS-IF它是一个IO
,我可以传入一个StringIO
,他们永远不会知道差异。
当然,这并不理想,但与Java相比:散文中还指出了许多重要的要求和保证! 例如,在List.sort
的类型签名中,它表示结果列表将被排序吗? 无处! 这只在JavaDoc中提到过。 function界面的类型是什么? 再次,仅在英文散文中指定。 Stream API有一个完整的概念动物园,它们没有在类型系统中捕获,如非干扰和可变性。
我为这篇长篇文章道歉,但理解类和类型之间的区别,以及理解像Ruby这样的OO语言中的类型是非常重要的。
处理类型的最佳方法是简单地使用对象并记录协议。 如果你想打电话,只需call
; 不要求它是Proc
。 (对于一个,这意味着我无法传递一个Method
,这将是一个恼人的限制。)如果你想添加一些东西,只需调用+
,如果你想追加一些东西,只需调用<<
,如果你想打印某些东西,只需调用print
或puts
(后者是有用的,例如,在测试中,当我可以传入StringIO
而不是File
)时。 不要试图以编程方式确定某个对象是否满足某个协议,这是徒劳的:它等同于解决暂停问题。 YARD文档系统有一个用于描述类型的标记。 它是完全自由格式的文本。 但是,有一种建议的类型语言(我并不特别喜欢,因为我认为它过分关注类而不是协议)。
如果你真的 必须有一个特定类的实例(而不是满足某个协议的对象),你可以使用许多类型转换方法。 但请注意,只要您需要某些类而不是依赖协议,您就会离开面向对象编程领域。
您应该知道的最重要的类型转换方法是单字母和多字母to_X
方法。 这是两者之间的重要区别:
- 如果一个对象可以“有些合理地” 表示为数组,字符串,整数,浮点数等,它将响应
to_a
,to_s
,to_i
,to_f
等。 - 如果一个对象与
Array
,String
,Integer
,Float
等实例的类型相同 ,它将响应to_ary
,to_str
,to_int
,to_float
等。
对于这两种方法,保证它们永远不会引发exception。 (当然,如果它们存在,否则将引发NoMethodError
。)对于这两种方法,保证返回值将是相应核心类的实例。 对于多字母方法,转换应该在语义上无损。 (注意,当我说“有保证”时,我说的是现有的方法。如果你自己编写,这不是保证,而是你必须履行的要求,这样它就成了使用你的其他人的保证。方法。)
多字母方法通常要严格得多,而且它们的数量要少得多。 例如,将nil
“表示为”空字符串是完全合理的,但是说nil
IS-AN为空字符串是荒谬的,因此nil
响应to_s
,但不响应to_str
。 同样,float通过返回它的截断来响应to_i
,但是它不响应to_int
,因为你不能无损地将float转换为整数。
以下是Ruby API中的一个示例: Array
实际上并未使用OO原则实现。 出于性能原因,Ruby作弊。 因此,您实际上只能使用Integer
类的实际实例索引到一个Array
,而不能只使用任意“类似整数”对象。 但是 ,Ruby不会要求你传入一个Integer
,而是先调用to_int
,让你有机会继续使用你自己的整数类对象。 但是,它不会调用to_i
,因为使用不是整数的东西索引到数组是没有意义的; 只能作为一个“合理地代表”。 OTOH, Kernel#print
, Kernel#puts
, IO#print
, IO#puts
和friends在他们的参数上调用to_s
,允许你合理地打印任何对象。 并且Array#join
在其参数上调用to_str
,但在数组元素上调用to_s
; 一旦你理解为什么这是有意义的,你就更接近理解Ruby中的类型。
以下是一些经验法则:
- 不要测试类型,只需使用它们并记录它们
- 如果你绝对肯定有一个特定类的实例,你应该使用多字母类型的转换; 不只是测试类,给对象一个转换自己的机会
- 单字母类型的转换几乎总是错误的,除了用于打印的
to_s
; 有多少情况你能想象在哪里静静地将nil
或"one hundred"
转换为0
而你甚至没有意识到有一个nil
或一个字符串是正确的做法?
我不确定为什么你只需要将整数传递给你的方法,但我不会在我的代码中主动检查值是一个整数。 例如,如果您正在执行需要整数的算术,我会在需要时将类型转换或转换为整数,并通过注释或在方法头中解释这样做的目的。
有趣的问题!
类型安全
Java和Ruby几乎截然相反。 在Ruby中,您可以:
String = Array # warning: already initialized constant String p String.new # []
因此,您几乎可以忘记从Java中了解到的任何类型安全性。
对于您的第一个问题,您可以:
- 确保除了Integer之外没有调用该方法(例如
my_method(array.size)
) - 接受可以使用Float,Integer或Rational调用该方法,并可能在输入上调用
to_i
。 - 使用与Floats一起使用的方法:例如
(1..3.5).to_a #=> [1, 2, 3]
,'a'*2.5 #=> 'aa'
- 如果用其他东西调用它,你可能会得到一个
NoMethodError: undefined method 'to_i' for object ...
,你可以尝试处理它(例如rescue
)
文档
记录方法的预期输入和输出的第一步是在正确的位置(类或模块)定义方法并使用适当的方法名称:
-
is_prime?
应该返回一个布尔值 -
is_prime?
应该在Integer
定义
否则, YARD支持文档中的类型:
# @param [Array] arg takes an Array of Strings or Symbols def foo(arg) end