chibaqn /
Posts
2022-11-11 20:01:17

オブジェクトのコピーについて

はじめに

Ruby にはオブジェクトをコピーするメソッドとして Object#cloneObject#dup が存在する。

これらの違いとコピーの挙動(浅いコピー)について理解していないと、プログラムに予期せぬ影響が出てしまう(実際に、オブジェクトのコピーが原因で障害につながったケースが職場であった)。

この記事では Ruby におけるオブジェクトのコピーについて調べたことをまとめる。

※ 以前調査した内容をもとに内容を最新にアップデートする。

clone と dup の比較

共通点

clone と dup は、レシーバのオブジェクトのコピーを作成して返す。

オブジェクトのコピーとは、「元のオブジェクトと同じクラスの新しいオブジェクトで、元のオブジェクトのインスタンス変数を新しいオブジェクトにコピーしたもの」である。

内部の挙動としては、インスタンス変数のコピーを行ったあと、initialize_copy メソッドを呼び出している。

※ どちらも「浅いコピー」を作ることに注意。

違い

元のオブジェクトからコピーするものの違いがある。

※ ◎ がコピーする。× がコピーしない。

凍結状態 汚染状態 信頼状態 特異メソッド
clone
dup × ×

汚染状態と信頼状態について

関連するメソッドは Ruby 3.2 で廃止済みである。

プロと読み解く Ruby 3.2 NEWS - クックパッド開発者ブログ

Rubyにはオブジェクトを汚染する仕組みがあった

汚染状態

「信用できない入力をもとに作られたオブジェクトを安全に操作すること」と「信用しているオブジェクト(汚染されていないオブジェクト)を信用できないプログラムから守ること」を目的とした仕組み。

オブジェクト自体に汚染フラグを持たせて、汚染されているかどうかを判別する。

セキュリティモデル - Ruby 3.2 リファレンスマニュアル

信頼状態

汚染状態と同じような仕組み。

オブジェクトに「untrust マーク(信頼できない状態であることを示すフラグ)」を持たせて、そのオブジェクトが信頼出来る状態かどうかを判別する。

Object#trust - Ruby 1.9.3 リファレンスマニュアル

浅いコピー(シャローコピー)について

変数が持つのはオブジェクトへの参照

Ruby における値は全てオブジェクトなので、実際には変数が持つのはオブジェクトへの参照である。

よって、メソッドの引数は、変数の値ではなく「オブジェクトへの参照」を渡されているといえる。

コレクションをコピーしたときの注意点

Object#cloneObject#dup はオブジェクトを複製する(オブジェクト自体は別物なので、object_id は異なる)。

しかし、そのオブジェクトが持っている他のオブジェクトへの参照情報はそのままコピーされる(インスタンス変数の参照情報はそのままコピーされるということ。つまり、複製したオブジェクトでもインスタンス変数の object_id は同一)。

つまり、オブジェクト自体は別物でも、そのオブジェクトがもっている他のオブジェクトへの参照情報は変わらない。

よって、コレクションオブジェクト(Array, Hash など)などでは、コピーの要素(String オブジェクトなど)を書き換えると、オリジナルの要素も書き換わってしまう。

a = ["ruby", "java", "python"]
# => ["ruby", "java", "python"]
b = a.dup
# => ["ruby", "java", "python"]
a.object_id
# => 47001770547140
b.object_id
# => 47001775682440

#############################################################################
# 複製したオブジェクト自体を変更
# => オリジナルに影響なし
#############################################################################
b.push("Go")
# => ["ruby", "java", "python", "Go"]
p a
# => ["ruby", "java", "python"]

#############################################################################
# 複製したオブジェクトの要素を変更
# => オリジナルに影響あり
# ※ 要素である Stringオブジェクトへの参照はそのままコピーされているから!
#############################################################################
b[0].capitalize!
p b
# => ["Ruby", "java", "python"]
p a
# => ["Ruby", "java", "python"]

メソッドの引数は参照渡し?値渡し?

Ruby は 値渡し である(変数の値をコピーして渡す)。

値渡しでは変数に格納されている値がコピーされて渡されるが、その値が「参照」を表している場合がある。これを「参照の値渡し」という。

よって、メソッド内で参照情報が示すオブジェクトに変更を加えた場合、呼び出し側の変数(実引数)に影響が出る。

引数でコレクションオブジェクトが渡された場合

コレクションオブジェクトのコピーが渡されるが、その際コレクションオブジェクトが持っている「他のオブジェクトへの参照情報(要素のオブジェクトへの参照)」もコピーされる。

従って、メソッド内でコレクションオブジェクトの要素に変更を加えた(== 参照情報が示すオブジェクトに変更を加えた)場合、呼び出し側の変数にも影響が出る。

def hoge(arr)
  arr[0].capitalize!
end

arr = ["ruby", "java", "python"]
hoge(arr)
p arr
=> ["Ruby", "java", "python"]

おわりに

浅いコピーに関する挙動は Ruby 初学者にとっては落とし穴である。他の言語を学ぶ際もコピーの挙動には注意したい。

※ 以前調査したときから時間が経っていたので、汚染状態や信頼状態に関するメソッドが廃止されていた。まとめ直して良かった。

参考資料