Ruby 3.0 中位置参数和关键字参数的分离

本文解释了 Ruby 3.0 中计划的关键字参数不兼容性

简而言之

在 Ruby 3.0 中,位置参数和关键字参数将分离。Ruby 2.7 将对 Ruby 3.0 中会发生改变的行为发出警告。如果您看到以下警告,则需要更新您的代码

  • 使用最后一个参数作为关键字参数已弃用,或
  • 将关键字参数作为最后一个哈希参数传递已弃用,或
  • 将最后一个参数拆分为位置参数和关键字参数已弃用

在大多数情况下,您可以通过添加双星号运算符来避免不兼容性。它明确指定传递关键字参数,而不是 Hash 对象。同样,您可以添加大括号 {} 来显式传递 Hash 对象,而不是关键字参数。有关更多详细信息,请阅读下面的“典型案例”部分。

在 Ruby 3 中,委托所有参数的方法必须显式委托关键字参数以及位置参数。如果您想保留 Ruby 2.7 及更早版本中的委托行为,请使用 ruby2_keywords。有关更多详细信息,请参见下面的“处理参数委托”部分。

典型案例

这是最典型的案例。您可以使用双星号运算符 (**) 来传递关键字,而不是哈希。

# This method accepts only a keyword argument
def foo(k: 1)
  p k
end

h = { k: 42 }

# This method call passes a positional Hash argument
# In Ruby 2.7: The Hash is automatically converted to a keyword argument
# In Ruby 3.0: This call raises an ArgumentError
foo(h)
  # => demo.rb:11: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
  #    demo.rb:2: warning: The called method `foo' is defined here
  #    42

# If you want to keep the behavior in Ruby 3.0, use double splat
foo(**h) #=> 42

这是另一种情况。您可以使用大括号 ({}) 来显式传递哈希,而不是关键字。

# This method accepts one positional argument and a keyword rest argument
def bar(h, **kwargs)
  p h
end

# This call passes only a keyword argument and no positional arguments
# In Ruby 2.7: The keyword is converted to a positional Hash argument
# In Ruby 3.0: This call raises an ArgumentError
bar(k: 42)
  # => demo2.rb:9: warning: Passing the keyword argument as the last hash parameter is deprecated
  #    demo2.rb:2: warning: The called method `bar' is defined here
  #    {:k=>42}

# If you want to keep the behavior in Ruby 3.0, write braces to make it an
# explicit Hash
bar({ k: 42 }) # => {:k=>42}

什么被弃用了?

在 Ruby 2 中,关键字参数可以被视为最后一个位置哈希参数,而最后一个位置哈希参数可以被视为关键字参数。

由于自动转换有时过于复杂和麻烦,如最后一节所述。因此,它现在在 Ruby 2.7 中被弃用,并将在 Ruby 3 中删除。换句话说,关键字参数将在 Ruby 3 中与位置参数完全分离。因此,当您要传递关键字参数时,您应该始终使用 foo(k: expr)foo(**expr)。如果您想接受关键字参数,原则上您应该始终使用 def foo(k: default)def foo(k:)def foo(**kwargs)

请注意,当使用关键字参数调用不接受关键字参数的方法时,Ruby 3.0 的行为不会不同。例如,以下情况不会被弃用,并且在 Ruby 3.0 中将继续工作。关键字参数仍然被视为位置哈希参数。

def foo(kwargs = {})
  kwargs
end

foo(k: 1) #=> {:k=>1}

这是因为这种风格使用非常频繁,并且在如何处理参数方面没有歧义。禁止这种转换会带来额外的不兼容性,而好处甚微。

但是,不建议在新代码中使用这种风格,除非您经常将哈希作为位置参数传递,并且还使用关键字参数。否则,请使用双星号

def foo(**kwargs)
  kwargs
end

foo(k: 1) #=> {:k=>1}

我的代码会在 Ruby 2.7 上崩溃吗?

简短的回答是“可能不会”。

Ruby 2.7 中的更改被设计为向 3.0 的迁移路径。尽管原则上,Ruby 2.7 仅警告将在 Ruby 3 中发生改变的行为,但它包含了一些我们认为是次要的不兼容更改。有关详细信息,请参阅“其他次要更改”部分。

除了警告和次要更改外,Ruby 2.7 尝试保持与 Ruby 2.6 的兼容性。因此,您的代码可能可以在 Ruby 2.7 上运行,尽管它可能会发出警告。通过在 Ruby 2.7 上运行它,您可以检查您的代码是否为 Ruby 3.0 做好准备。

如果您想禁用弃用警告,请使用命令行参数 -W:no-deprecated 或将 Warning[:deprecated] = false 添加到您的代码中。

处理参数委托

Ruby 2.6 或更早版本

