5

这个问题是关于 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>'

如何进行这项工作并保留命名参数检查的好处?我愿意将位置参数转换为命名参数。

4

2 回答 2

7

这里的问题是,由于父级对子级的参数一无所知,它无法知道您传递给它的第一个参数是否是位置参数,或者它是否旨在提供关键字参数父方法。这是因为 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 运算符编写您的子方法:

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.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 方法的签名并相应地向其传递参数。除非您真的需要这样做,否则我建议您改用其他解决方案之一。

于 2015-05-01T20:34:39.303 回答
1

据我所知,您想要:

  • 在子方法中为相同的关键字参数使用不同的默认值
  • 让孩子的方法有一些单独的关键字参数,这些参数不会传递给父母
  • 当父方法定义的签名改变时不必改变子方法定义

我认为您的问题可以通过捕获关键字参数来解决,这些关键字参数将直接传递给子方法中的单独变量中的父方法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"
于 2015-05-01T19:37:26.927 回答