4
x = 10      
xx = 10 
1. x == xx                      #true
2. x.eql?xx                     #true
3. x.equal?xx                   #true
4. x.object_id == xx.object_id  #true

Бородатые дядьки объясните почему в строке 3 и 4 возвращается true.

1 Answers1

4

Осторожно: ответ написан, когда актуальным был MRI 2.3.
В 2.4 Fixnum и Bignum сведены в один тип Integer. Но описываемая особенность всё ещё имеет место, просто если раньше числа ей подверженные было легко распознать по классу, теперь это глубоко закопанная деталь реализации.


Потому что (следуя семантике Object#eql? и Object#equal?) в x и xx не просто равное, а одно и то же значение, равны даже ссылки (в Ruby, напоминаю, все значения это ссылки, с парой оговорок, об одной из которых я сейчас и расскажу).

В MRI если посмотреть в object_id (который фактически выводит значение ссылки; самой ссылки, а не значения за ней), можно увидеть, что у равных Fixnum'ов (литерал 10 представляет один из них) одинаковые object_id при любых обстоятельствах: литерал ли это в коде, результат ли вычислений...

Кэш? То есть, MRI кэширует объекты чисел и достаёт их по значению?

Почти. Но не совсем.

Безумные бинарные хаки

"Ссылка" на Fixnum в MRI содержит его значение внутри, прямо в ссылке. Если у большинства объектов object_id это указатель на объект в памяти (или адрес объекта в памяти), то у Fixnum (и у некоторых других классов) эта "ссылка", если интерпретировать её именно как указатель ведёт... куда-то, куда лучше не смотреть. Там может быть что-то интересное, а может быть чужая память. Потому что у Fixnum'ов это не указатель.

object_id у Fixnum это значение числа, сдвинутое на 1 влево, и 1 на младшем бите. Если на время забыть об отрицательных числах, то чтобы получить object_id любого Fixnumа, надо его умножить на 2 и прибавить 1.

(0..100_000).all? { |i| i.object_id == (i * 2 + 1) } # => true

С отрицательными числами всё несколько сложнее, поскольку числа хранятся в дополнительном коде. Нужно пользоваться уже битовой арифметикой:

(-100_000..100_000).all? { |i| i.object_id == (i << 1 | 1) } # => true

Каким образом MRI догадывается, является ли значение указателем на объект в памяти или Fixnumом? Очень просто, по младшему биту (на самом деле не только). В объектом пространстве Ruby не используются нечётные указатели, поскольку все типы имеют размер больше 2 байт.


Чуть более наглядно (и на английском) можно почитать об этом в этой статье, а можно даже покопаться в исходном коде Ruby (вот преобразование из Fixnum в сишный long).

Но я здесь говорю исключительно об MRI. Гарантируется ли такое поведение во всех реализациях? Поскольку спецификации у Ruby, как у языка (а не реализации), нет, то нет.

  • По первой ссылке, где на английском. Почему в первом примере 32.class.name выводит Fixnum, а у меня Integer? Это какое-то нововведение в последних версиях? Старая статья? – smellyshovel May 05 '17 at 16:33
  • @smellyshovel ага, нововведение MRI 2.4. Прям самый что ни на есть свежак. –  May 05 '17 at 16:34
  • О, спасибо за инфу :) – smellyshovel May 05 '17 at 16:35
  • А, я понял ваш сарказм, когда посмотрел на дату публикации :D Давненько за Руби не брался, что уж тут поделаешь – smellyshovel May 05 '17 at 17:00
  • @smellyshovel никакого сарказма. Это правда свежак. И конкретно эта черта может много где сломать совместимость. Не очень получается у MRI жить по семверу. –  May 05 '17 at 17:01
  • Насчет совместимости не совсем понятно почему, судя по соглашению об именовании версий. Все-таки не мажорное обновление (относительно 2.3.x, конечно же) – smellyshovel May 05 '17 at 17:06
  • А, да. Вы именно об этом и сказали. Как обычно. – smellyshovel May 05 '17 at 17:07
  • 1
    @smellyshovel формально совместимость не сломана, 2.4 при использовании Fixnum только сыплет предупреждениями. Но поскольку "хорошие разработчики"™ рассматривают предупреждения как ошибки, это всё равно требует починки в зависящих продуктах. –  May 05 '17 at 17:08