在 Ruby 2 中,您可以通过接受 *rest 参数和 &block 参数,并将两者传递给目标方法来编写委托方法。在这种行为中,关键字参数也通过位置参数和关键字参数之间的自动转换隐式处理。

def foo(*args, &block)
  target(*args, &block)
end

Ruby 3

您需要显式委托关键字参数。

def foo(*args, **kwargs, &block)
  target(*args, **kwargs, &block)
end

或者,如果您不需要与 Ruby 2.6 或更早版本的兼容性,并且您不更改任何参数,则可以使用 Ruby 2.7 中引入的新委托语法 (...)。

def foo(...)
  target(...)
end

Ruby 2.7

简而言之:使用 Module#ruby2_keywords 并委托 *args, &block

ruby2_keywords def foo(*args, &block)
  target(*args, &block)
end

ruby2_keywords 将关键字参数作为最后一个哈希参数接受,并在调用其他方法时将其作为关键字参数传递。

实际上,Ruby 2.7 在许多情况下允许新的委托风格。但是,有一个已知的极端情况。请参阅下一节。

在 Ruby 2.6、2.7 和 Ruby 3 上兼容的委托

简而言之:再次使用 Module#ruby2_keywords

ruby2_keywords def foo(*args, &block)
  target(*args, &block)
end

不幸的是,我们需要使用旧式委托(即,没有 **kwargs),因为 Ruby 2.6 或更早版本无法正确处理新的委托风格。这是关键字参数分离的原因之一;详细信息在最后一节中描述。ruby2_keywords 允许您即使在 Ruby 2.7 和 3.0 中也可以运行旧风格。由于在 2.6 或更早版本中没有定义 ruby2_keywords,请使用 ruby2_keywords gem 或自己定义

def ruby2_keywords(*)
end if RUBY_VERSION < "2.7"

如果您的代码不必在 Ruby 2.6 或更早版本上运行,您可以在 Ruby 2.7 中尝试新风格。在几乎所有情况下,它都可以工作。但是,请注意,存在以下不幸的极端情况

def target(*args)
  p args
end

def foo(*args, **kwargs, &block)
  target(*args, **kwargs, &block)
end

foo({})       #=> Ruby 2.7: []   ({} is dropped)
foo({}, **{}) #=> Ruby 2.7: [{}] (You can pass {} by explicitly passing "no" keywords)

空的哈希参数会被自动转换并吸收到 **kwargs 中,并且委托调用会删除空的关键字哈希,因此没有参数传递给 target。据我们所知,这是唯一的极端情况。

如最后一行所述,您可以通过使用 **{} 来解决此问题。

如果您真的很担心可移植性,请使用 ruby2_keywords。(承认 Ruby 2.6 或更早版本本身在关键字参数中有很多极端情况。:-) ruby2_keywords 可能会在 Ruby 2.6 达到生命周期结束后在未来被删除。届时,我们建议显式委托关键字参数(请参见上面的 Ruby 3 代码)。

其他次要更改

Ruby 2.7 中关于关键字参数有三个次要更改。

1. 关键字参数中允许非符号键

在 Ruby 2.6 或更早版本中,关键字参数中仅允许符号键。在 Ruby 2.7 中,关键字参数可以使用非符号键。

def foo(**kwargs)
  kwargs
end
foo("key" => 42)
  #=> Ruby 2.6 or before: ArgumentError: wrong number of arguments
  #=> Ruby 2.7 or later: {"key"=>42}

如果方法同时接受可选参数和关键字参数,则在 Ruby 2.6 中,具有符号键和非符号键的哈希对象将被分成两个。在 Ruby 2.7 中,两者都被接受为关键字,因为允许非符号键。

def bar(x=1, **kwargs)
  p [x, kwargs]
end

bar("key" => 42, :sym => 43)
  #=> Ruby 2.6: [{"key"=>42}, {:sym=>43}]
  #=> Ruby 2.7: [1, {"key"=>42, :sym=>43}]

# Use braces to keep the behavior
bar({"key" => 42}, :sym => 43)
  #=> Ruby 2.6 and 2.7: [{"key"=>42}, {:sym=>43}]

如果将带有符号键和非符号键的哈希或关键字参数传递给接受显式关键字但不接受关键字其余参数 (**kwargs) 的方法,Ruby 2.7 仍然会拆分哈希并发出警告。此行为将在 Ruby 3 中删除,并且将引发 ArgumentError

def bar(x=1, sym: nil)
  p [x, sym]
end

bar("key" => 42, :sym => 43)
# Ruby 2.6 and 2.7: => [{"key"=>42}, 43]
# Ruby 2.7: warning: Splitting the last argument into positional and keyword parameters is deprecated
#           warning: The called method `bar' is defined here
# Ruby 3.0: ArgumentError

2. 带有空哈希的双星号 (**{}) 不传递任何参数

