Что происходит, когда вы используете строчную интерполяцию в рубине?

Я думал, что Ruby просто вызовет метод to_s, но я не могу объяснить, как это работает:

class Fake
  def to_s
    self
  end
end

"#{Fake.new}"

По логике это должно повысить уровень стека слишком глубоко из-за бесконечности рекурсии. Но он отлично работает и, кажется, вызывает #to_s из Object.

=> "#<Fake:0x137029f8>"

Но почему?

ДОБАВЛЕНО:

class Fake
  def to_s
    Fake2.new
  end
end

class Fake2
  def to_s
    "Fake2#to_s"
  end
end

Этот код работает по-разному в двух случаях:

puts "#{Fake.new}" => "#<Fake:0x137d5ac4>"

Но:

puts Fake.new.to_s => "Fake2#to_s"

Я думаю, что это ненормально. Может ли кто-нибудь предложить, когда в рубиновом интерпретаторе это происходит внутри?

Ответ 1

Краткая версия

Ruby вызывает to_s, но проверяет, что to_s возвращает строку. Если это не так, Ruby вызывает стандартную реализацию to_s. Вызов to_s рекурсивно не будет хорошей идеей (без гарантии прекращения) - вы могли бы свернуть VM, а код ruby ​​не смог бы сбой всей виртуальной машины.

Вы получаете отличный результат от Fake.new.to_s, потому что irb вызывает inspect, чтобы отобразить результат для вас, и inspect вызывает to_s второй раз

Длинная версия

Чтобы ответить "что происходит, когда ruby ​​is x", хорошим местом для начала является просмотр того, какие команды генерируются для виртуальной машины (это все зависит от MRI). Для вашего примера:

puts RubyVM::InstructionSequence.compile('"#{Foo.new}"').disasm

выходы

0000 trace            1                                               (   1)
0002 getinlinecache   9, <is:0>
0005 getconstant      :Foo
0007 setinlinecache   <is:0>
0009 opt_send_simple  <callinfo!mid:new, argc:0, ARGS_SKIP>
0011 tostring         
0012 concatstrings    1
0014 leave      

Там некоторые проблемы с кешем, и вы всегда получите trace, leave, но в двух словах это говорит.

  • получить константу Foo
  • вызов его нового метода
  • выполнить инструкцию tostring
  • выполнить команду concatstrings с результатом инструкции tostring (последнее значение в стеке (если вы делаете это с несколькими последовательностями # {}, вы можете увидеть, как он создает все отдельные строки и затем вызывает concatstrings один раз на всех потребляющих все эти строки)

Инструкции в этом дампе определены в insns.def: это сопоставляет эти инструкции с их реализацией. Вы можете видеть, что tostring просто вызывает rb_obj_as_string.

Если вы ищете rb_obj_as_string через ruby ​​codebase (я нахожу http://rxr.whitequark.org полезный для этого), вы можете увидеть, что он определен здесь как

VALUE
rb_obj_as_string(VALUE obj)
{
    VALUE str;

    if (RB_TYPE_P(obj, T_STRING)) {
    return obj;
    }
    str = rb_funcall(obj, id_to_s, 0);
    if (!RB_TYPE_P(str, T_STRING))
    return rb_any_to_s(obj);
    if (OBJ_TAINTED(obj)) OBJ_TAINT(str);
    return str;
}

Вкратце, если у нас уже есть строка, верните ее. Если нет, вызовите метод to_s объекта. Затем (и это важно для вашего вопроса), он проверяет тип результата. Если это не строка, она возвращает rb_any_to_s, а это функция, которая реализует значение по умолчанию to_s