ハッシュの安全な扱い方 (デフォルト値とその落とし穴について)

# <i class='fa fa-question fa-fw'></i>問題

以下のような初期化済みのハッシュがあります。

```ruby
hash
#=> {:madoka=>"鹿目 まどか"}

hash.class
#=> Hash
```

この時、以下のコードを実行するとどうなるでしょうか？
(`hash` に `:sayaka` というキーは存在していません。)

```ruby
if hash[:sayaka]
  raise('あたしって、ほんとバカ')
end
```

ほとんどの場合、

```ruby
if hash[:sayaka]
  raise('あたしって、ほんとバカ')
end
#=> nil
```

と何も起きないでしょう。
なぜなら、ハッシュ (`Hash#[]` メソッド) は通常、キーに対応する値が存在しない時には `nil` を返すからです。
(そして Ruby は `nil` を偽として扱います。)
ところがどっこい、実は例外が起きる場合もあるのです！

```ruby
if hash[:sayaka]
  raise('あたしって、ほんとバカ')
end
#=> RuntimeError: あたしって、ほんとバカ
```

```
＿人人人人人人人人人人人人人＿
＞　あたしって、ほんとバカ　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y￣
```

あれ？ この場合の `hash[:sayaka]` の返り値は一体どうなってるでしょうか？
このハッシュの中身を調べてみましょう。

```ruby
hash
#=> {:madoka=>"鹿目 まどか"}

hash[:madoka]
#=> "鹿目 まどか"

hash[:sayaka]
#=> "nil かと思った？ 残念！ さやかちゃんでした！"
```

```
＿人人人人人人人人人人人人人人人人人人人人人人人人＿
＞　nil かと思った？ 残念！ さやかちゃんでした！　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y￣
```
<i class='fa fa-4x fa-exclamation'></i><i class='fa fa-4x fa-question'></i>

## <i class='fa fa-lightbulb-o fa-fw'></i>種明かし

実はこのハッシュには **デフォルト値** が設定されていたのでした。
先に述べたように、ハッシュは通常、キーに対応する値が存在しない場合に `nil` を返すのですが、
これを任意の値を返すように変更することができます。
`Hash.new` の引数に任意の値を指定すると、それがそのハッシュのデフォルト値となります。

```ruby
hash = {}
hash[:sayaka]
#=> nil

hash = Hash.new([])
hash[:sayaka]
#=> []

hash = Hash.new('nil かと思った？ 残念！ さやかちゃんでした！')
hash[:sayaka]
#=> "nil かと思った？ 残念！ さやかちゃんでした！"
```

このテクニックは例えば以下のように応用できます。

```rb
# 改良前
def count_by_char(string)
  string.each_char.inject({}) do |hash, char|
    hash[char] ||= 0 # hash[char] を初期化しているこの行を消したい。
    hash[char] += 1
    hash
  end
end

count_by_char('banana') #=> {"b"=>1, "a"=>3, "n"=>2}

# 改良後
def count_by_char(string)
  string.each_char.inject(Hash.new(0)) do |hash, char|
    # hash[char] にはデフォルト値 0 が設定されているので、初期化は不要になった。
    hash[char] += 1
    hash
  end
end

count_by_char('banana') #=> {"b"=>1, "a"=>3, "n"=>2}
```

デフォルト値便利！ ✌('ω'✌ )三✌('ω')✌三( ✌'ω')✌ 

しかし、ここで懸念すべきなのは、
**キーに対応する値が存在しない場合に返されるのは、必ずしも `nil` (偽) というわけではない**
ということです。
つまり、よく見かける

```ruby
if hash[:sayaka]
  # do_something
end
```

という書き方は安全ではない (必ずしも期待通りに動くとは限らない) のです。

# <i class='fa fa-wrench fa-fw'></i>安全な扱い方

ではデフォルト値が変更されていることを想定してハッシュをより安全に利用するにはどうすればいいのでしょうか？
これには2つのアプローチがあります。

## (1) キーや値の存在チェックを工夫する

文脈に応じて適切な存在チェックを行いましょう。

```ruby
if hash[:sayaka]
  # do_something
end
```

この if 文が「ハッシュに `:sayaka` というキーが存在している場合」という意図であれば、

```ruby
if hash.has_key?(:sayaka)
  # do_something
end
```

と書くのが適切でしょう。

もしくは「`hash[:sayaka]` の値が `nil` ではない場合」という意図であれば、

```ruby
unless hash[:sayaka].nil?
  # do_something
end
```

と書くべきでしょう。
この場合は、ハッシュのデフォルト値が `nil` (もしくは `false`) ではない時はもちろん unless 文の中身が実行されてしまいますが
`if hash[:sayaka]` と書くよりもコードの意図が明らかになると思います。

## (2) Hash#fetch を利用してデフォルト値を設定する

先に述べたように `Hash.new` を利用してデフォルト値を設定する方法では、
思いがけない箇所に副作用を及ぼしてしまう危険性があります。

そこで、より安全にハッシュのデフォルト値を設定するには `Hash#fetch` を使うという手があります。
`Hash#fetch` は `Hash#[]` と同じような機能を持つメソッドですが、ハッシュに対応するキーが存在しない場合の挙動が異なります。

```ruby
hash = { madoka: '鹿目 まどか' }
#=> {:madoka=>"鹿目 まどか"}

hash[:madoka]
#=> "鹿目 まどか"

hash[:sayaka]
#=> nil

hash.fetch(:madoka)
#=> "鹿目 まどか"

hash.fetch(:sayaka)
#=> KeyError: key not found: :sayaka
```

このように `Hash#fetch` の第一引数として存在しないキーを与えると `nil` を返す代わりに `KeyError` が発生します。

そして `Hash#fetch` に第二引数を渡すとさらに挙動が変わります。

```ruby
hash.fetch(:sayaka)
#=> KeyError: key not found: :sayaka

hash.fetch(:sayaka, '奇跡も、魔法も、あるんだよ')
#=> "奇跡も、魔法も、あるんだよ"
```

そう、第二引数にデフォルト値を指定できるのです。
このように `Hash#fetch` を利用すれば、`Hash.new` を使うことなく一時的なデフォルト値を設定することが可能です。

# <i class='fa fa-thumbs-o-up fa-fw'></i>まとめ

1. ハッシュのキーや値の存在チェックには `Hash#has_key?` や `Object#nil?` などを利用する。
2. ハッシュにデフォルト値を設定したいときは `Hash#fetch` の利用を検討する。

ハッシュ (Hash) は Ruby を扱う上で欠かすことのできない存在です。
とても便利でよくお世話になるクラスですが、その反面、思わぬ落とし穴もあります。
より安全な扱いを心がけたいですね。

# <i class='fa fa-book fa-fw'></i>参考

## 書籍

* [Amazon.co.jp: Effective Ruby](http://www.amazon.co.jp/Effective-Ruby-Peter-J-Jones/dp/4798139823/)
  * **項目20** で言及されています。

## リファレンス

* [singleton method Hash.new](http://docs.ruby-lang.org/ja/2.2.0/method/Hash/s/new.html)
* [instance method Hash#default](http://docs.ruby-lang.org/ja/2.2.0/method/Hash/i/default.html)
* [instance method Hash#fetch](http://docs.ruby-lang.org/ja/2.2.0/method/Hash/i/fetch.html)