9

我正在开发一个应用程序,该应用程序从 YAML 文件中获取输入,将它们解析为对象,然后让它们做自己的事情。我现在遇到的唯一问题是 YAML 解析器似乎忽略了对象“初始化”方法。我指望构造函数填充 YAML 文件缺少默认值的任何实例变量,并将一些东西存储在类变量中。这是一个例子:

class Test

    @@counter = 0

    def initialize(a,b)
        @a = a
        @b = b

        @a = 29 if @b == 3

        @@counter += 1
    end

    def self.how_many
        p @@counter
    end

    attr_accessor :a,:b

end

require 'YAML'

a = Test.new(2,3)
s = a.to_yaml
puts s
b = YAML::load(s)
puts b.a
puts b.b
Test.how_many

puts ""

c = Test.new(4,4)
c.b = 3
t = c.to_yaml
puts t
d = YAML::load(t)
puts d.a
puts d.b
Test.how_many

我本来希望上面的输出:

--- !ruby/object:Test
a: 29
b: 3
29
3
2

--- !ruby/object:Test
a: 4
b: 3
29
3
4

相反,我得到了:

--- !ruby/object:Test
a: 29
b: 3
29
3
1

--- !ruby/object:Test
a: 4
b: 3
4
3
2

如果不使用它们定义的初始化方法,我不明白它是如何制作这些对象的。我还想知道是否有强制解析器使用初始化方法。

4

3 回答 3

10

从 Yaml 反序列化对象不使用该initialize方法,因为通常在对象的实例变量(这是默认 Yaml 序列化存储的内容)和initialize.

作为一个例子,考虑一个initialize看起来像这样的对象(没有其他实例变量):

def initialize(param_one, param_two)
  @a_variable = some_calculation(param_one, param_two)
end

现在当反序列化 this 的一个实例时,Yaml 处理器有一个值 for @a_variable,但是该initialize方法需要两个参数,所以它不能调用它。即使实例变量的数量与参数的数量相匹配,initialize它们也不一定是对应的,即使它们对应,处理器也不知道它们应该传递给的顺序initialize

将 Ruby 对象序列化和反序列化到 Yaml 的默认过程是在序列化期间写出所有实例变量(及其名称),然后在反序列化时分配该类的新实例并在此新实例上简单地设置相同的实例变量。

当然,有时您需要更多地控制此过程。如果您使用的是 Psych Yaml 处理器(这是 Ruby 1.9.3 中的默认值),那么您应该适当地实现encode_with(用于序列化)或或init_with(用于反序列化)方法。

对于序列化,如果对象存在,Psych 将调用对象的encode_with方法,并传递一个coderobject。这个对象允许你指定对象应该如何在 Yaml 中表示——通常你只是把它当作一个哈希。

对于反序列化,如果该方法存在于您的对象上,Psych 将调用该init_with方法,而不是使用上述默认过程,再次传递一个coder对象。这次coder将包含有关 Yaml 中对象表示的信息。

请注意,您不需要同时提供这两种方法,如果需要,您可以只提供其中一种。如果两者都提供,则coder传入的对象本质上与该方法运行后init_with传递给的对象相同。encode_with

举个例子,考虑一个对象,它有一些实例变量是从其他变量中计算出来的(可能是为了避免大量计算的优化),但不应该序列化到 Yaml。

class Foo

  def initialize(first, second)
    @first = first
    @second = second
    @calculated = expensive_calculation(@first, @second)
  end

  def encode_with(coder)
    # @calculated shouldn’t be serialized, so we just add the other two.
    # We could provide different names to use in the Yaml here if we
    # wanted (as long as the same names are used in init_with).
    coder['first'] = @first
    coder['second'] = @second
  end

  def init_with(coder)
    # The Yaml only contains values for @first and @second, we need to
    # recalculate @calculated so the object is valid.
    @first = coder['first']
    @second = coder['second']
    @calculated = expensive_calculation(@first, @second)
  end

  # The expensive calculation
  def expensive_calculation(a, b)
    ...
  end
end

当你将这个类的一个实例转储到 Yaml 时,它看起来像这样,没有calculated值:

--- !ruby/object:Foo
first: 1
second: 2

