【Ruby超入門】Hashに存在しないキーを指定したらnilが返ってきたので、もうちょっと調べた

[ゼロからわかる Ruby 超入門 (かんたんIT基礎講座)](https://www.amazon.co.jp/%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E3%82%8F%E3%81%8B%E3%82%8B-Ruby-%E8%B6%85%E5%85%A5%E9%96%80-%E3%81%8B%E3%82%93%E3%81%9F%E3%82%93IT%E5%9F%BA%E7%A4%8E%E8%AC%9B%E5%BA%A7-%E4%BA%94%E5%8D%81%E5%B5%90/dp/4297101238/ref=asc_df_4297101238/?tag=jpgo-22&linkCode=df0&hvadid=295686767484&hvpos=1o1&hvnetw=g&hvrand=13775278072923236435&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=1009717&hvtargid=pla-541272432931&psc=1&th=1&psc=1)を参考書にしながらRubyを勉強していて学んだことを書いています。

##ハッシュを使ってifを書き換える
ペアプロでやっていて色々教えていただいたのでシェアします！メソッドについてまとめた7章の章末問題7-4の問6。

> 品物の値段を表すpriceメソッドを定義し、キーワード引数でitemを渡す。
> itemがコーヒーの時は300を、カフェラテの時は400を戻り値として返す。

という処理を書く問題。何の気なしにこう書きました

```
def price(item:)
  if item == "コーヒー"
    return 300
  elsif item == "カフェラテ"
    return 400
  end
end
puts price(item: "コーヒー")  #=> 300
puts price(item: "カフェラテ")  #=> 400
```

でも次の問7で、さらにキーワード引数としてsizeがでてきます。
> sizeによって価格を上乗せしていく
> sizeがショートの時は+0円、トールの時+50円、ベンティの時+100円

以上の条件が新たに加わります。
こうなってくると、ifとか、case & whenで書いてるとコードが長くなるし条件の追加とかダルそう。。。でもこれしか思いつかなかった。

```ruby

# caseで書いた例
def price(item:, size:)
  case item
  when "コーヒー"
    price = 300
  when "カフェラテ"
    price = 400
  end
  case size
  when "ショート"
    price += 0
  when "トール"
    price += 50
  when "ベンティ"
    price += 100
  end
end

puts price(item: "コーヒー", size: "ベンティ")  #=> 400
```

解答例を見てみると、メソッドの中で条件分岐させるのではなく、ハッシュを使ってスッキリ書いています。

```ruby
# ハッシュを使おう！
def price(item:, size: "ショート")
  items = {"コーヒー" => 300, "カフェラテ" => 400}
  sizes = {"ショート" => 0, "トール" => 50, "ベンティ" => 100}
  items[item] + sizes[size]
end

puts price(item: "コーヒー", size: "トール")  #=> 350
```

この書き方の良いところは、priceメソッドの中では決まった入力に決まった値を返す、という処理だけが与えられているところです。if文などで条件分岐させるよりも拡張性が高く、コードの見通しも良いです。

##存在しないキーが引数として渡されると…
スッキリ書けて満足なのですが、実はまだ足りません。先ほどのコードだと例えば最後の行を、`puts price(item: "ミルクティー", size: "トール")`とすると、エラーになります。

```text
# errorメッセージ
`price': undefined method `+' for nil:NilClass (NoMethodError)`
```

何が起こっているかというと、

* ミルクティーという存在しないキーを引数に指定しているために、`items[item] + sizes[size]`の中身が、`nil + ５０`になっている

* エラーメッセージのとおり、nilの所属するNilClassには'+'メソッドは定義されていないのでエラーですと怒られる


という流れです。 個人的には`+メソッドが定義されている`というのが面白く感じました。NilClassについて調べてみることにします。

##NilClassについて調べてみた
```
# クラスを調べてみる
irb(main):004:0> nil.class
=> NilClass
irb(main):005:0> 1.class
=> Integer
irb(main):006:0> "1".class
=> String
```
nilはNilClass、面白い。+メソッドを使えるか調べてみます

```
# +メソッドが存在するか調べる
irb(main):015:0> nil.respond_to?(:+)
=> false
irb(main):016:0> 1.respond_to?(:+)
=> true
irb(main):017:0> "1".respond_to?(:+)
=> true
```
IntegerやStringでは+使えるけど、NilClassでは+は使えない。NilClassの全部のメソッドをみてみます。たしかに :+ は存在しない。

```
irb(main):019:0> nil.methods
=> [:&, :inspect, :to_a, :to_s, :===, :to_f, :to_i, :=~, :to_h, :nil?, :to_r,
 :rationalize, :|, :to_c, :^, :instance_variable_defined?, :remove_instance_variable,
 :instance_of?, :kind_of?, :is_a?, :tap, :instance_variable_set, :protected_methods,
 :instance_variables, :instance_variable_get, :public_methods, :private_methods,
 :method, :public_method, :public_send, :singleton_method, :define_singleton_method,
 :extend, :to_enum, :enum_for, :<=>, :!~, :eql?, :respond_to?, :freeze, :object_id,
 :send, :display, :hash, :class, :singleton_class, :clone, :dup, :itself, :yield_self,
 :then, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :frozen?, :methods,
 :singleton_methods, :equal?, :!, :==, :instance_exec, :!=, :instance_eval, :__id__,
 :__send__]
```

##存在しないキーが渡された時の処理を考える
ここから本題。存在しないキーが指定された時の処理方法をいくつか考えます。

###nilを.to_iで変換して＋メソッドを使えるようにする。

```ruby

def price(item:, size: "ショート")
  items = {"コーヒー" => 300, "カフェラテ" => 400}
  sizes = {"ショート" => 0, "トール" => 50, "ベンティ" => 100}
  items[item].to_i + sizes[size]  #　中身は 0 + 50
end

puts price(item: "ミルクティー", size: "トール")  #=> 50
```
ただし.to_iで変換するとnilは0になるので、0として処理してOKな場合に限ります。


###ハッシュにデフォルト値を渡す
いくつかやり方がありますがとりあえず１つだけ。下記以外にHash.new()として値を渡すこともできます。

```ruby
# 元のハッシュitemsに.fetchを使って、デフォルトの値を渡しておく
def price(item:, size: "ショート")
  items = {"コーヒー" => 300, "カフェラテ" => 400}
  sizes = {"ショート" => 0, "トール" => 50, "ベンティ" => 100}
  items.fetch(item, 350) + sizes[size]
end

puts price(item: "ミルクティー", size: "トール")  #=> 400
```
.fetchは、第二引数にデフォルト値を一緒に渡すことができます。以下[リファレンス](https://docs.ruby-lang.org/ja/latest/class/Hash.html#I_FETCH)より。
>fetch(key, default = nil) {|key| ... } -> object[permalink][rdoc]
key に関連づけられた値を返します。該当するキーが登録されてい ない時には、引数 default が与えられていればその値を、ブロッ クが与えられていればそのブロックを評価した値を返します。


###論理演算子で、nil(つまり偽)のときの値を設定しておく
```ruby
# items[item]がnil、つまり条件が偽のときの値を用意して渡す

def price(item:, size: "ショート")
  items = {"コーヒー" => 300, "カフェラテ" => 400}
  sizes = {"ショート" => 0, "トール" => 50, "ベンティ" => 100}
  (items[item] || 350) + sizes[size]
end

puts price(item: "ミルクティー", size: "トール")  #=> 400
```
こんな感じで掘り下げていくと、とても面白いです。

####参考図書
[ゼロからわかる Ruby 超入門 (かんたんIT基礎講座)](https://www.amazon.co.jp/%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E3%82%8F%E3%81%8B%E3%82%8B-Ruby-%E8%B6%85%E5%85%A5%E9%96%80-%E3%81%8B%E3%82%93%E3%81%9F%E3%82%93IT%E5%9F%BA%E7%A4%8E%E8%AC%9B%E5%BA%A7-%E4%BA%94%E5%8D%81%E5%B5%90/dp/4297101238/ref=asc_df_4297101238/?tag=jpgo-22&linkCode=df0&hvadid=295686767484&hvpos=1o1&hvnetw=g&hvrand=13775278072923236435&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=1009717&hvtargid=pla-541272432931&psc=1&th=1&psc=1)