Effective Ruby: 定数がミュータブルなことに注意しよう

良書やなEffective Ruby

項目4 定数がミュータブルなことに注意しよう

定数のfreeze

module Defaults
  NETWORKS = ["192.168.1", "192.168.2"].freeze
end

def host_addresses(host, networks=Defaults::NETWORKS)
  networks.map { |net| net << ".#{host}" }
end

>> host_addresses(1)
=> ["192.168.1.5.1", "192.168.2.5.1"]

>> Defaults::NETWORKS
=> ["192.168.1.1", "192.168.2.1"] # 書き換えられた!

Arrayのfreezeはハマるやつ。
こう書かないとダメ。

module Defaults
  NETWORKS = ["192.168.1", "192.168.2"].map!(&:freeze).freeze
end

>> host_addresses(2)
Traceback (most recent call last):
        5: from /Users/rochefort/.anyenv/envs/rbenv/versions/2.5.0/bin/irb:11:in `<main>'
        4: from (irb):12
        3: from (irb):9:in `host_addresses'
        2: from (irb):9:in `map'
        1: from (irb):9:in `block in host_addresses'
FrozenError (can't modify frozen String)

再代入禁止

以下は、書籍とは異なりますが、こっちの方がわかりやすいと思ったので、Defaults::NETWORKS を例にして見ました。

>> Defaults::NETWORKS
=> ["192.168.1", "192.168.2"]

>> Defaults::NETWORKS = "a"
(irb):32: warning: already initialized constant Defaults::NETWORKS
(irb):3: warning: previous definition of NETWORKS was here
=> "a" # 再代入できた!

というようにwarningは出るが書き換わります。

これを防ぎたければ、module/class自体をfreezeすれば良い。

>> Defaults.freeze
=> Defaults

>> Defaults::NETWORKS
=> ["192.168.1", "192.168.2"]

>> Defaults::NETWORKS = "a"
Traceback (most recent call last):
        2: from /Users/trsw/.anyenv/envs/rbenv/versions/2.5.0/bin/irb:11:in `<main>'
        1: from (irb):39
FrozenError (can't modify frozen Module)

余談

このmodule/classでfreezeさせるのは使ったことなかったけど、これを利用しちゃえば Array#map!(&:freeze).freeze という書き方なんてしなくても済むのか。

module Defaults
  NETWORKS = ["192.168.1", "192.168.2"]
end
Defaults.freeze

def host_addresses(host, networks=Defaults::NETWORKS)
  networks.map { |net| net << ".#{host}" }
end

>> host_addresses(3)
Traceback (most recent call last):
        5: from /Users/rochefort/.anyenv/envs/rbenv/versions/2.5.0/bin/irb:11:in `<main>'
        4: from (irb):58
        3: from (irb):55:in `host_addresses'
        2: from (irb):55:in `map'
        1: from (irb):55:in `block in host_addresses'
FrozenError (can't modify frozen String)

なるほど、これなら定数用のモジュールを用意して、そこでfreezeしちゃえばいいのね。
まぁでも定数って割と限定的なclass内で利用したいケースの方が多い気がするからケースバイケースかなぁ。

覚えておく事項

  • 定数は書き換えられないようにするためにかならずフリーズしよう
  • 定数が配列やハッシュなどのコレクションオブジェクトを参照する場合、コレクションとその要素をフリーズしよう。
  • 既存の定数に新しい値が代入されるのを防ぐためには、定数が定義されているモジュールをフリーズしよう

Effective Ruby

Effective Ruby