如何在子类中添加命名参数或在 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_methodUnboundMethod#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=&gt;"child" 最终作为传递给父级的位置参数(至少在 Ruby 2.2.1 上......)。 @Ajedi32 你说得对,似乎解决这个问题的唯一方法就是写 Child.new.foo(this: 23, **) 这很难看。 :( 我不知道是否有任何其他方法可以防止 ruby​​ 将哈希解释为一组关键字参数,而不是在子方法中使用 Parent.instance_method(:foo).parameters

以上是关于如何在子类中添加命名参数或在 Ruby 2.2 中更改它们的默认值?的主要内容,如果未能解决你的问题,请参考以下文章

为啥不能在 Ruby 3 中结合 `...` 和命名参数?

何时在 Ruby 中使用关键字参数,也就是命名参数

Ruby 中没有命名参数?

命名参数作为 Ruby 中的局部变量

在 Ruby 中动态定义命名类

使用 Laravel Eloquent 和命名绑定的 SQL 查询:混合命名参数和位置参数