こんにちは、hachi8833です。@jnchitoさんの『プロを目指す人のためのruby入門』をKindleで読み返していて、ハッシュにキーがない場合のデフォルト値について今更知ったことをメモします。
Kindleだと、サンプルコードをコピーしたときに書名が入ったりアンダースコア_
の後ろに勝手にスペースが追加されたりするのが少々つらいなー😢と思ったら、サンプルコードがちゃんとありました↓。
- リポジトリ: JunichiIto/ruby-book-codes
以下はRuby 2.5.3で確認しました。なお、私の環境ではpryのプロンプトをカスタマイズして短くしています。
ハッシュの初期値
同書5.6.8「ハッシュの初期値を理解する」によると、RubyではHash.new
でハッシュを作成するときに、ハッシュキーがない場合のデフォルト値を以下のように引数で与えられます。
私はハッシュをリテラルで書くことはあってもHash.new
はしたことがありませんでした。
h = Hash.new("vocal")
#» {}
h[:brian] = "guitar"
#» "guitar"
h[:john] = "bass"
#» "bass"
h[:rodger] = "drums"
#» "drums"
h[:freddie] # 存在しないキー
#» "vocal"
このデフォルト値はハッシュのどこに保存されているのかなと思ったら、#default
アクセサメソッドで取れることがわかりました。#default=
もあるので、後からデフォルト値を設定したり変更したりもできます。
h.default
#» "vocal"
h.default = "piano"
#» "piano"
h[:freddie]
#» "piano"
デフォルト値の破壊的変更には注意
同書で注意されていたのは、この値を破壊的に変更すると、そのデフォルト値を使っているハッシュの値がすべて変わってしまうという点でした。
a = h[:freddie]
#» "piano"
b = h[:rami]
#» "piano"
a.upcase! # 値を破壊的に変更
#» "PIANO"
a
#» "PIANO"
b
#» "PIANO" # bの値まで変わってしまった
Hash.new
しているのでデフォルト値はそのたびに生成されているように錯覚しそうですが、これらのデフォルト値はいずれも同一オブジェクトです。
もちろん、Integer
やSymbol
のように破壊的変更のできないデフォルト値では起きません。
公式情報↓にも注意があることを社内で教えていただきました🙇。
参考: singleton method Hash.new
(Ruby 2.5.0)
デフォルト値として、毎回同一のオブジェクトifnoneを返します。 それにより、一箇所のデフォルト値の変更が他の値のデフォルト値にも影響します。
これを避けるには、破壊的でないメソッドで再代入する必要が有ります。 また、このようなミスを防ぐためにもifnoneはfreeze
して破壊的操作を禁止しておくのが無難です。
docs.ruby-lang.orgより
デフォルト値はブロックでも与えられる
Hash.new("vocal")
のようにデフォルト値を与える他に、ブロックで与えることができます。
h = Hash.new do
"vocal"
end
ブロックを使う場合、キーがなければその都度ブロックからデフォルト値が生成されるので、値としては同じでも異なるオブジェクトになってくれます(つまりobject_id
が異なる)。
これなら、取り出した値を破壊的に変更しても他には影響しません。
a = h[:freddie]
#» "vocal"
b = h[:rami]
#» "vocal"
a.upcase!
#» "VOCAL"
a
#» "VOCAL"
b
#» "vocal"
ブロックにしておけば、より複雑な方法でハッシュにデフォルト値を設定することもできます。
デフォルト値のブロックは後からも設定できる
pryで動かしてみると、Hashクラスにはdefault_proc
とdefault_proc=
というアクセサメソッドもあることにコマンド補完で気づきました。推測どおり、これでProc.new
でブロックを渡せば、Hash.new
しなくてもデフォルト値をブロックで設定できるようになります。
h.default_proc = Proc.new do
"vocal"
end
#» #<Proc:0x00007fc1b39f70c8@(pry):57>
h.default_proc
#» #<Proc:0x00007fc1b39f70c8@(pry):57>
a = h[:freddie]
#» "vocal"
私はHash.new
でデフォルト値ブロックを設定するよりも、default_proc
でやる方が好みです。
proc
やlambdaを使う場合
もちろん、proc
でもやれます。
h.default_proc = proc do
"vocal"
end
ただしlambdaは引数(ここではブロック引数)の数が合わないとエラーになります。
h.default_proc = lambda do
"vocal"
end
TypeError: default_proc takes two arguments (2 for 0)
from (pry):78:in `default_proc='
実はRubyのハッシュは、キーがない場合にブロックに2つの引数(ハッシュそのものとキー)を渡します。以下の例ではlambdaでエラーを出さないためだけに|hash, key|
というブロック引数を置いています。
h.default_proc = lambda do |hash, key|
"vocal"
end
#» #<Proc:0x00007fc1b4ad68a8@(pry):81 (lambda)>
この2つのブロック引数を使えば、たとえばキーがない場合に自動的にキーを追加することもできると同書で説明されていました。この部分は同書でご覧ください。
おまけ: ハッシュでやろうと頑張りすぎない
同書では、Rubyのハッシュをひととおり説明してから次の「第7章: クラス」の作成を理解する」で次のようにハッシュについて注意を喚起するという構成になっているのが素晴らしいと思いました。
- ハッシュはキーがないときに
nil
を返すだけなのでキーの誤りに気づきにくい - ハッシュはキーを追加したり内容を変更したりがなまじ簡単にできるので、コードが壊れやすくなる
- ハッシュが大きくなると管理しきれなくなる
それに、ハッシュで保存してしまうとRubyらしく.メソッド
でアクセスできないのも残念ですね。
はみ出しつっつき
いつもお世話になっているRailsエンジニアのkazzさんと、このハッシュの話をしました。
「デフォルト値の破壊的変更、そういえばそういうのあった!😳ま、自分はHash.new
しないし、デフォルト値でそこまで頑張るならハッシュでやるより専用のクラスを作る方がいいと思うし😆」「😆」
「そうそう、ハッシュで何でもやろうとするのが好きな人、よくいますよね: たいていの場合クラスにする方がいいのに、何でそうしないんだろう?」「クラスを追加することの心の障壁が高いのかもしれないですね🤔」
「普段から言ってるけど、遠慮しないでもっとクラスを作っていいんだよね: 1ファイルあたりのコード量も減るから保守しやすくなるし、イヤなら捨てられるし❤️」「クラスを足したりメソッドを分割したりするより、メソッドを長々と書いてやり過ごせばいいと思い込みそうですね: 実際は逆なのに」
「ただ、素のRubyならクラスを作るでいいけど、これがRailsになると、そのクラスをどこに置く?の方で悩みますけどね😅」「あ、確かにー」
「あと、クラスを作るときの最大の心の障壁は、クラスに適切な名前を付けるのが大変ということだと思ってる」「そっか、そこって一番難儀しそうですね: 赤ちゃんの名前👶やアニメキャラの名前もそうですが、一度付けると後々まで影響しますし」「名前付けは大変だけど設計でとても重要だし、いいクラス名が見つかるととっても爽快😋: みんなも頑張って名前付け名人を目指そう!」「シソーラスが強い味方ですね」
おたより発掘
Ruby: `Hash.new`に渡すデフォルト値の破壊的変更に注意
#プロを目指す人のためのRuby入門 の内容をより深く掘り下げてもらいました。いい考察だと思います!素晴らしい〜👏
ありがとうございます!😂
破壊的変更に注意っていうか『デフォルト値は共通のオブジェクトになること』に注意やね(重箱の隅を突きつつ
— バンビちゃん@実際社会不適合者 (@pink_bangbi) November 29, 2018