💭

[Bug #20440] super にキーワード引数をフォワードするときの挙動に関するチケット

2024/04/24に公開

[Bug #20440] super from child class duplicating a keyword argument as a positional Hash

  • 次のように super に引数をフォワードする場合にキーワード引数ではなくて位置引数として渡っている、というバグ報告
    • Ruby 2.7 では期待する挙動になっていたが Ruby 3.0 から挙動が変わった
class Base
  def foo(*args, x: 1)
    "Base: calling foo with args: #{args}, x: #{x}"
  end
end

class Child < Base
  def foo(*)
    # このときに foo に渡ってきた引数がそのまま親メソッドにフォワードされることを期待するがそのときにキーワード引数ではなくて位置引数として渡ってしまっている
    super
  end
end

pp Child.new.foo(0, x: 2)
# Ruby 2.7 => "Base: calling foo with args: [0], x: 2"
# Ruby 3.0 => "Base: calling foo with args: [0, {:x=>2}], x: 1"
  • これは foo(*) のシグネチャが『位置引数の可変長引数』になっているのが原因で * だと『位置引数』として引数を受け取る『位置引数を』 super でフォワードすることにある
  • なので次のように foo(*, **) とすることで『キーワード引数』を受け取りそれをフォワードすることができる
class Base
  def foo(*args, x: 1)
    "Base: calling foo with args: #{args}, x: #{x}"
  end
end

class Child < Base
  # * だけだと位置引数で受け取るが ** を付ける事でキーワード引数として値を受け取る事ができる
  def foo(*, **)
    super
  end
end

pp Child.new.foo(0, x: 2)
# => "Base: calling foo with args: [0], x: 2"
  • 他には (...) でも回避する事ができますね
class Base
  def foo(*args, x: 1)
    "Base: calling foo with args: #{args}, x: #{x}"
  end
end

class Child < Base
  # * だけだと位置引数で受け取るが ** を付ける事でキーワード引数として値を受け取る事ができる
  def foo(...)
    super
  end
end

pp Child.new.foo(0, x: 2)
# => "Base: calling foo with args: [0], x: 2"
  • ちなみに Ruby 2.7 と同じ挙動にしたい場合は ruby2_keywords が利用できます
class Base
  def foo(*args, x: 1)
    "Base: calling foo with args: #{args}, x: #{x}"
  end
end

class Child < Base
  ruby2_keywords def foo(*)
    # このときに foo に渡ってきた引数がそのまま親メソッドにフォワードされることを期待するがそのときにキーワード引数ではなくて位置引数として渡ってしまっている
    super
  end
end

pp Child.new.foo(0, x: 2)
# => "Base: calling foo with args: [0], x: 2"
GitHubで編集を提案

Discussion