如何在子类中添加命名参数或在 Ruby 2.2 中更改它们的默认值?
Posted
技术标签:
【中文标题】如何在子类中添加命名参数或在 Ruby 2.2 中更改它们的默认值?【英文标题】:How do I add named parameters in a subclass or change their default in Ruby 2.2? 【发布时间】:2015-07-11 16:08:06 【问题描述】:这个问题是关于 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>'
如何进行这项工作并保留命名参数检查的好处?我愿意将位置参数转换为命名参数。
【问题讨论】:
很难说出你在问什么......代码Child.new.foo( this: 23 )
不会引发错误,因为named3
被传递给父级,它会引发错误,因为this
被传递给父母。您是否希望将哈希 this: 23
传递给父方法中的 positional
参数?
@Adrian 是的,我期待 this: 23
作为位置参数传递。它被视为命名参数。我显然误解了一些东西。
【参考方案1】:
这里的问题是,由于父级对子级的参数一无所知,它无法知道您传递给它的第一个参数是否是位置参数,或者它是否旨在提供父方法的关键字参数。这是因为 Ruby 允许将哈希作为关键字参数样式参数传递的历史特性。例如:
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
被子进程解释为关键字参数,并且父方法结束up 将转发给它的两个关键字参数解释为单个位置参数(哈希):
Child.new.foo(this: 23)
打印:
Passing to parent: [:named1=>"child", :this=>23]
:named1=>"child", :this=>23
"parent"
"parent"
有几种方法可以解决这个问题,但没有一个是完全理想的。
解决方案 1
正如您在第三个示例中尝试做的那样,您可以告诉孩子传递的第一个参数将始终是位置参数,其余的将是关键字 args:
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
和 UnboundMethod#parameters
来读取父级 foo 方法的签名并相应地向它传递参数。除非您真的需要这样做,否则我建议您改用其他解决方案之一。
【讨论】:
谢谢,我最终选择了解决方案 2。很高兴知道解决方案 1。【参考方案2】:据我所知,你想要:
在子方法中为相同的关键字参数使用不同的默认值 让子方法有一些单独的关键字参数,这些参数不会传递给父方法 当父方法定义的签名改变时,不必改变子方法定义我认为您的问题可以通过捕获关键字参数来解决,这些关键字参数将直接传递给子方法中的单独变量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"
【讨论】:
我只是建议这样做。问题是当您不将任何关键字参数传递给孩子时,它不起作用。试试Child.new.foo(this: 23)
。 :parent_kw1=>"child"
最终作为传递给父级的位置参数(至少在 Ruby 2.2.1 上......)。
@Ajedi32 你说得对,似乎解决这个问题的唯一方法就是写 Child.new.foo(this: 23, **)
这很难看。 :( 我不知道是否有任何其他方法可以防止 ruby 将哈希解释为一组关键字参数,而不是在子方法中使用 Parent.instance_method(:foo).parameters
。以上是关于如何在子类中添加命名参数或在 Ruby 2.2 中更改它们的默认值?的主要内容,如果未能解决你的问题,请参考以下文章