たのしいRubyの破壊的メソッドの説明にある、「レシーバにあたるオブジェクトの値そのものを変更してしまう」について、詳しく動きを追ってみました。
紹介する内容は以下の通りです。
- 破壊的メソッドとは
- 実行時のオブジェクトの動き
- 配列やハッシュで使用する場合の注意点
- 破壊的メソッドを手作りする
なお、実行環境は ruby 2.4.1p111 です。
破壊的メソッドとは
前述したとおり、レシーバにあたるオブジェクトの値そのものを変更してしまうメソッドのことです。
本投稿では、文字列置換の gsub と gsub!(破壊的メソッド) を例に実際の動きを見ていきます。
破壊的メソッドの実行例(String の場合)
gsub
または gsub!
を使用して、"hogehoge" という文字列を "higehige" に置換します。
以下の 2 つの Ruby のプログラムを実行すると、どちらも str1
は "higehige" と出力されます。
str1 = "hogehoge"
str2 = str1
str1 = str1.gsub("hoge", "hige")
puts str1 # => higehige
puts str2 # => hogehoge
str1 = "hogehoge"
str2 = str1
str1 = str1.gsub!("hoge", "hige")
puts str1 # => higehige
puts str2 # => higehige
一方で、str1
を代入しておいた str2
の出力結果は各メソッドで異なります。
gsub
で置換した場合、str2
は代入時の str1
が持っていた値 "hogehoge" を保持しています。
一方で、破壊的メソッドである gsub!
を使用すると、str2
に代入した str1
が現在持つ値 "higehige" が表示されます。
破壊的メソッド使用時のオブジェクトの動き
.object_id
を付与して、前述のプログラムにおけるstr1
と str2
のオブジェクトとしての動きを確認してみます。
代入時
str1 = "hogehoge"
str2 = str1
puts "#{str1} at #{str1.object_id}" # => hogehoge at 23001360
puts "#{str2} at #{str2.object_id}" # => hogehoge at 23001360
代入時は str1
と str2
が同じオブジェクトを見ていることを確認できました。
代入時を動きを図示すると以下の通りです。
str1
も str2
も "hogehoge" という値をもつ「23001360」番のオブジェクトを見ています。
gsub 置換時
次に代入時のプログラムに続けて gsub
で置換した場合の動きを見てみます。
# (略)
str1 = str1.gsub("hoge", "hige")
puts "#{str1} at #{str1.object_id}" # => higehige at 23001080
puts "#{str2} at #{str2.object_id}" # => hogehoge at 23001360
gsub
置換後、str1
が新しいオブジェクトを見ていることが分かります。
一方で、str2
は代入時と同じオブジェクトを見ています。
上記処理を図示すると以下の通りです。
gsub
で置換したことにより、str1
が見るのは "higehige" という値を持つ「23001080」番の 新しいオブジェクト になりました。
gsub! 置換時
では代入時のプログラムに続けて gsub!
で置換した場合の動きを見てみます。
# (略)
str1 = str1.gsub!("hoge", "hige")
puts "#{str1} at #{str1.object_id}" # => higehige at 23001360
puts "#{str2} at #{str2.object_id}" # => higehige at 23001360
先程とは異なり、gsub!
置換後も str1
は置換前と同じオブジェクトを見ていることが分かります。
同様に図示してみましょう。
gsub!
を使用すると、置換前に str1
が見ていた「23001360」番のオブジェクトが持つ値そのものが "higehige" に変更します。
そのため、依然同じオブジェクトを見ている str2
の出力結果も "higehige" になります。
これより、破壊的メソッドを使用すると、「(見るオブジェクトは変わらないまま、) オブジェクトの値そのものを変更する」と分かりました。
まとめ
ポイントをまとめると以下の通りです。
- 非破壊的メソッドでは、新しいオブジェクトが生成されて値が変更される
- 破壊的メソッドでは、オブジェクトがもつ値そのものが変更される
配列やハッシュの場合の注意点
注意点
配列やハッシュの場合は、非破壊的メソッド使用時でも、以下例のように破壊的メソッド適用時と同様の動きになってしまう点で注意が必要です。
list1 = ["hogehoge", "higehige"]
list2 = list1
puts "#{list1} at #{list1.object_id}" # => ["hogehoge", "higehige"] at 23091600
puts "#{list2} at #{list2.object_id}" # => ["hogehoge", "higehige"] at 23091600
list1[0] = list1[0].gsub("hoge", "hige")
puts "#{list1} at #{list1.object_id}" # => ["higehige", "higehige"] at 23091600
puts "#{list2} at #{list2.object_id}" # => ["higehige", "higehige"] at 23091600 ※list2[0] まで置換されてしまう
置換していないはずの list2[0]
も list1[0]
と同様に "higehige" と出力されてしまいます。
これは、個々の格納値を置換しても新しい配列オブジェクトは作られず、置換後も list1
と list2
は同じオブジェクトを見ているためです。
どうすれば良いか
各オブジェクトの変更影響を受けないようにするためには、以下のように list1
を gsub
で置換する際に新しい配列として代入する必要があります。
# (略)
list1 = [list1[0].gsub("hoge", "hige"), "higehige"]
puts "#{list1} at #{list1.object_id}" # => ["higehige", "higehige"] at 23123420 ※ オブジェクト id が変わる
puts "#{list2} at #{list2.object_id}" # => ["hogehoge", "higehige"] at 23091600 ※list2[0]は置換されない
破壊的メソッドを手作りする
インスタンス変数の置換を例に、非破壊的メソッド gsub
を使用してインスタンス変数の持つ値そのものが変更される破壊的メソッドを手作りしてみました。
replace を使用
オブジェクトの値自体を変更する破壊的メソッド replace
を使用した場合です。
class Bang
def initialize(str)
@str = str
end
attr_accessor :str
def diy_gsub!(pattern, replace)
@str.replace(@str.gsub(pattern, replace))
end
end
bang = Bang.new("hogehoge")
str1 = bang.str
# 置換前
puts bang.str # => hogehoge
puts str1 # => hogehoge
bang.diy_gsub!("hoge", "hige")
# 置換後
puts bang.str # => higehige
puts str1 # => higehige
実行結果より diy_gsub!
を使用するとインスタンス変数がもつ値が変更されることが確認できました。
str1
も "higehige" と出力されることから、gsub!
の場合と同様、元のオブジェクトが持つ値が変更されていることが分かります。
replace を使用しない
破壊的メソッドに一切頼らずに破壊的メソッドを作れないかと思い、replace
を使用しないバージョンを作ってみました。
class Bang
def initialize(str)
@str = str
end
attr_accessor :str
def diy_gsub!(pattern, replace)
@str = @str.gsub(pattern, replace)
end
end
bang = Bang.new("hogehoge")
str1 = bang.str
# 置換前
puts bang.str # => hogehoge
puts str1 # => hogehoge
bang.diy_gsub!("hoge", "hige")
# 置換後
puts bang.str # => higehige ※インスタンス変数がもつ値が変更される
puts str1 # => hogehoge
実行結果より、インスタンス変数がもつ値が変更されることの確認が取れました。
なお、@str.gsub(pattern, replace)
で生成された新しいオブジェクト**をインスタンス変数 @str
が見ているため、str1
の出力結果は "hogehoge" のままです。