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 ba puts bb Test.how_many puts "" c = Test.new(4,4) cb = 3 t = c.to_yaml puts t d = YAML::load(t) puts da puts db 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方法。

从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的参数的数量匹配,也不一定是它们对应的情况,并且即使它们确实处理器不知道它们应该传递给initialize的顺序。

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

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

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

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

请注意,您不需要提供这两种方法,如果需要,您可以提供任何一种方法。 如果你确实提供了两者,你在init_with传递的coder对象将基本上与在运行该方法后传递给encode_withcoder对象相同。

例如,考虑一个对象具有一些从其他实例变量计算的实例变量(可能作为优化以避免大量计算),但不应序列化为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实例变量。

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

如果您只想使用使用@ -style实例变量的纯ruby类(不是来自编译扩展而不是Struct -style的那些)的此行为,则以下内容应该有效。 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.foo = Foo.new => # >> [bar.yaml_flag, bar.foo.yaml_flag] => [nil, nil] >> bar_reloaded = YAML.load YAML.dump bar => #, @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 => #, @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 => #, @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'", 

from_yaml(输入)

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

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

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