to_s が String を返さなかったら?

puts すると String じゃないと to_s が呼ばれるが、to_s が String を返さなかったらどうなるか?さらに to_s が呼ばれて無限ループと言うことは無いだろうけど。

class Foo
  def to_s
    self
  end
end

puts Foo.new #=> #<Foo:0x0000000dcc2858>

Foo#to_s が String じゃないと Object#to_s が呼ばれているのか?

class Object
  def to_s
    puts "call Object#to_s"
    nil
  end
end
class Foo
  def to_s
    puts "call Foo#to_s"
    self
  end
end

puts Foo.new #=> call Foo#to_s
                 #<Foo:0x0000000ac84948>

どうもそういうわけでもない。ということで、ソースを見てみる。まずは、IO#write を見る。

static VALUE
io_write(VALUE io, VALUE str, int nosync)
{
(中略)
    str = rb_obj_as_string(str);
(中略)

どうも、ここで String に変換しているようだ。

VALUE
rb_obj_as_string(VALUE obj)
{
    VALUE str;

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

String かどうか調べて、String じゃないとメソッドの to_s を呼んで、その結果が String かどうかまた調べて、String じゃないとこんどはCの関数 rb_any_to_s() を呼んでいる。

VALUE
rb_any_to_s(VALUE obj)
{
    const char *cname = rb_obj_classname(obj);
    VALUE str;

    str = rb_sprintf("#<%s:%p>", cname, (void*)obj);
    OBJ_INFECT(str, obj);

    return str;
}

と、ここで有無を言わせず文字列に変換している。