如何在子类中添加命名参数或在 Ruby 2.2 中更改它们的默认值?
How do I add named parameters in a subclass or change their default in Ruby 2.2?
这个问题是关于Ruby 2.2.
假设我有一个接受位置和命名参数的方法。
class Parent
def foo(positional, named1: "parent", named2: "parent")
puts positional.inspect
puts named1.inspect
puts named2.inspect
end
end
子类既要覆盖一些默认值,又要添加自己的命名参数。我将如何最好地做到这一点?理想情况下,它不必知道父级签名的详细信息,以防父级想要添加一些可选的位置参数。我的第一次尝试是这样的。
class Child < Parent
def foo(*args, named1: "child", named3: "child" )
super
end
end
但这会爆炸,因为未知的 named3:
被传递给父级。
Child.new.foo({ this: 23 })
/Users/schwern/tmp/test.rb:10:in `foo': unknown keyword: this (ArgumentError)
from /Users/schwern/tmp/test.rb:15:in `<main>'
我试过将参数显式传递给 super,但这也不起作用。似乎第一个位置参数被视为命名参数。
class Child < Parent
def foo(*args, named1: "child", named3: "child" )
super(*args, named1: "child")
end
end
Child.new.foo({ this: 23 })
/Users/schwern/tmp/test.rb:10:in `foo': unknown keyword: this (ArgumentError)
from /Users/schwern/tmp/test.rb:15:in `<main>'
我可以让 Child 知道第一个位置参数,它有效...
class Child < Parent
def foo(arg, named1: "child", named3: "child" )
super(arg, named1: "child")
end
end
Child.new.foo({ this: 23 })
Parent.new.foo({ this: 23 })
{:this=>23}
"child"
"parent"
{:this=>23}
"parent"
"parent"
...直到我传入一个命名参数。
Child.new.foo({ this: 23 }, named2: "caller")
Parent.new.foo({ this: 23 }, named2: "caller")
/Users/schwern/tmp/test.rb:10:in `foo': unknown keyword: named2 (ArgumentError)
from /Users/schwern/tmp/test.rb:15:in `<main>'
如何进行这项工作并保留命名参数检查的好处?我愿意将位置参数转换为命名参数。
据我所知,你想要:
- 在子方法中对相同的关键字参数使用不同的默认值
- 子方法是否有一些不传递给父方法的单独关键字参数
- 当父方法定义的签名改变时不必改变子方法定义
我认为你的问题可以通过捕获关键字参数来解决,这些关键字参数将在子方法的单独变量中直接传递给父方法,kwargs
,如下所示:
class Parent
def foo(positional, parent_kw1: "parent", parent_kw2: "parent")
puts "Positional: " + positional.inspect
puts "parent_kw1: " + parent_kw1.inspect
puts "parent_kw2: " + parent_kw2.inspect
end
end
class Child < Parent
def foo(*args, parent_kw1: "child", child_kw1: "child", **kwargs)
# Here you can use `child_kw1`.
# It will not get passed to the parent method.
puts "child_kw1: " + child_kw1.inspect
# You can also use `parent_kw1`, which will get passed
# to the parent method along with any keyword arguments in
# `kwargs` and any positional arguments in `args`.
super(*args, parent_kw1: parent_kw1, **kwargs)
end
end
Child.new.foo({this: 23}, parent_kw2: 'ABCDEF', child_kw1: 'GHIJKL')
这会打印:
child_kw1: "GHIJKL"
Positional: {:this=>23}
parent_kw1: "child"
parent_kw2: "ABCDEF"
这里的问题是,由于 parent 对 child 的参数一无所知,它无法知道您传递给它的第一个参数是否是为了是一个位置参数,或者它是否打算为 parent 方法提供关键字参数。这是因为 Ruby 允许将散列作为 keyword-argument 样式参数传递的历史特征。例如:
def some_method(options={})
puts options.inspect
end
some_method(arg1: "Some argument", arg2: "Some other argument")
打印:
{:arg1=>"Some argument", :arg2=>"Some other argument"}
如果 Ruby 不允许该语法(这会破坏与现有程序的向后兼容性),您可以使用 double splat operator:
class Child < Parent
def foo(*args, named1: "child", named2: "child", **keyword_args)
puts "Passing to parent: #{[*args, named1: named1, **keyword_args].inspect}"
super(*args, named1: named1, **keyword_args)
end
end
事实上,除了位置参数之外,当您传递关键字参数时,这种方法工作正常:
Child.new.foo({ this: 23 }, named2: "caller")
打印:
Passing to parent: [{:this=>23}, {:named1=>"child"}]
{:this=>23}
"child"
"parent"
但是,由于 Ruby 在您仅传递单个散列时无法区分位置参数和关键字参数,因此 Child.new.foo({ this: 23 })
导致 this: 23
被解释为关键字child 的参数和 parent 方法最终将转发给它的两个关键字参数解释为单个位置参数(哈希)而不是:
Child.new.foo({this: 23})
打印:
Passing to parent: [{:named1=>"child", :this=>23}]
{:named1=>"child", :this=>23}
"parent"
"parent"
有几种方法可以解决此问题,但 none 是最理想的方法。
解决方案 1
正如您在第三个示例中尝试做的那样,您可以告诉 child 传递的第一个参数将始终是位置参数,其余参数将是关键字参数:
class Child < Parent
def foo(arg, named1: "child", named2: "child", **keyword_args)
puts "Passing to parent: #{[arg, named1: named1, **keyword_args].inspect}"
super(arg, named1: named1, **keyword_args)
end
end
Child.new.foo({this: 23})
Child.new.foo({this: 23}, named1: "custom")
打印:
Passing to parent: [{:this=>23}, {:named1=>"child"}]
{:this=>23}
"child"
"parent"
Passing to parent: [{:this=>23}, {:named1=>"custom"}]
{:this=>23}
"custom"
"parent"
解决方案 2
完全切换到使用命名参数。这完全避免了这个问题:
class Parent
def foo(positional:, named1: "parent", named2: "parent")
puts positional.inspect
puts named1.inspect
puts named2.inspect
end
end
class Child < Parent
def foo(named1: "child", named3: "child", **args)
super(**args, named1: named1)
end
end
Child.new.foo(positional: {this: 23})
Child.new.foo(positional: {this: 23}, named2: "custom")
打印:
{:this=>23}
"child"
"parent"
{:this=>23}
"child"
"custom"
解决方案 3
编写一些代码以编程方式解决所有问题。
这个解决方案可能会非常复杂,并且在很大程度上取决于您希望它如何工作,但我们的想法是您将使用 Module#instance_method
, and UnboundMethod#parameters
来读取 parent 的签名的 foo 方法并相应地向它传递参数。除非您真的需要这样做,否则我建议您改用其他解决方案之一。
这个问题是关于Ruby 2.2.
假设我有一个接受位置和命名参数的方法。
class Parent
def foo(positional, named1: "parent", named2: "parent")
puts positional.inspect
puts named1.inspect
puts named2.inspect
end
end
子类既要覆盖一些默认值,又要添加自己的命名参数。我将如何最好地做到这一点?理想情况下,它不必知道父级签名的详细信息,以防父级想要添加一些可选的位置参数。我的第一次尝试是这样的。
class Child < Parent
def foo(*args, named1: "child", named3: "child" )
super
end
end
但这会爆炸,因为未知的 named3:
被传递给父级。
Child.new.foo({ this: 23 })
/Users/schwern/tmp/test.rb:10:in `foo': unknown keyword: this (ArgumentError)
from /Users/schwern/tmp/test.rb:15:in `<main>'
我试过将参数显式传递给 super,但这也不起作用。似乎第一个位置参数被视为命名参数。
class Child < Parent
def foo(*args, named1: "child", named3: "child" )
super(*args, named1: "child")
end
end
Child.new.foo({ this: 23 })
/Users/schwern/tmp/test.rb:10:in `foo': unknown keyword: this (ArgumentError)
from /Users/schwern/tmp/test.rb:15:in `<main>'
我可以让 Child 知道第一个位置参数,它有效...
class Child < Parent
def foo(arg, named1: "child", named3: "child" )
super(arg, named1: "child")
end
end
Child.new.foo({ this: 23 })
Parent.new.foo({ this: 23 })
{:this=>23}
"child"
"parent"
{:this=>23}
"parent"
"parent"
...直到我传入一个命名参数。
Child.new.foo({ this: 23 }, named2: "caller")
Parent.new.foo({ this: 23 }, named2: "caller")
/Users/schwern/tmp/test.rb:10:in `foo': unknown keyword: named2 (ArgumentError)
from /Users/schwern/tmp/test.rb:15:in `<main>'
如何进行这项工作并保留命名参数检查的好处?我愿意将位置参数转换为命名参数。
据我所知,你想要:
- 在子方法中对相同的关键字参数使用不同的默认值
- 子方法是否有一些不传递给父方法的单独关键字参数
- 当父方法定义的签名改变时不必改变子方法定义
我认为你的问题可以通过捕获关键字参数来解决,这些关键字参数将在子方法的单独变量中直接传递给父方法,kwargs
,如下所示:
class Parent
def foo(positional, parent_kw1: "parent", parent_kw2: "parent")
puts "Positional: " + positional.inspect
puts "parent_kw1: " + parent_kw1.inspect
puts "parent_kw2: " + parent_kw2.inspect
end
end
class Child < Parent
def foo(*args, parent_kw1: "child", child_kw1: "child", **kwargs)
# Here you can use `child_kw1`.
# It will not get passed to the parent method.
puts "child_kw1: " + child_kw1.inspect
# You can also use `parent_kw1`, which will get passed
# to the parent method along with any keyword arguments in
# `kwargs` and any positional arguments in `args`.
super(*args, parent_kw1: parent_kw1, **kwargs)
end
end
Child.new.foo({this: 23}, parent_kw2: 'ABCDEF', child_kw1: 'GHIJKL')
这会打印:
child_kw1: "GHIJKL"
Positional: {:this=>23}
parent_kw1: "child"
parent_kw2: "ABCDEF"
这里的问题是,由于 parent 对 child 的参数一无所知,它无法知道您传递给它的第一个参数是否是为了是一个位置参数,或者它是否打算为 parent 方法提供关键字参数。这是因为 Ruby 允许将散列作为 keyword-argument 样式参数传递的历史特征。例如:
def some_method(options={})
puts options.inspect
end
some_method(arg1: "Some argument", arg2: "Some other argument")
打印:
{:arg1=>"Some argument", :arg2=>"Some other argument"}
如果 Ruby 不允许该语法(这会破坏与现有程序的向后兼容性),您可以使用 double splat operator:
class Child < Parent
def foo(*args, named1: "child", named2: "child", **keyword_args)
puts "Passing to parent: #{[*args, named1: named1, **keyword_args].inspect}"
super(*args, named1: named1, **keyword_args)
end
end
事实上,除了位置参数之外,当您传递关键字参数时,这种方法工作正常:
Child.new.foo({ this: 23 }, named2: "caller")
打印:
Passing to parent: [{:this=>23}, {:named1=>"child"}]
{:this=>23}
"child"
"parent"
但是,由于 Ruby 在您仅传递单个散列时无法区分位置参数和关键字参数,因此 Child.new.foo({ this: 23 })
导致 this: 23
被解释为关键字child 的参数和 parent 方法最终将转发给它的两个关键字参数解释为单个位置参数(哈希)而不是:
Child.new.foo({this: 23})
打印:
Passing to parent: [{:named1=>"child", :this=>23}]
{:named1=>"child", :this=>23}
"parent"
"parent"
有几种方法可以解决此问题,但 none 是最理想的方法。
解决方案 1
正如您在第三个示例中尝试做的那样,您可以告诉 child 传递的第一个参数将始终是位置参数,其余参数将是关键字参数:
class Child < Parent
def foo(arg, named1: "child", named2: "child", **keyword_args)
puts "Passing to parent: #{[arg, named1: named1, **keyword_args].inspect}"
super(arg, named1: named1, **keyword_args)
end
end
Child.new.foo({this: 23})
Child.new.foo({this: 23}, named1: "custom")
打印:
Passing to parent: [{:this=>23}, {:named1=>"child"}]
{:this=>23}
"child"
"parent"
Passing to parent: [{:this=>23}, {:named1=>"custom"}]
{:this=>23}
"custom"
"parent"
解决方案 2
完全切换到使用命名参数。这完全避免了这个问题:
class Parent
def foo(positional:, named1: "parent", named2: "parent")
puts positional.inspect
puts named1.inspect
puts named2.inspect
end
end
class Child < Parent
def foo(named1: "child", named3: "child", **args)
super(**args, named1: named1)
end
end
Child.new.foo(positional: {this: 23})
Child.new.foo(positional: {this: 23}, named2: "custom")
打印:
{:this=>23}
"child"
"parent"
{:this=>23}
"child"
"custom"
解决方案 3
编写一些代码以编程方式解决所有问题。
这个解决方案可能会非常复杂,并且在很大程度上取决于您希望它如何工作,但我们的想法是您将使用 Module#instance_method
, and UnboundMethod#parameters
来读取 parent 的签名的 foo 方法并相应地向它传递参数。除非您真的需要这样做,否则我建议您改用其他解决方案之一。