Ruby 3 陷阱
本节记录了我们致力于 Ruby 3 支持 时发现的几个问题,这些问题导致了难以理解的细微错误或测试失败。我们鼓励每位经常编写 Ruby 代码的极狐GitLab 贡献者熟悉这些问题。
要查找 Ruby 3 语言和标准库的完整更改列表,请参阅 Ruby 更改。
Hash#each 一致地向 lambda 传递一个 2 元素数组
考虑以下代码片段:
ruby1def foo(a, b) 2 p [a, b] 3end 4 5def bar(a, b = 2) 6 p [a, b] 7end 8 9foo_lambda = method(:foo).to_proc 10bar_lambda = method(:bar).to_proc 11 12{ a: 1 }.each(&foo_lambda) 13{ a: 1 }.each(&bar_lambda)
在 Ruby 2.7 中,该程序的输出表明,向 lambda 传递哈希条目时,行为会根据所需参数的数量而有所不同:
ruby# Ruby 2.7 { a: 1 }.each(&foo_lambda) # 打印 [:a, 1] { a: 1 }.each(&bar_lambda) # 打印 [[:a, 1], 2]
Ruby 3 使此行为保持一致,并始终尝试将哈希条目作为单个 [key, value] 数组传递:
ruby# Ruby 3.0 { a: 1 }.each(&foo_lambda) # `foo': wrong number of arguments (given 1, expected 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 issue 12706。
Symbol#to_proc 返回与 lambda 一致的签名元数据
Ruby 中的一个常见习惯用法是使用 &:<symbol> 简写获取 Proc 对象,并将其传递给高阶函数:
ruby[1, 2, 3].each(&:to_s)
Ruby 将 &:<symbol> 解糖为 Symbol#to_proc。我们可以用方法 接收者 作为其第一个参数(此处为 Integer),并将所有方法 参数(此处无)作为其余参数来调用它。
这在 Ruby 2.7 和 Ruby 3 中行为相同。Ruby 3 的不同之处在于捕获此 Proc 对象并检查其调用签名时。 这通常在编写 DSL 或使用其他形式的元编程时发生:
rubyp = :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 issue 16260。
OpenStruct 不会延迟计算字段
OpenStruct 实现在 Ruby 3 中进行了部分重写,导致行为发生变化。在 Ruby 2.7 中,OpenStruct 在首次访问方法时才延迟定义方法。 在 Ruby 3.0 中,它在初始化器中急切地定义这些方法,这可能会破坏继承自 OpenStruct 并覆盖这些方法的类。
出于这些原因,不要从 OpenStruct 继承;理想情况下,根本不要使用它。 OpenStruct 被认为是有问题的。 编写新代码时,请优先使用 Struct,它的实现更简单,尽管灵活性较差。
Regexp 和 Range 实例被冻结
不再需要显式冻结 Regexp 或 Range 实例,因为 Ruby 3 在创建时会自动冻结它们。
这有一个微妙的副作用:对这些类型进行 stub 方法调用的测试现在会失败并报错,因为 RSpec 无法 stub 冻结的对象:
ruby# Ruby 2.7: 可用 # Ruby 3.0: 错误: "can't modify frozen object" allow(subject.function_returning_range).to receive(:max).and_return(42)
通过不对冻结对象进行方法调用的 stub 来重写受影响的测试。上面的示例可以重写为:
ruby# 适用于任何 Ruby 版本 allow(subject).to receive(:function_returning_range).and_return(1..42)
表测试在 Ruby 3.0.2 中失败
Ruby 3.0.2 有一个已知的错误,当表值由整数值组成时,会导致 表测试 失败。 该问题已在 Ruby 中修复,预计包含在 Ruby 3.0.3 中。
该问题仅影响运行未打补丁的 Ruby 3.0.2 的用户。当你手动安装 Ruby 或通过 asdf 等工具安装时,很可能是这种情况。gitlab-development-kit (GDK) 的用户也会受到此问题的影响。
构建镜像不受影响,因为它们包含了解决此错误的补丁集。
在 irb 和 rails console 中测试
另一个陷阱是,在 irb/rails c 中进行测试会静默弃用警告, 因为 Ruby 2.7.x 中的 irb 有一个 错误,阻止了弃用警告的显示。
在编写代码和进行代码审查时,要特别注意 f({k: v}) 形式的方法调用。 当 f 接受 Hash 或关键字参数时,这在 Ruby 2 中是有效的,但 Ruby 3 仅当 f 接受 Hash 时才认为这是有效的。 为了符合 Ruby 3 的要求,如果 f 接受关键字参数,应将其更改为以下调用之一:
- f(**{k: v})
- f(k: v)
RSpec with 参数匹配器对简写 Hash 语法失败
由于关键字参数("kwargs")在 Ruby 3 中是一等概念,关键字参数不再转换为内部的 Hash 实例。这导致当接收者接受位置选项哈希而不是 kwargs 时,RSpec 方法参数匹配器失败:
rubydef m(options={}); end
rubyexpect(subject).to receive(:m).with(a: 42)
在 Ruby 3 中,此期望会失败并显示以下错误:
plaintextFailure/Error: #<subject> received :m with unexpected arguments expected: ({:a=>42}) got: ({:a=>42})
这是因为 RSpec 在这里使用了 kwargs 参数匹配器,但该方法接受的是哈希。 它在 Ruby 2 中可用,因为 a: 42 会先转换为哈希,RSpec 将使用哈希参数匹配器。
一种解决方法是,当我们知道某个方法接受选项哈希时,不要使用简写语法,而是传递一个实际的 Hash:
ruby# 注意键值对周围的大括号。 expect(subject).to receive(:m).with({ a: 42 })
有关更多信息,请参阅 RSpec 的官方问题报告。