Ruby: `Hash.new`に渡すデフォルト値の破壊的変更に注意

こんにちは、hachi8833です。@jnchitoさんの『プロを目指す人のためのruby入門』をKindleで読み返していて、ハッシュにキーがない場合のデフォルト値について今更知ったことをメモします。

Kindleだと、サンプルコードをコピーしたときに書名が入ったりアンダースコア_の後ろに勝手にスペースが追加されたりするのが少々つらいなー😢と思ったら、サンプルコードがちゃんとありました↓。

以下は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しているのでデフォルト値はそのたびに生成されているように錯覚しそうですが、これらのデフォルト値はいずれも同一オブジェクトです。

もちろん、IntegerSymbolのように破壊的変更のできないデフォルト値では起きません。

公式情報↓にも注意があることを社内で教えていただきました🙇。

参考: 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_procdefault_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入門 の内容をより深く掘り下げてもらいました。いい考察だと思います!素晴らしい〜👏

2018/11/29 21:13

ありがとうございます!😂

関連記事

Ruby: `each`よりも`map`などのコレクションを積極的に使おう(社内勉強会)

Ruby: ブロック変数の「シャドウイング」はシャドウイングなのかが気になって調べた(社内勉強会)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

BPSアドベントカレンダー

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