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-A module不是类型
  • 协议非正式的东西):以消息为特征的对象类型响应以及它如何响应它们

另请注意,对象可以有多种类型。 例如,字符串对象具有类型“ Appendable ”(它响应<< )和“ Indexable ”(它响应[] )。

那么,回顾一下重点:

  • Ruby语言中不存在类型,只有程序员才能使用
  • 类和模块不是类型
  • 类型是协议,其特征在于对象如何响应消息

显然,协议不能在语言中指定,因此它们通常在文档中指定。 虽然通常没有指定它们。 这实际上没有它听起来那么糟糕:例如,通常情况下,对消息的参数施加的要求从名称或方法的预期用法“显而易见”。 此外,在某些项目中,预计面向用户的验收测试将发挥作用。 (例如,在不再存在的Merb Web框架中就是这种情况。在验收测试中完全描述了API。)传递错误类型时得到的错误消息和exception通常足以弄清楚方法是什么需要。 最后但并非最不重要的是,始终存在源代码。

有两个众所周知的协议,例如在Enumerable混合所需的each协议(对象必须通过逐个产生其元素来响应each协议,并且如果传递块并返回Enumerator则返回self如果没有传递块,则,如果一个对象想要成为一个Range的端点(它必须响应succ及其后继并且它必须响应<= ),或者所需的<=>协议,则需要Range协议。在Comparable混合(对象必须以-1nil响应<=> )。 这些也不是在任何地方写下来的,或者只是在片段中写下来,它们只是被现有的Rubyists所熟知,并且很好地教给了新的。

一个很好的例子是StringIO :它具有与IO相同的协议,但不从它inheritance,也不从共同的祖先inheritance(除了明显的Object )。 因此,当有人检查IO ,我无法传入StringIO (对测试非常有用),但如果他们只是使用对象AS-IF它是一个IO ,我可以传入一个StringIO ,他们永远不会知道差异。

当然,这并不理想,但与Java相比:散文中还指出了许多重要的要求和保证! 例如,在List.sort的类型签名中,它表示结果列表将被排序吗? 无处! 这只在JavaDoc中提到过。 function界面的类型是什么? 再次,仅在英文散文中指定。 Stream API有一个完整的概念动物园,它们没有在类型系统中捕获,如非干扰和可变性。

我为这篇长篇文章道歉,但理解类型之间的区别,以及理解像Ruby这样的OO语言中的类型是非常重要的。

处理类型的最佳方法是简单地使用对象并记录协议。 如果你想打电话,只需call ; 不要求它是Proc 。 (对于一个,这意味着我无法传递一个Method ,这将是一个恼人的限制。)如果你想添加一些东西,只需调用+ ,如果你想追加一些东西,只需调用<< ,如果你想打印某些东西,只需调用printputs (后者是有用的,例如,在测试中,当我可以传入StringIO而不是File )时。 不要试图以编程方式确定某个对象是否满足某个协议,这是徒劳的:它等同于解决暂停问题。 YARD文档系统有一个用于描述类型的标记。 它是完全自由格式的文本。 但是,有一种建议的类型语言(我并不特别喜欢,因为我认为它过分关注类而不是协议)。

如果你真的 必须有一个特定类的实例(而不是满足某个协议的对象),你可以使用许多类型转换方法。 但请注意,只要您需要某些类而不是依赖协议,您就会离开面向对象编程领域。

您应该知道的最重要的类型转换方法是单字母和多字母to_X方法。 这是两者之间的重要区别:

  • 如果一个对象可以“有些合理地” 表示为数组,字符串,整数,浮点数等,它将响应to_ato_sto_ito_f等。
  • 如果一个对象与ArrayStringIntegerFloat等实例的类型相同 ,它将响应to_aryto_strto_intto_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#printKernel#putsIO#printIO#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