загрузка / выгрузка / обновление класса в ruby - PullRequest
5 голосов
/ 14 декабря 2009

Я провел несколько экспериментов с динамической загрузкой / выгрузкой / обновлением класса Ruby в качестве реализации инфраструктуры плагинов.Я обнаружил несколько моментов:

  1. Если загружать новую версию того же класса без предварительной ее выгрузки, новая, по сути, «top» или «объединяется» с предыдущей версией.Для всех существующих объектов, созданных в предыдущей версии, определение их класса будет «обновлено».
  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

Ниже представлены 3 версии класса A:

$ 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

Ответы [ 2 ]

1 голос
/ 18 декабря 2009

Очевидно, что существует опасность полной замены определения класса новым определением класса, независимо от того, объединяете ли вы новую версию или удаляете старую версию и ожидаете, что объекты будут автоматически обновлены. Эта опасность заключается в том, что старая версия объекта может находиться в недопустимом состоянии для новой версии. (Например, переменные экземпляра, которые новая версия класса инициализирует в своем методе 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
1 голос
/ 14 декабря 2009

Короче говоря, без серьезного взлома сделать это невозможно. Я предлагаю вам создать метод 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)

Обратите внимание, что это не гнездо, поэтому, если какой-либо из объектов, на которые ссылаются переменные экземпляра, тоже перезагружается, вам нужно "сериализовать" их самостоятельно.

...