Help us understand the problem. What is going on with this article?

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

ゼロからわかる Ruby 超入門 (かんたんIT基礎講座)を参考書にしながら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で書いてるとコードが長くなるし条件の追加とかダルそう。。。でもこれしか思いつかなかった。

# 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

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

# ハッシュを使おう!
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: "トール")とすると、エラーになります。

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

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

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

  • エラーメッセージのとおり、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で変換して+メソッドを使えるようにする。

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な場合に限ります。

ハッシュにデフォルト値を渡す

いくつかやり方がありますがとりあえず1つだけ。下記以外にHash.new()として値を渡すこともできます。

# 元のハッシュ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は、第二引数にデフォルト値を一緒に渡すことができます。以下リファレンスより。

fetch(key, default = nil) {|key| ... } -> object[permalink][rdoc]
key に関連づけられた値を返します。該当するキーが登録されてい ない時には、引数 default が与えられていればその値を、ブロッ クが与えられていればそのブロックを評価した値を返します。

論理演算子で、nil(つまり偽)のときの値を設定しておく

# 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基礎講座)

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away