Ruby YAML解析器通过传递构造函数

时间:2012-10-10 14:15:35

标签: ruby yaml

我正在开发一个应用程序,它从YAML文件中获取输入,将它们解析为对象,然后让它们完成它们的工作。我现在唯一的问题是,YAML解析器似乎忽略了对象“initialize”方法。我指望构造函数填充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

我不明白如何在不使用定义的initialize方法的情况下创建这些对象。我也想知道是否还有强制解析器使用initialize方法。

3 个答案:

答案 0 :(得分:10)

从Yaml反序列化对象不使用initialize方法,因为通常对象的实例变量(默认的Yaml序列化存储的变量)和initialize的参数之间没有对应关系

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

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

现在,当反序列化一个实例时,Yaml处理器的值为@a_variable,但initialize方法需要两个参数,因此无法调用它。即使实例变量的数量与initialize的参数数量匹配,也不一定是它们对应的情况,即使它们确实处理器不知道它们的顺序,它们也应该传递给{{1} }。

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

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

对于序列化,Psych会调用对象的init_with方法(如果它存在),并传递coder object。此对象允许您指定如何在Yaml中表示对象 - 通常您只需将其视为哈希。

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

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

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

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 值:

calculated

当您将此Yaml加载回Ruby时,创建的对象将设置--- !ruby/object:Foo first: 1 second: 2 实例变量。

如果你想要可以@calculated内拨打initialize,但我认为最好在初始化 new <之间保持清晰的分离/ em>类的实例,并从Yaml反序列化现有的实例。我建议将通用逻辑提取到可以从两者中调用的方法

答案 1 :(得分:3)

如果您只想使用纯ruby类使用@样式实例变量(不是来自编译扩展而不是Struct - 样式)的行为,则以下内容应该有效。 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加载,你将不得不做一些更脆弱和复杂的事情,比如在allocate方法中检查caller堆栈以查看yaml是否正在调用它。

我正在使用ruby 1.9.3(带有心理)并且堆栈的顶部看起来像这样(删除了路径前缀):

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'",

答案 2 :(得分:1)

  

from_yaml(input)

     

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

     

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

这就是执行YAML::Load时没有运行initialize方法的原因。