Ruby
オブジェクト指向
RubyOnRails

Rubyの破壊的メソッドについて

たのしいRubyの破壊的メソッドの説明にある、「レシーバにあたるオブジェクトの値そのものを変更してしまう」について、詳しく動きを追ってみました。

紹介する内容は以下の通りです。

  • 破壊的メソッドとは
  • 実行時のオブジェクトの動き
  • 配列やハッシュで使用する場合の注意点
  • 破壊的メソッドを手作りする

なお、実行環境は ruby 2.4.1p111 です。

破壊的メソッドとは

前述したとおり、レシーバにあたるオブジェクトの値そのものを変更してしまうメソッドのことです。

本投稿では、文字列置換の gsubgsub!(破壊的メソッド) を例に実際の動きを見ていきます。

破壊的メソッドの実行例(String の場合)

gsub または gsub! を使用して、"hogehoge" という文字列を "higehige" に置換します。

以下の 2 つの Ruby のプログラムを実行すると、どちらも str1 は "higehige" と出力されます。

gsub.rb
str1 = "hogehoge"
str2 = str1
str1 = str1.gsub("hoge", "hige")

puts str1 # => higehige
puts str2 # => hogehoge
gsub!.rb
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 を付与して、前述のプログラムにおけるstr1str2 のオブジェクトとしての動きを確認してみます。

代入時

str1 = "hogehoge"
str2 = str1

puts "#{str1} at #{str1.object_id}" # => hogehoge at 23001360
puts "#{str2} at #{str2.object_id}" # => hogehoge at 23001360

代入時は str1str2 が同じオブジェクトを見ていることを確認できました。

代入時を動きを図示すると以下の通りです。
str1str2 も "hogehoge" という値をもつ「23001360」番のオブジェクトを見ています。

破壊的メソッドの挙動_代入時.png

gsub 置換時

次に代入時のプログラムに続けて gsub で置換した場合の動きを見てみます。

gsub.rb
# (略)
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.png

gsub! 置換時

では代入時のプログラムに続けて gsub! で置換した場合の動きを見てみます。

gsub!.rb
# (略)
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" になります。

破壊的メソッドの挙動_gsub!.png

これより、破壊的メソッドを使用すると、「(見るオブジェクトは変わらないまま、) オブジェクトの値そのものを変更する」と分かりました。

まとめ

ポイントをまとめると以下の通りです。

  • 非破壊的メソッドでは、新しいオブジェクトが生成されて値が変更される
  • 破壊的メソッドでは、オブジェクトがもつ値そのものが変更される

配列やハッシュの場合の注意点

注意点

配列やハッシュの場合は、非破壊的メソッド使用時でも、以下例のように破壊的メソッド適用時と同様の動きになってしまう点で注意が必要です。

gsub_list.rb
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" と出力されてしまいます。

これは、個々の格納値を置換しても新しい配列オブジェクトは作られず、置換後も list1list2 は同じオブジェクトを見ているためです。

どうすれば良いか

各オブジェクトの変更影響を受けないようにするためには、以下のように list1gsub で置換する際に新しい配列として代入する必要があります。

gsub_list.rb
# (略)
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" のままです。

参考