『Effective Ruby』を読んだのでまとめました - 第3章 コレクション


関連記事
- 『Effective Ruby』を読んだのでまとめました - 第1章 Rubyに体を慣らす -
- 『Effective Ruby』を読んだのでまとめました - 第2章 クラス、オブジェクト、モジュール - 前編
- 『Effective Ruby』を読んだのでまとめました - 第2章 クラス、オブジェクト、モジュール - 後編
- 『Effective Ruby』を読んだのでまとめました - 第3章 コレクション 現在の記事

『Effective Ruby』第3章のまとめ。今回はコレクションです!

最近やりたい事が増えて来て、フロントエンドやAIなど、色々なものに手を出しているのですが、しっかりRubyの記事も書いて行きたい。

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

Rubyのメソッド引数は値渡しではなく参照渡しです。(*注 Fixnumは例外で、値渡し)
そのため、メソッド内で引数を破壊的に変更すると参照先の値を変えてしまう問題があリます。

メソッド引数の参照先の値を変えてしまう問題の例

果物の名前を入れた配列を変数@fruite_beforeに入れ、そこからorangeを削除した配列を@fruite_afterに入れるとします。
下記のコードの場合、@fruite_before@fruite_afterはどうなるでしょうか。

class Fruits
  def initialize(fruits_array)
    @fruits_before = fruits_array
    @fruits_after  = delete_orange
  end

  def delete_orange
    # 3.ここが肝心。渡された引数はfruits_beforeのコピーではなく、参照。
    @fruits_before.delete_if { |item| item == 'orange' }
  end

  def check_fruits
    p @fruits_before
    p @fruits_after
  end
end

Fruits.new(%w[apple orange banana]).check_fruits

実行結果

["apple", "banana"] # @fruits_beforeからもorangeが削除されてしまった!
["apple", "banana"]

このように@fruits_afterのみorangeが削除されてると思ったら@fruits_beforeからも削除されてたというバグを作ってしまいました。

もちろん、この例くらいのコード量であれば気づくかもしれませんが、コード量が増えて来ると話は別です。
「オブジェクト間の複雑なやり取りのために気づかれず、テストされないことが多い。」と本文にも書いてありました。

また、「うっかりinitializeに渡された引数をインスタンス変数にしまい込むと、あとでそのインスタンス変数を書き換えたときにトラブルに見舞われる。」とも書いてあるので、引数を書き換える可能性がある場合はコピーを作るべきです。

dupを使用して引数を書き換える前にコピーを作ろう

Rubyにはオブジェクトのコピーを作るメソッドとしてclonedupがあります。
cloneとdupについて

2つの違いについてですが、cloneはフリーズ状態をコピーするのに対し、dupはしません。そのため、内容を書き換える前提でコピーを作るのであればdupを使う方が良いでしょう。

deep copyしたい場合はMarshalを使おう

オブジェクトのコピーを作るメソッドとしてcloneとdupの説明をしましたが、cloneとdupはshallow copy(浅いコピー)を返す事に注意しましょう。
[ruby]浅いコピーと深いコピー

簡単にいうとcloneとdupはオブジェクトのコピーを作りますが、要素のコピーは作りません。
配列等のオブジェクトをコピーする場合、deep copyしなければならないのですが、Effective RubyではMarshalを使う方法を紹介しています。
Marshalについて

項目17 nil、スカラーオブジェクトを配列に変換するには、Arrayメソッドを使おう

Kernalモジュールには「Array」という先頭が大文字の普通ではない名前を持つメソッドがあります。
このArrayメソッドは、引数がto_aryかto_aに応答する場合にはそのメソッドが呼び出せれ、どちらも応答しない場合は新しい配列で引数で包んで返しますので便利です。
ただしHashを渡すとHashの要素を配列に変換し、さらにそれらをネストした配列を作ってしまうので、この方法は避けましょう。

項目18 要素が含まれているかどうかの処理を効率よく行うために集合を使うことを検討しよう

配列でinclude?を使用して含まれている要素をチェックするケースは、配列が大きくなるとパフォーマンス上の問題となります。
全てのコレクションクラスの中で、Arrayのinclude?メソッドは時間計算量O(n)でもっともパフォーマンスが低いので注意が必要です。

配列のパフォーマンスが気になる場合にハッシュに置き換える事を思いつくかもしれませんが、その時、Setを使う事を検討しましょう。配列をハッシュに変更するコードがシンプルに実装出来ます。
内部実装としてはSetクラスはHashに要素を格納するため、重複要素は失われます。また、Set::newに渡すオブジェクトはハッシュキーを持っている必要もあります。

項目19 reduceを使ってコレクションを畳み込む方法を身に付けよう

reduceはinjectの別名です。
著者はreduceもinjectという呼び方も好きではないようですが、どちらかというとreduceを選びたいらしい。

collection = 1..100
collection.inject(0) {|sum, i| sum + i }

ソースコレクションが空ならreduceは意味のある値ではなくnilを返してしまう。初期値を与えておけば防げる話しなので、常にアキュムレータとして常に初期値を指定して、nilを返さないようにした方が良い。

reduceのブロックは、新しいアキュムレータさえ返していれば、与えられたアキュムレータと要素に何でも自由に操作を加えられる。

項目20 ハッシュのデフォルト値を利用する事を検討しよう

存在しないキーを使ってハッシュから値を取り出そうとするとnilが返されます。
nilを返す事を防ぐためにnilガードをバラまいてしまっているかもしれませんが、ハッシュのデフォルト値を利用する事を検討しましょう。
ハッシュのデフォルト値を使う事で、存在しないキーを渡した時にデフォルトのキーを返してくれます。

Hash.new(0) # ハッシュを作る時に引数としてデフォルト値を渡す

存在しないキーにアクセスしたらnilが返される事を前提にコードを書いてはならない。has_key?で使用可能なキーか確認するクセをつけるようにしましょう。

項目21 コレクションクラスからの継承よりも委譲を使うようにしよう

中級者のRubyプログラマは、コレクションクラスのサブクラスを作りたがる傾向があると筆者は感じているそうです。
コレクションクラスのサブクラスを作る時、is-a関係であれば継承向きなのですが、本当は継承階層に無理やり押し込んだhas-a関係である事が多いようです。

自分で作ったサブクラスを使う他のプログラマは、スーパークラスメソッドがサブクラスでも正しく動作するだろうと考えています。
そのため委譲を使って細かく制御してクラスを作るべきです。

筆者は委譲に関して、継承したがいらない部品を取り除くundef_methodoのようなものではなく、外部の部品からクラスを組み立てていくことだと考えているそうです。