Ruby Marshal.load не поддерживает порядок отсортированного набора

Я сохраняю объект SortedSet в файле с помощью Marshal.dump. Элементы в наборе также являются объектами (которые включают Comparable и реализуют метод ‹=>).

Позже при восстановлении этого объекта с помощью Marshal.load SortedSet, загружаемый из файла, не сортируется...

Любая идея, почему или как это исправить?

Вот упрощенный пример, который воспроизводит проблему:

require 'set'
class Foo
  include Comparable

  attr_accessor :num

  def initialize(num)
    @num = num
  end

  def <=>(other)
    num <=> other.num
  end
end

f1 = Foo.new(1)
f2 = Foo.new(2)
f3 = Foo.new(3)

s = SortedSet.new([f2, f1, f3])

File.open('set_test.dump', 'wb') { |f| Marshal.dump(s, f) }

Затем, чтобы загрузить объект из файла, который я использую -

File.open('set_test.dump', 'rb') { |f| ls = Marshal.load(f) }

** Я использую Rails 3.2.3 с Ruby 2.1.8.

** При загрузке дампа из файла - делайте это в новой/отдельной консоли rails (и не забудьте скопировать-вставить определение класса Foo :-) )


person Shai Kerer    schedule 23.01.2017    source источник
comment
Я пробовал это в Ruby 2.1.10 и 2.4.0. Я не могу воспроизвести вашу проблему.   -  person spickermann    schedule 24.01.2017
comment
Я тоже не вижу проблемы. Примечание File.open('set_test.dump', 'rb') { |f| ls = Marshal.load(f) } также может быть записано как Marshal.load(File.binread('set_test.dump')).   -  person Cary Swoveland    schedule 24.01.2017
comment
Серьезно? Я только что воспроизвел его в Ruby 2.1.10 и 2.2.2 (в IRB, без рельсов. Нужно требовать «установить»). Возможно, это связано с ОС (я на Mac с OS-X El Capitan)? или тот факт, что я использую RVM? Странно, что вы не можете воспроизвести... У меня нет идей... просто трепаться...   -  person Shai Kerer    schedule 24.01.2017
comment
Mac Os X 10.9.5, Ruby 2.0.0, без RVM. Я могу воспроизвести проблему.   -  person Eric Duminil    schedule 24.01.2017
comment
Linux Mint, рубин 2.3.1p112, RVM. Marshal.load также возвращает #<SortedSet: {#<Foo:0x0000000291b480 @num=2>, #<Foo:0x0000000291bbb0 @num=1>, #<Foo:0x00000002919d10 @num=3>}>   -  person Eric Duminil    schedule 24.01.2017
comment
@muistooshort: я не понимаю вашего комментария. Пропущено слово?   -  person Eric Duminil    schedule 24.01.2017
comment
Я думаю, что он пропустил "не" - ... должен НЕ использовать Marshal. И он прав. При создании дампа с помощью Marshal с определенной версией Ruby он может неправильно загрузиться с другой версией Ruby.   -  person Shai Kerer    schedule 24.01.2017
comment
@EricDuminil Правильно, там отсутствует not, я перепишу его.   -  person mu is too short    schedule 24.01.2017
comment
Кроме того, вам действительно не следует использовать Marshal для настойчивости. Формат Marshal зависит (более или менее) от версии Ruby, которую вы используете, и нет никакой гарантии, что новый/старый Ruby сможет читать старые/новые форматы. (Как указывает @ShaiKerer, в исходном комментарии отсутствовал not).   -  person mu is too short    schedule 24.01.2017


Ответы (1)


Воспроизведение ошибки

Я мог воспроизвести это поведение на каждом Ruby, который пробовал.

# write_sorted_set.rb
require 'set'
class Foo
  include Comparable

  attr_accessor :num

  def initialize(num)
    @num = num
  end

  def <=>(other)
    num <=> other.num
  end
end

f1 = Foo.new(1)
f2 = Foo.new(2)
f3 = Foo.new(3)

s = SortedSet.new([f2, f1, f3])
File.open('set_test.dump', 'wb') { |f| Marshal.dump(s, f) }
p s.to_a

а также

# load_sorted_set.rb
require 'set'
class Foo
  include Comparable

  attr_accessor :num

  def initialize(num)
    @num = num
  end

  def <=>(other)
    num <=> other.num
  end
end

ls = Marshal.load(File.binread('set_test.dump'))
p ls.to_a

При запуске

ruby write_sorted_set.rb && ruby load_sorted_set.rb

Он выводит

[#<Foo:0x000000010cae30 @num=1>, #<Foo:0x000000010cae08 @num=2>, #<Foo:0x000000010cadb8 @num=3>]
[#<Foo:0x0000000089be08 @num=2>, #<Foo:0x0000000089bd18 @num=1>, #<Foo:0x0000000089bc78 @num=3>]

Почему?

Сопоставимый не используется

Используя это определение:

class Foo
  attr_accessor :num
  def initialize(num)
    @num = num
  end
end

в load_sorted_set.rb должно вызывать исключение (comparison of Foo with Foo failed (ArgumentError)), но это не так. Похоже, что SortedSet неправильно инициализирован Marshal.load

lib/set.rb

Глядя на исходный код для SortedSet :

  module_eval {
    # a hack to shut up warning
    alias old_init initialize
  }

а также

      module_eval {
        # a hack to shut up warning
        remove_method :old_init
      }

      @@setup = true
    end
  end

  def initialize(*args, &block) # :nodoc:
    SortedSet.setup
    initialize(*args, &block)
  end
end

Похоже, что SortedSet был исправлен, чтобы гарантировать, что SortedSet.setup будет выполняться до инициализации любого SortedSet.

Marshal.load похоже об этом не знает.

Решение

SortedSet.setup

Вы можете позвонить

SortedSet.setup

после require 'set' и до Marshal.load

SortedSet.new

Вы можете принудительно инициализировать SortedSet с помощью:

ls = SortedSet.new(Marshal.load(File.binread('set_test.dump')))
person Eric Duminil    schedule 23.01.2017
comment
Хорошие выводы! хотя я не уверен, что понимаю, как этот хак в исходном коде объясняет проблему ... Кроме того, в реальном мире моя проблема более сложна, и это часть иерархии объектов, поэтому предложенное вами решение невозможно. .. Возможно, я переопределю метод загрузки в своем классе в качестве патча. Я надеялся, что есть действительное решение, без хаков или патчей. Возможно, есть способ исправить SortedSet обезьяны? Спасибо! - person Shai Kerer; 24.01.2017
comment
Прохладный. Всегда интересно взглянуть на исходный код Ruby и попытаться понять некоторые его части. - person Eric Duminil; 24.01.2017