Избегайте повторения именованных аргументов по умолчанию при вызове инициализатора суперкласса в Ruby (2.1+)

Скажем, у меня есть родительский класс, инициализатор которого имеет аргумент со значением по умолчанию:

class Parent
  attr_reader :foo
  def initialize(foo: 123)
    @foo = foo
  end
end

Я хочу создать подкласс с тем же значением по умолчанию для foo. Я могу сделать это, если повторю объявление:

class Child < Parent
  attr_reader :bar
  def initialize(foo: 123, bar: 456)
    super(foo: foo)
    @bar = bar
  end
end

Однако это означает, что я должен написать 123 дважды. Если я попытаюсь избежать повторения этого, опустив его...

class Child < Parent
  attr_reader :bar
  def initialize(foo:, bar: 456)
    super(foo: foo)
    @bar = bar
  end
end

-- теперь это означает, что ранее необязательный foo по умолчанию теперь требуется для подкласса, и на самом деле я вообще не получаю никакой выгоды от значения по умолчанию.

Я подумал, что смогу установить по умолчанию nil в подклассе --

class Child < Parent
  attr_reader :bar
  def initialize(foo: nil, bar: 456)
    super(foo: foo)
    @bar = bar
  end
end

-- но нет; Child.new(bar:789).foo теперь nil, тогда как я хотел 123.

Я тоже не могу полностью отказаться от аргумента...

class Child < Parent
  attr_reader :bar
  def initialize(bar: 456)
    super(foo: foo)
    @bar = bar
  end
end

-- потому что тогда, если я попытаюсь указать это (Child.new(foo: 345, bar:789)), я получу unknown keyword: foo (ArgumentError).

Есть ли способ фактически опустить аргумент вместо того, чтобы присвоить ему значение по умолчанию? И/или способ разрешить инициализатору принимать произвольные дополнительные именованные параметры и передавать их инициализатору суперкласса?


Обновление: я придумал следующий хак (по сути, вручную меняя свои собственные «параметры по умолчанию»), но я не очень доволен этим.

class Parent
  attr_reader :foo
  def initialize(foo: nil)
    @foo = foo || 123 # faking 'default'-ness
  end
end

class Child < Parent
  attr_reader :bar
  def initialize(foo: nil, bar: 456)
    super(foo: foo)
    @bar = bar
  end
end

Наверняка есть какой-то более Ruby-подобный способ сделать это?


person David Moles    schedule 07.05.2015    source источник


Ответы (2)


В Ruby 2.0+ вы можете использовать оператор двойного знака.

def initialize(bar: 456, **args)
  super(**args)
  @bar = bar
end

Пример:

[1] pry(main)> class Parent
[1] pry(main)*   def initialize(a: 456)
[1] pry(main)*     @a = a
[1] pry(main)*   end  
[1] pry(main)* end  
=> :initialize
[2] pry(main)> class Child < Parent
[2] pry(main)*   def initialize(b: 789, **args)
[2] pry(main)*     super(**args)
[2] pry(main)*     @b = b
[2] pry(main)*   end  
[2] pry(main)* end  
=> :initialize
[3] pry(main)> ch = Child.new(b: 3)
=> #<Child:0x007fc00513b128 @a=456, @b=3>
[4] pry(main)> ch = Child.new(b: 3, a: 6829)
=> #<Child:0x007fc00524a550 @a=6829, @b=3>

Оператор двойного знака аналогичен оператору одиночного знака, но вместо того, чтобы записывать все дополнительные аргументы в массив, он захватывает их в хэш. Затем, когда он используется в качестве аргумента для super, двойной знак сглаживает хэш в именованные параметры, вроде того, как одиночный знак делает для массивов.

person Sean Hill    schedule 08.05.2015

Это больше похоже на Ruby-1.9, чем на Ruby-2.1, но я считаю, что он делает то, что вы хотите, избегая повторений:

class Parent
  attr_reader :foo
  def initialize(opts = {})
    @foo = opts[:foo] || 123
  end
end

class Child < Parent
  attr_reader :bar
  def initialize(opts = {})
    super(opts)
    @bar = opts[:bar] || 456
  end
end

puts Parent.new.foo # => 123
puts Parent.new(foo: 1).foo # => 1
puts Parent.new(bar: 2).foo # => 123
puts Parent.new(foo: 1, bar: 2).foo # => 1
person Hew Wolff    schedule 07.05.2015