Rubyのコレクション書き換え時の注意点

これも嵌りがちな内容。
コレクションのコピーについて。

Effective Ruby

Effective Ruby

項目16 コレクションを書き換える前に引数として渡すコレクションのコピーを作っておこう

ラジオのTunerを例に。

class Tuner
  def initialize(presets)
    @presets = presets
    clean
  end

  private
    def clean
      # 末尾が奇数のみを抽出
      @presets.delete_if { |f| f[-1].to_i.even? }
    end
end

>> presets = %w(90.1 106.2 88.5)
>> tuner = Tuner.new(presets)

# 書き換えられちゃった!
>> presets
=> ["90.1", "88.5"]

改善案(reject

Array#delete_if でなく Array#reject を使えば良い。
でも、どっかで書き換えられるかもしれない。

改善案(clone/dup

コピーすれば良い。
cloneはオブジェクトの状態(freezeと特メソッド)を残す。
dupは残さない。
大抵の場合はdupで良い。

class Tuner
  def initialize(presets)
    @presets = presets.dup
    clean
  end
end

コピー時の注意点

dup/cloneはシャロー(shallow)コピーを返す。
コレクションクラスの場合、コンテナのコピーは作られるが、要素のコピーは作られない。

>> a = ["Polar"]
>> b = a.dup << "Bear"
=> ["Polar", "Bear"]

>> b.each { |x| x.sub!("lar", "oh") }
=> ["Pooh", "Bear"]

# 書き換えられてる
>> a
=> ["Pooh"]

deepコピーが欲しい場合

Marashalを使えば手軽にできるが、メモリも食うし、Marshalみ対応していないオブジェクト(IO、Fileなど)もあるので要注意。

>> a = ["Polar"]
>> b = Marshal.load(Marshal.dump(a)) << "Bear"
=> ["Polar", "Bear"]

>> b.each { |x| x.sub!("lar", "oh") }
=> ["Pooh", "Bear"]

# 書き換えられていない!
>> a
=> ["Polar"]

覚えておくべき事項

  • Rubyのメソッド引数は値渡しではなく参照渡しである。ただし、この規則には、Fixnumオブジェクトという顕著な例外がある。
  • 引数として渡されたコレクションは、書き換える前にコピーを作ろう。
  • dup、cloneメソッドは、シャローコピーしか作らない。
  • ほとんどのオブジェクトでは、Marshalを使えば必要な時にディープコピーを作れる。