Ruby 3 的陷阱
本节记录了我们在开发 Ruby 3 支持 时发现的几个问题, 这些问题导致了难以理解的细微错误或测试失败。我们鼓励每一位定期编写 Ruby 代码的 GitLab 贡献者熟悉这些问题。
要查看 Ruby 3 语言和标准库的完整变更列表,请参阅 Ruby 变更。
Hash#each 一致地向 lambda 传递 2 元素数组
请看以下代码片段:
def foo(a, b)
p [a, b]
end
def bar(a, b = 2)
p [a, b]
end
foo_lambda = method(:foo).to_proc
bar_lambda = method(:bar).to_proc
{ a: 1 }.each(&foo_lambda)
{ a: 1 }.each(&bar_lambda)在 Ruby 2.7 中,该程序的输出表明,向 lambda 传递哈希条目的行为会根据必需参数的数量而有所不同:
# Ruby 2.7
{ a: 1 }.each(&foo_lambda) # 打印 [:a, 1]
{ a: 1 }.each(&bar_lambda) # 打印 [[:a, 1], 2]Ruby 3 使这种行为保持一致,并始终尝试将哈希条目作为单个 [key, value] 数组传递:
# Ruby 3.0
{ a: 1 }.each(&foo_lambda) # `foo': 参数数量错误 (给定 1,期望 2) (ArgumentError)
{ a: 1 }.each(&bar_lambda) # 打印 [[:a, 1], 2]要编写在 2.7 和 3.0 下都能运行的代码,可以考虑以下选项:
- 始终将 lambda 主体作为块传递:
{ a: 1 }.each { |a, b| p [a, b] }。 - 解构 lambda 参数:
{ a: 1 }.each(&->((a, b)) { p [a, b] })。
我们建议始终显式传递块,并优先使用两个必需参数作为块参数。
有关更多信息,请参阅 Ruby 问题 12706。
Symbol#to_proc 返回与 lambda 一致的签名元数据
Ruby 中常见的惯用法是使用 &:<symbol> 简写获取 Proc 对象,并将它们传递给高阶函数:
[1, 2, 3].each(&:to_s)Ruby 将 &:<symbol> 解糖为 Symbol#to_proc。我们可以使用方法 接收器 作为其第一个参数(这里是 Integer),
并将所有方法 参数(这里为空)作为其余参数来调用它。
这种行为在 Ruby 2.7 和 Ruby 3 中是相同的。Ruby 3 的不同之处在于捕获这个 Proc 对象并检查其调用签名时。
这在编写 DSL 或使用其他形式的元编程时经常发生:
p = :foo.to_proc # 这通常通过 `&:foo` 的转换发生
# Ruby 2.7: 打印 [[:rest]] (-1)
# Ruby 3.0: 打印 [[:req], [:rest]] (-2)
puts "#{p.parameters} (#{p.arity})"Ruby 2.7 报告此 Proc 对象有零个必需参数和一个可选参数,而 Ruby 3 报告一个必需参数和一个可选参数。
Ruby 2.7 是不正确的:第一个参数必须始终传递,因为它是 Proc 对象所表示方法的接收器,
而方法在没有接收器的情况下无法被调用。
Ruby 3 纠正了这一点:测试 Proc 对象数量或参数列表的代码现在可能会中断,需要更新。
有关更多信息,请参阅 Ruby 问题 16260。
OpenStruct 不会延迟评估字段
OpenStruct 的实现在 Ruby 3 中经历了部分重写,导致行为发生变化。在 Ruby 2.7 中,OpenStruct 在方法首次访问时延迟定义方法。
在 Ruby 3.0 中,它在初始化器中急切地定义这些方法,这可能会破坏继承自 OpenStruct 并覆盖这些方法的类。
由于这些原因,不要继承 OpenStruct;理想情况下,根本不要使用它。
OpenStruct 被认为是有问题的。
在编写新代码时,优先使用 Struct,它的实现更简单,虽然灵活性较差。
Regexp 和 Range 实例被冻结
不再需要显式冻结 Regexp 或 Range 实例,因为 Ruby 3 在创建时会自动冻结它们。
这有一个微妙的副作用:在这些类型上存根方法调用的测试现在会失败并报错,因为 RSpec 无法存根冻结对象:
# Ruby 2.7: 有效
# Ruby 3.0: 错误: "can't modify frozen object"
allow(subject.function_returning_range).to receive(:max).and_return(42)通过不在冻结对象上存根方法调用来重写受影响的测试。上面的示例可以重写为:
# 适用于任何 Ruby 版本
allow(subject).to receive(:function_returning_range).and_return(1..42)使用 Ruby 3.0.2 时表测试失败
Ruby 3.0.2 有一个已知错误,当表值由整数值组成时,会导致 表测试 失败。 原因记录在 问题 337614 中。 这个问题已在 Ruby 中修复,预计该修复将包含在 Ruby 3.0.3 中。
这个问题只影响运行未打补丁的 Ruby 3.0.2 的用户。当你手动安装 Ruby 或通过 asdf 等工具安装时,很可能就是这种情况。
gitlab-development-kit (GDK) 的用户也受到此问题的影响。
构建镜像不受影响,因为它们包含解决此错误的补丁集。
如果方法被存根,DeprecationToolkit 不会捕获弃用警告
我们依赖 deprecation_toolkit 在使用 Ruby 2 中已弃用并在 Ruby 3 中删除的功能时快速失败。
从 Ruby 2 过渡到 Ruby 3 期间捕获的一个常见问题与 Ruby 3.0 中位置参数和关键字参数的分离 有关。
不幸的是,如果作者在测试中存根了这些方法,将不会捕获弃用警告。
我们通过 deprecation_toolkit 在测试中运行对此警告的自动检测,
但它依赖于 Kernel#warn 发出警告的事实,因此存根此调用将有效地移除警告调用,这意味着 deprecation_toolkit 永远不会看到弃用警告。
存根实现会移除该警告,我们永远不会捕获它,因此构建是绿色的。
有关更多上下文,请参阅 问题 364099。
在 irb 和 rails console 中测试
另一个陷阱是在 irb/rails c 中测试会静默弃用警告,
因为 Ruby 2.7.x 中的 irb 有一个 错误,阻止了弃用警告的显示。
在编写代码和进行代码审查时,请特别注意 f({k: v}) 形式的方法调用。
这在 Ruby 2 中是有效的,当 f 接受 Hash 或关键字参数时,但 Ruby 3 只有在 f 接受 Hash 时才认为这是有效的。
为了符合 Ruby 3,如果 f 接受关键字参数,这应该更改为以下调用之一:
f(**{k: v})f(k: v)
RSpec with 参数匹配器对简写哈希语法失败
因为关键字参数(“kwargs”)是 Ruby 3 中的一等公民概念,关键字参数不再转换为内部 Hash 实例。
这导致当接收器采用位置选项哈希而不是 kwargs 时,RSpec 方法参数匹配器失败:
def m(options={}); endexpect(subject).to receive(:m).with(a: 42)在 Ruby 3 中,此预期失败并显示以下错误:
Failure/Error:
#<subject> received :m with unexpected arguments
expected: ({:a=>42})
got: ({:a=>42})发生这种情况是因为 RSpec 在这里使用 kwargs 参数匹配器,但该方法接受一个哈希。
它在 Ruby 2 中有效,因为 a: 42 首先被转换为哈希,RSpec 将使用哈希参数匹配器。
解决方法是,当我们知道方法接受选项哈希时,不要使用简写语法,而是传递实际的 Hash:
# 注意键值对周围的括号
expect(subject).to receive(:m).with({ a: 42 })有关更多信息,请参阅 RSpec 的官方问题报告。