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

[たのしいRuby](https://www.amazon.co.jp/dp/B01C804DO8/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)の破壊的メソッドの説明にある、「レシーバにあたるオブジェクトの値そのものを変更してしまう」について、詳しく動きを追ってみました。

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

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

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

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

本投稿では、文字列置換の [gsub](https://ref.xaio.jp/ruby/classes/string/gsub#h-2133670205) と [gsub!(破壊的メソッド)](https://ref.xaio.jp/ruby/classes/string/gsub#h-1020884426) を例に実際の動きを見ていきます。


## 破壊的メソッドの実行例(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``` を付与して、前述のプログラムにおける```str1``` と ```str2``` のオブジェクトとしての動きを確認してみます。

### 代入時
```Ruby
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」番のオブジェクトを見ています。

![破壊的メソッドの挙動_代入時.png](https://qiita-image-store.s3.amazonaws.com/0/147552/a84ed353-3240-a225-9aa6-af03c8c61574.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](https://qiita-image-store.s3.amazonaws.com/0/147552/d3d60a74-2895-8983-548e-4b82a7cd1525.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](https://qiita-image-store.s3.amazonaws.com/0/147552/a146a436-b468-bc65-26b7-7ddc80a7c0cc.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" と出力されてしまいます。

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

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

```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``` を使用した場合です。

```rb
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``` を使用しないバージョンを作ってみました。

```rb
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" のままです。


## 参考

* [gsub, gsub\! \(String\) \- Rubyリファレンス](https://ref.xaio.jp/ruby/classes/string/gsub#h-2133670205)

* [Ruby での破壊的メソッドの作り方 \- Camera Obscura](http://obelisk.hatenablog.com/entry/2015/06/01/205533)

* [Rubyの破壊的メソッドと参照の値渡し \- Qiita](http://qiita.com/metheglin/items/ca452bc0a3866d2de7bf)

* [Rubyist Magazine \- 値渡しと参照渡しの違いを理解する](http://magazine.rubyist.net/?0032-CallByValueAndCallByReference)