如何在子类中添加命名参数或在Ruby 2.2中更改其默认值?

时间:2015-05-01 19:07:51

标签: ruby 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>'

我可以让孩子知道第一个位置参数,它有效......

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>'

如何使这项工作并保留命名参数检查的好处?我打开将位置参数转换为命名参数。

2 个答案:

答案 0 :(得分:4)

这里的问题是,由于父母对子的参数一无所知,因此无法知道传递给它的第一个参数是否是一个位置参数,或者是否打算为父方法提供关键字参数。这是因为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被孩子解释为关键字参数,并且父方法最终将作为单个位置参数(哈希)转发给它的两个关键字参数解释为:

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_methodUnboundMethod#parameters来读取签名。 parent的foo方法并相应地传递参数。除非您真的需要这样做,否则我建议您改用其他解决方案。

答案 1 :(得分: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"