如何实现 Ruby 结构的自动生成?

How to implement autovivification for Ruby structs?

Ruby 通过将块传递给 Hash.new:

来支持哈希的自动生成
hash = Hash.new { |h, k| h[k] = 42 }
hash[:foo] += 1   # => 43

我也想为结构实现自动生成。这是我能想到的最好的:

Foo = Struct.new(:bar) do
  def bar
    self[:bar] ||= 42
  end
end

foo = Foo.new
foo.bar += 1   # => 43

当然,这只会自动生成命名访问器 (foo.bar),而不是 [] 形式 (foo[:bar])。有没有更好的方法来实现结构的自动生成,特别是对 foo.barfoo[:bar] 形式都有效的方法?

我会采用以下方法:

module StructVivificator
  def self.prepended(base)
    base.send(:define_method, :default_proc) do |&λ|
      instance_variable_set(:@λ, λ)
    end
  end
  def [](name)
    super || @λ && @λ.() # or more sophisticated checks
  end
end

Foo = Struct.new(:bar) do
  prepend StructVivificator
end

foo = Foo.new
foo.default_proc { 42 } # declare a `default_proc` as in Hash

foo[:bar] += 1   # => 43
foo.bar += 1     # => 44

foo.bar 上面通过 method_missing 魔法调用 foo[:bar],所以唯一要覆盖的是 Struct#[] 方法。

预置模块使其更健壮、基于实例并且通常更灵活。


上面的代码只是一个例子。要复制 Hash#default_proc 的行为,可能(感谢@Stefan 的评论):

module StructVivificator
  def self.prepended(base)
    raise 'Sorry, structs only!' unless base < Struct

    base.singleton_class.prepend(Module.new do
      def new(*args, &λ) # override `new` to accept block
        super(*args).tap { @λ = λ }
      end
    end)
    base.send(:define_method, :default_proc=) { |λ| @λ = λ }
    base.send(:define_method, :default_proc) { |&λ| λ ? @λ = λ : @λ }

    # override accessors (additional advantage: performance/clarity)
    base.members.each do |m|
      base.send(:define_method, m) { self[m] }
      base.send(:define_method, "#{m}=") { |value| self[m] = value }
    end
  end
  def [](name)
    super || default_proc && default_proc.(name) # or more sophisticated checks
  end
end

现在 default_proc lambda 将收到一个 name 来决定在这种情况下如何表现。

Foo = Struct.new(:bar, :baz) do
  prepend StructVivificator
end

foo = Foo.new
foo.default_proc = ->(name) { name == :bar ? 42 : 0 }
puts foo.bar          # => 42
puts foo[:bar] += 1   # => 43
puts foo.bar += 1     # => 44
puts foo[:baz] += 1   # => 1