当您将此 Yaml 加载回 Ruby 时,创建的对象将@calculated设置实例变量。

如果你愿意,你可以initialize从inside调用init_with,但我认为在初始化类的实例和从 Yaml反序列化现有实例之间保持清晰的分离会更好。我建议将通用逻辑提取到可以从两者调用的方法中,

于 2012-10-16T19:54:43.263 回答
3

如果您只希望纯 ruby​​ 类使用@-style 实例变量(而不是来自已编译扩展而不是Struct-style)的纯 ruby​​ 类的这种行为,则以下内容应该有效。YAML 似乎allocate在加载该类的实例时调用类方法,即使该实例嵌套为另一个对象的成员也是如此。所以我们可以重新定义allocate. 例子:

class Foo
  attr_accessor :yaml_flag
  def self.allocate
    super.tap {|o| o.instance_variables.include?(:@yaml_flag) or o.yaml_flag = true }
  end
end
class Bar
  attr_accessor :foo, :yaml_flag
  def self.allocate
    super.tap {|o| o.instance_variables.include?(:@yaml_flag) or o.yaml_flag = true }
  end
end

>> bar = Bar.new
=> #<Bar:0x007fa40ccda9f8>
>> bar.foo = Foo.new
=> #<Foo:0x007fa40ccdf9f8>
>> [bar.yaml_flag, bar.foo.yaml_flag]
=> [nil, nil]
>> bar_reloaded = YAML.load YAML.dump bar
=> #<Bar:0x007fa40cc7dd48 @foo=#<Foo:0x007fa40cc7db90 @yaml_flag=true>, @yaml_flag=true>
>> [bar_reloaded.yaml_flag, bar_reloaded.foo.yaml_flag]
=> [true, true]

# won't overwrite false
>> bar.foo.yaml_flag = false
=> false
>> bar_reloaded = YAML.load YAML.dump bar
=> #<Bar:0x007fa40ccf3098 @foo=#<Foo:0x007fa40ccf2f08 @yaml_flag=false>, @yaml_flag=true>
>> [bar_reloaded.yaml_flag, bar_reloaded.foo.yaml_flag]
=> [true, false]

# won't overwrite nil
>> bar.foo.yaml_flag = nil
=> nil
>> bar_reloaded = YAML.load YAML.dump bar
=> #<Bar:0x007fa40cd73518 @foo=#<Foo:0x007fa40cd73360 @yaml_flag=nil>, @yaml_flag=true>
>> [bar_reloaded.yaml_flag, bar_reloaded.foo.yaml_flag]
=> [true, nil]

我故意避免o.nil?检查tap块,因为nil实际上可能是您不想覆盖的有意义的值。

最后一个警告:allocate可能由第三方库(或您自己的代码)使用,您可能不想在这些情况下设置成员。如果你想限制分配,只加载 yaml,你将不得不做一些更脆弱和复杂的事情,比如检查callerallocate 方法中的堆栈以查看 yaml 是否正在调用它。

我正在使用 ruby​​ 1.9.3(带有 psych),堆栈顶部看起来像这样(路径前缀已删除):

psych/visitors/to_ruby.rb:274:in `revive'",
psych/visitors/to_ruby.rb:219:in `visit_Psych_Nodes_Mapping'",
psych/visitors/visitor.rb:15:in `visit'",
psych/visitors/visitor.rb:5:in `accept'",
psych/visitors/to_ruby.rb:20:in `accept'",
psych/visitors/to_ruby.rb:231:in `visit_Psych_Nodes_Document'",
psych/visitors/visitor.rb:15:in `visit'",
psych/visitors/visitor.rb:5:in `accept'",
psych/visitors/to_ruby.rb:20:in `accept'",
psych/nodes/node.rb:35:in `to_ruby'",
psych.rb:128:in `load'",
于 2012-10-16T17:39:26.527 回答
1

from_yaml(输入)

YAML 文件的特殊加载器。当从 YAML 文件加载规范对象时,它会绕过正常的 Ruby 对象初始化例程(初始化)。这种方法弥补了这一点,并处理不同年龄的宝石。

输入可以是 YAML.load() 接受的任何内容:字符串或 IO。

这就是执行时未运行初始化方法的原因YAML::Load

于 2012-10-15T20:31:27.347 回答