【问题标题】:loading/unloading/updating class in ruby在 ruby​​ 中加载/卸载/更新类
【发布时间】:2009-12-14 16:21:34
【问题描述】:

我用 Ruby 类动态加载/卸载/更新作为实现插件的基础设施做了一些实验。我发现了几点:

  1. 如果加载同一类的新版本而不首先卸载它,则新版本本质上是“顶部”或“合并”与以前的版本。使用先前版本创建的所有现有对象都将“更新”其类定义。
  2. 卸载类不会影响使用该类创建的现有对象。现有对象保留在刚刚卸载的任何版本中。 (类不能再使用,但已经创建的对象不能使用)
  3. 如果在卸载旧版本后加载新版本,则创建的新对象将属于新版本。但是,在加载新版本之前创建的旧对象不会受到影响,并且仍然是旧版本。

我的问题是,有没有一种简单的方法可以使从旧类版本创建的现有对象“切换”到新版本(但不是新旧版本的合并版本)?在我看来,可能的方法是在卸载/加载后重新创建对象,这不适合插件(不希望它被破坏)。

更新:我的意图是用新版本更新现有对象,而不会出现将旧版本与新版本合并的问题(例如更改参数数量,或删除一个方法)。卸载然后重新加载似乎是最干净的方法,尽管您必须跟踪所有此类对象并在需要时重新创建它们。此外,昂贵的对象可能不适合重新创建。这给我留下了第二个选择,禁止发生意外的合并。只要没有删除任何方法,没有更改方法签名,合并就可以正常工作。

下面是我的测试程序:

$ cat test.rb
load 'v1.rb'
puts "=> 'v1.rb' loaded"
a1 = A.new
puts "=> object a1(#{a1}) created"
a1.common
a1.method_v1
load 'v2.rb'
puts '',"=> class A updated by 'v2.rb'"
a1.common
a1.method_v1
a1.method_v2

a2 = A.new
puts '',"=> object a2(#{a2}) created"
a2.common
a2.method_v1
a2.method_v2

Object.send(:remove_const, 'A')
puts '',"=> class A unloaded"

A.new rescue puts $!

puts '',"=> class A does not exist now"
a1.common
a1.method_v1
a1.method_v2 rescue puts $!
a2.common
a2.method_v1
a2.method_v2

load 'v3.rb'
puts '',"=> 'v3.rb' loaded"
a1.common
a1.method_v1
a1.method_v2 rescue puts $!
a1.method_v3 rescue puts $!
a2.common
a2.method_v1
a2.method_v2
a2.method_v3 rescue puts $!

a3 = A.new
puts '',"=> object a3(#{a3}) create"
a3.common
a3.method_v1 rescue puts $!
a3.method_v2 rescue puts $!
a3.method_v3

样本输出:

