代入メソッドの罠

Ruby ではメソッド名の末尾に = が付いていると、属性への代入のように使える。

def Foo
  def name=(name)
    @name = name
  end
end

foo = Foo.new       # => #<Foo:0x100c7f98 @name="">
foo.name = 'foo'    # => "foo"  ←ここに注目
foo                 # => #<Foo:0x100c7f98 @name="foo">

つまり代入のように書くと Foo#name=(name) を呼び出してくれる。

しかし、Foo#name=(name) が self ではなく name を返してるっぽいのが罠。*1

foo = Foo.new           # => #<Foo:0x100c6ff4 @name="">
name = foo.name = 'foo' # => "foo"
foo                     # => #<Foo:0x100c6ff4 @name="foo">

name[0] = 'F'
foo                     # => #<Foo:0x100c6ff4 @name="Foo">

foo の属性を外から変更できてしまう。

じゃあ、ちょっと不便になるけど Foo#name=(name) で self を返してやれば いいんじゃね?

class Foo
  def name=(name)
    @name=name
    self
  end
end

foo = Foo.new       # => #<Foo:0x100de604 @name="">
foo.name = 'foo'    # => "foo"  ← なにそれこわい
foo                 # => #<Foo:0x100de604 @name="foo">

代入式の値は左辺値ではなく右辺値らしい。*2

どうするか?

class Foo
  def name=(name)
    @name = Marshal.load(Marshal.dump(name))
  end
end

foo = Foo.new           # => #<Foo:0x100e10c8 @name="">
name = foo.name = 'foo' # => "foo"
foo                     # => #<Foo:0x100e10c8 @name="foo">

name[0] = 'F'
name                    # => "Foo"
foo                     # => #<Foo:0x100e10c8 @name="foo">

浅いコピーではなく深いコピーをすることにした。*3

*1:普通はこれで困らないし、この方が便利。

*2:foo.__send__(:name=, 'foo') すると self が返る。

*3:Hash もキーが文字列のときは深いコピーをしてるらしい。