在 Ruby 2.6 或更早版本中,传递 **empty_hash 将传递一个空的哈希作为位置参数。在 Ruby 2.7 或更高版本中,它不传递任何参数。

def foo(*args)
  args
end

empty_hash = {}
foo(**empty_hash)
  #=> Ruby 2.6 or before: [{}]
  #=> Ruby 2.7 or later: []

请注意,foo(**{}) 在 Ruby 2.6 和 2.7 中都不传递任何内容。在 Ruby 2.6 及更早版本中,**{} 被解析器删除,而在 Ruby 2.7 及更高版本中,它的处理方式与 **empty_hash 相同,从而提供了一种向方法传递任何关键字参数的简便方法。

在 Ruby 2.7 中,当使用所需位置参数不足的数量调用方法时,foo(**empty_hash) 将传递一个空哈希,并发出警告,以与 Ruby 2.6 兼容。此行为将在 3.0 中删除。

def foo(x)
  x
end

empty_hash = {}
foo(**empty_hash)
  #=> Ruby 2.6 or before: {}
  #=> Ruby 2.7: warning: Passing the keyword argument as the last hash parameter is deprecated
  #             warning: The called method `foo' is defined here
  #=> Ruby 3.0: ArgumentError: wrong number of arguments

3. 引入了无关键字参数语法 (**nil)

您可以在方法定义中使用 **nil 来显式标记该方法不接受关键字参数。使用关键字参数调用此类方法将导致 ArgumentError。(这实际上是一个新功能,而不是不兼容性)

def foo(*args, **nil)
end

foo(k: 1)
  #=> Ruby 2.7 or later: no keywords accepted (ArgumentError)

这对于明确该方法不接受关键字参数很有用。否则,关键字将在以上示例中的其余参数中被吸收。如果您扩展方法以接受关键字参数,则该方法可能会出现以下不兼容性

# If a method accepts rest argument and no `**nil`
def foo(*args)
  p args
end

# Passing keywords are converted to a Hash object (even in Ruby 3.0)
foo(k: 1) #=> [{:k=>1}]

# If the method is extended to accept a keyword
def foo(*args, mode: false)
  p args
end

# The existing call may break
foo(k: 1) #=> ArgumentError: unknown keyword k

我们为什么要弃用自动转换

自动转换最初似乎是个好主意,并且在许多情况下都效果很好。但是,它有太多的极端情况,并且我们收到了许多关于此行为的错误报告。

当方法接受可选位置参数和关键字参数时,自动转换效果不佳。有些人期望最后一个哈希对象被视为位置参数,而另一些人则期望将其转换为关键字参数。

这是最令人困惑的情况之一

def foo(x, **kwargs)
  p [x, kwargs]
end

def bar(x=1, **kwargs)
  p [x, kwargs]
end

foo({}) #=> [{}, {}]
bar({}) #=> [1, {}]

bar({}, **{}) #=> expected: [{}, {}], actual: [1, {}]

在 Ruby 2 中,foo({}) 将一个空哈希作为普通参数传递(即,{} 被分配给 x),而 bar({}) 传递一个关键字参数(即,{} 被分配给 kwargs)。因此,any_method({}) 非常模棱两可。

你可能会认为使用 bar({}, **{}) 可以显式地将空哈希传递给 x。令人惊讶的是,它并没有像你预期的那样工作;在 Ruby 2.6 中,它仍然会打印 [1, {}]。这是因为 **{} 在 Ruby 2.6 中会被解析器忽略,并且第一个参数 {} 会被自动转换为关键字参数(**kwargs)。在这种情况下,你需要调用 bar({}, {}),这非常奇怪。

同样的问题也适用于接受剩余参数和关键字参数的方法。这使得关键字参数的显式委托无法工作。

def target(*args)
  p args
end

def foo(*args, **kwargs, &block)
  target(*args, **kwargs, &block)
end

foo() #=> Ruby 2.6 or before: [{}]
      #=> Ruby 2.7 or later:  []

foo() 不传递任何参数,但在 Ruby 2.6 中,target 接收到一个空的哈希参数。这是因为方法 foo 显式地委托了关键字参数 (**kwargs)。当调用 foo() 时,args 是一个空数组,kwargs 是一个空哈希,而 blocknil。然后 target(*args, **kwargs, &block) 会传递一个空哈希作为参数,因为 **kwargs 会被自动转换为位置哈希参数。

这种自动转换不仅会让人感到困惑,还会使方法的可扩展性降低。有关行为变更的原因以及为什么做出某些实现选择的更多详细信息,请参阅 [Feature #14183]

致谢

本文由 Jeremy Evans 和 Benoit Daloze 友好地审阅(甚至共同撰写)。

历史

  • 更新于 2019-12-25:在 2.7.0-rc2 中,警告消息略有更改,并添加了一个 API 来抑制警告。