$ ruby test.rb
=> 'v1.rb' loaded
=> object a1(#<A:0x1042d4b0>) created
#<A:0x1042d4b0>: common: v1
#<A:0x1042d4b0>: method v1

=> class A updated by 'v2.rb'
#<A:0x1042d4b0>: common: v2
#<A:0x1042d4b0>: method v1
#<A:0x1042d4b0>: method v2

=> object a2(#<A:0x1042cec0>) created
#<A:0x1042cec0>: common: v2
#<A:0x1042cec0>: method v1
#<A:0x1042cec0>: method v2

=> class A unloaded
uninitialized constant A

=> class A does not exist now
#<A:0x1042d4b0>: common: v2
#<A:0x1042d4b0>: method v1
#<A:0x1042d4b0>: method v2
#<A:0x1042cec0>: common: v2
#<A:0x1042cec0>: method v1
#<A:0x1042cec0>: method v2

=> 'v3.rb' loaded
#<A:0x1042d4b0>: common: v2
#<A:0x1042d4b0>: method v1
#<A:0x1042d4b0>: method v2
undefined method `method_v3' for #<A:0x1042d4b0>
#<A:0x1042cec0>: common: v2
#<A:0x1042cec0>: method v1
#<A:0x1042cec0>: method v2
undefined method `method_v3' for #<A:0x1042cec0>

=> object a3(#<A:0x1042c3f8>) create
#<A:0x1042c3f8>: common: v3
undefined method `method_v1' for #<A:0x1042c3f8>
undefined method `method_v2' for #<A:0x1042c3f8>
#<A:0x1042c3f8>: method v3

以下是A类的3个版本:

$ cat v1.rb
class A
  def common
    puts "#{self}: common: v1"
  end
  def method_v1
    puts "#{self}: method v1"
  end
end

$ cat v2.rb
class A
  def common
    puts "#{self}: common: v2"
  end
  def method_v2
    puts "#{self}: method v2"
  end
end

$ cat v3.rb
class A
  def common
    puts "#{self}: common: v3"
  end
  def method_v3
    puts "#{self}: method v3"
  end
end

【问题讨论】:

  • 您实际上并没有卸载类 A。您将其与其名称断开连接,但类 A 的现有实例仍然持有对该类的引用。该类在可以被垃圾回收时被删除,这发生在该类的所有实例都被垃圾回收时。

标签: ruby metaprogramming


【解决方案1】:

显然,用新的类定义完全替换类定义存在危险,无论您是合并新版本还是删除旧版本并期望对象自动更新。这种危险在于对象的旧版本可能对新版本处于无效状态。 (例如,新版本的类在其initialize 方法中初始化的实例变量可能没有被旧版本定义,但也可能存在比这更微妙的错误)。 因此,无论您如何实现这一目标,都需要小心(以及精心策划的升级路径)。

鉴于您知道要升级的版本是什么样的(无论如何,您需要它才能进行明智的升级),让新版本的类从旧版本的类中删除不需要的方法非常简单:

class A
  remove_method :foo
end

当您说重新定义采用不同数量参数的方法存在问题时,我不确定您在说什么。它对我来说很好用:

class A
  def foo a
    a
  end
end
ainst=A.new
p(ainst.foo 1) rescue puts($!)
p(ainst.foo 1,2) rescue puts($!)

class A
  def foo a,b
    [a,b]
  end
end
p(ainst.foo 1) rescue puts($!)
p(ainst.foo 1,2) rescue puts($!)

你唯一不能做的事情(AFAIK)就是改变类的超类。这是在您第一次定义类时定义的,您不能更改它(尽管您可以再次指定相同的祖先类)。

class A < Object
end
class A < Object
end
class A < String #TypeError: superclass mismatch for class A
end

【讨论】:

  • 我完全同意你的观点,升级必须有良好的计划和控制。因此,如果一个方法被删除,新版本应该明确地删除它(通过 remove_method)。方法签名的问题(参数数量的变化)实际上与调用者和此类之间的版本不匹配有关。但考虑到这一点,这也应该包含在“受控”升级路径中。不应该有任何版本不匹配,即使在这个类之外。
【解决方案2】:

简而言之,如果没有一些严重的黑客攻击,就没有办法做到这一点。我建议您做的是创建一个to_serialized 方法,该方法返回一个数组,initialize 方法接受该数组以获得相同的状态。如果你只是想复制所有实例变量,你可以这样做:

class A
  def initialize(instance_variables)
    instance_variables.each do |key, value|
      self.instance_variable_set(key, value)
    end
  end

  def to_serialized
    iv = {}
    self.instance_variables.each do |key|
      iv[key] = self.instance_variable_get(key)
    end
  end
end

要重新加载该方法,您可以这样做:

obj_state = object.to_serialized
Object.send(:remove_const, 'A')
load 'file.rb'
object = A.new(obj_state)

请注意,这不会嵌套,因此如果实例变量引用的任何对象也被重新加载,您需要自己“序列化”它们。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-07-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多