Ruby 中的类型约定是什么?



由于Ruby是一种纯动态类型的语言,我永远不确定我应该对传递给我的方法的类型抱有什么样的期望。例如,如果我的方法仅在传递 Integer 时才起作用,我应该主动检查以确保是这种情况,还是应该在这种情况下只允许类型异常?

此外,在围绕 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

预先检查类型(并引发有用的异常),请执行以下操作:

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中还有其他类型不是类,即接口,原语和泛型类型参数)。事实上,几乎每本关于Java风格的书都会告诉你不要使用类作为类型。此外,William R. Cook在他的开创性论文《重新审视理解数据抽象》中指出,在Java中,类描述的是抽象数据类型,而不是对象。接口描述对象,所以如果你在 Java 中使用类作为类型,你就不是在做 OO;如果你想在 Java 中 OO,你唯一可以用作类型的是接口,你唯一可以使用类的就是作为工厂。

在 Ruby 中,类型更像是网络协议:类型描述对象理解的消息以及它如何对它们做出反应。(这种相似性并非偶然:Ruby的远祖Smalltalk的灵感来自后来的互联网。在Smalltalk的说法中,"协议"是非正式用于描述对象类型的术语。在Objective-C中,这种非正式的协议概念成为语言的一部分,而主要受Objective-C影响的Java直接复制了这个概念,但将其重命名为"接口"。

因此,在 Ruby 中,我们有:

  • module(一种语言功能):代码共享和差异实现的工具;不是类型
  • class(一种语言功能):对象的工厂,也是 IS-Amodule而不是类型
  • 协议(一种非正式的东西):对象的类型,以消息为特征,以及它如何响应它们

另请注意,一个对象可以有多个类型。 例如,字符串对象同时具有">Appendable"(它响应<<)和">可索引"(它响应[])。

因此,回顾一下要点:

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

显然,协议不能在语言中指定,因此通常在文档中指定。尽管通常情况下,它们根本没有指定。这实际上并不像听起来那么糟糕:通常,例如,对消息发送的参数施加的要求从方法的名称或预期用法来看是"显而易见的"。此外,在某些项目中,预计面向用户的验收测试将发挥该作用。(例如,在不再存在的Merb网络框架中就是这种情况。该 API 在验收测试中进行了完整描述。传递错误类型时收到的错误消息和异常通常也足以确定该方法需要什么。最后但并非最不重要的一点是,总是有源代码。

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

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

当然,这并不理想,但与Java相比:许多重要的要求和保证也在散文中指定!例如,在List.sort的类型签名中,它在哪里表示将排序结果列表?无处!这仅在JavaDoc中提到。功能接口的类型是什么?同样,仅在英语散文中指定。Stream API 具有一整堆未在类型系统中捕获的概念,例如非干扰和可变性。

对于这篇长文,我深表歉意,但理解类型之间的区别,以及理解像 Ruby 这样的 OO 语言中的类型是什么非常重要。

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

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

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

  • 如果一个对象可以"合理地">表示为数组、字符串、整数、浮点数等,它将响应to_ato_sto_ito_f等。
  • 如果一个对象与ArrayStringIntegerFloat等的实例类型相同,它将响应to_aryto_strto_intto_float等。

对于这两种方法,可以保证它们永远不会引发异常。(当然,如果它们存在的话,否则就会提出NoMethodError。对于这两种方法,可以保证返回值将是相应核心类的实例。对于多字母方法,转换应该是语义无损的。(注意,当我说"它是有保证的"时,我说的是已经存在的方法。如果你自己写,这不是一个保证,而是一个你必须满足的要求,这样它就会成为使用你的方法的其他人的保证。

多字母方法通常要严格得多,而且数量要少得多。例如,说nil"可以表示为"空字符串是完全合理的,但说nilIS-AN 空字符串是荒谬的,因此nil响应to_s,而不是to_str。同样,浮点数通过返回其截断来响应to_i,但它不响应to_int,因为您无法无损地将浮点数转换为整数。

下面是 Ruby API 的一个例子:Array实际上不是使用 OO 原则实现的。Ruby 作弊,出于性能原因。因此,您实际上只能使用Integer的实际实例索引Array,而不能使用任何任意的"类似整数"的对象。但是,Ruby 不会要求您传入Integer,而是会先调用to_int,让您有机会仍然使用自己的类似整数的对象。但是,它调用to_i,因为索引到具有非整数的数组中是没有意义的;这只能"在某种程度上合理地代表"为一体。OTOH,Kernel#printKernel#putsIO#printIO#puts和朋友打电话给to_s他们的论点,以允许您合理地打印任何对象。Array#join调用to_str参数,但to_s调用数组元素;一旦你理解了为什么这是有意义的,你就更接近于理解 Ruby 中的类型。

以下是一些经验法则:

  • 不要测试类型,只需使用它们并记录它们
  • 如果你绝对肯定必须有一个特定类的实例,你可能应该使用多字母类型转换;不要只测试类,给对象一个转换自己的机会。
  • 单字母类型的转换几乎总是错误的事情,除了to_s打印;你能想象多少种情况,在你甚至没有意识到有nil或字符串的情况下默默地将nil"one hundred"转换为0是正确的做法?

我不确定为什么您只需要将整数传递到您的方法中,但我不会在整个代码中主动检查该值是否为整数。例如,如果您正在执行需要整数的算术,我会在需要时将值类型转换或转换为整数,并通过注释或方法标头解释这样做的目的。

有趣的问题!

类型安全

Java和Ruby几乎截然相反。 在 Ruby 中,您可以执行以下操作:

String = Array
# warning: already initialized constant String
p String.new
# []

所以你几乎可以忘记你从Java中知道的任何类型安全。

对于您的第一个问题,您可以:

  • 确保该方法不是用整数以外的任何东西调用的(例如my_method(array.size))
  • 接受该方法可能会被调用浮点数、整数或有理数,并可能在输入上调用to_i
  • 使用适用于浮点的方法:例如(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<String, Symbol>] arg takes an Array of Strings or Symbols
def foo(arg)
end

最新更新