4

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

此外,在围绕 Ruby 代码编写设计文档时,指定方法应该操作的类型的正确方法是什么?Javadocs(尽管通常不用于设计文档)例如准确指定方法将操作的类型,因为语言本身是静态类型的,但似乎 Ruby 文档对方法的前置条件和后置条件始终非常不精确. 是否有在 Ruby 中指定这种格式的标准做法?

4

4 回答 4

3

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

这实际上取决于您的需求以及您需要的安全级别。

于 2017-01-12T17:04:25.207 回答
3

您需要注意的第一件事是类型之间的区别。

非常不幸的是,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,这将是一个烦人的限制。)如果你想添加一些东西,只需调用+,如果你想附加一些东西,只需调用<<,如果你想打印一些东西,只需调用printputs(后一个很有用,例如,在测试中,当我可以传入 aStringIO而不是 aFile)。不要试图以编程方式确定一个对象是否满足某个协议,这是徒劳的:它相当于解决了停机问题。YARD 文档系统有一个用于描述类型的标签。它是完全自由格式的文本。但是,有一种建议的类型语言(我不是特别喜欢,因为我认为它过于关注类而不是协议)。

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

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

  • 如果一个对象可以“稍微合理地”表示为一个数组、一个字符串、一个整数、一个浮点数等,它将响应to_a, to_s, to_i,to_f等。
  • 如果一个对象与 , , , 等的实例属于同一类型。它将响应, , ,等。ArrayStringIntegerFloatto_aryto_strto_intto_float

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

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

下面是一个来自 Ruby API 的示例:Arrays 实际上不是使用 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"转换是正确的做法?0nil
于 2017-01-12T20:04:22.857 回答
0

有趣的问题!

类型安全

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

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

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

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

  • 确保该方法没有使用除 Integer 以外的任何其他内容调用(例如my_method(array.size)
  • 接受可能会使用浮点数、整数或 Rational 调用该方法,并可能调用to_i输入。
  • 使用适用于浮点数的方法:例如(1..3.5).to_a #=> [1, 2, 3]'a'*2.5 #=> 'aa'
  • 如果它被其他东西调用,你可能会得到 a NoMethodError: undefined method 'to_i' for object ...,你可以尝试处理它(例如 with rescue

文档

记录方法的预期输入和输出的第一步是在正确的位置(类或模块)定义方法并使用适当的方法名称:

  • is_prime?应该返回一个布尔值
  • is_prime?应该定义在Integer

否则,YARD支持文档中的类型:

# @param [Array<String, Symbol>] arg takes an Array of Strings or Symbols
def foo(arg)
end
于 2017-01-12T16:49:56.910 回答
0

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

于 2017-01-12T16:50:04.323 回答