如何在子类中添加命名参数或在 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 方法并相应地向它传递参数。除非您真的需要这样做,否则我建议您改用其他解决方案之一。