為什麼 Hash 好像有不同的寫法?

Ruby 裡的 Hash 的寫法,是用一個大括號,裡面是一堆 key 跟 value 的配對組合,一個蘿蔔一個坑,就像這樣:

profile = { :name => "見龍", :age => 18, :title => "紅寶石鑑定商" }

在 Ruby 1.9 之後,Ruby 提供了另一款類似 JSON 格式的 Hash 寫法,上面這行可以改寫成這樣:

profile = { name: "見龍", age: 18, title: "紅寶石鑑定商" }

少了 =>,改用 : 並且把 key 前面的冒號也移掉了,看起來好像比較清爽一點。但不管是舊式或是新式的 Hash 寫法,本質上並沒有改變,新式寫法其實只是語法糖衣而已,不信可試著把它印出來看看:

puts profile       # => {:name=>"見龍", :age=>18, :title=>"紅寶石鑑定商"}

會得到跟舊式寫法一樣的結果。 所以,如果想要取得 profile 這個 Hash 的內容,還是得用 Symbol 來存取,使用字串會取得不正確結果:

puts profile[:name]      # => 見龍
puts profile["name"]     # => nil

puts profile[:title]     # => 紅寶石鑑定商
puts profile["title"]    # => nil

但我在寫 Rails 的時候好像都可以耶...

隨便打開一個 Rails 專案的 Controller,你可能會看過像這樣的程式碼:

class ProductionController < ApplicationController
  def show
    @product = Product.find(params[:id])
  end
end

這裡不管你是用 params[:id] 或是 params["id"],都可以拿到你要的結果。

怎麼辦到的? 我們直接在 show action 裡放個中斷點看看,順便來練習怎麼追程式碼:

[9, 19] in /private/tmp/blog/app/controllers/products_controller.rb
    9:
   10:  # GET /products/1
   11:  # GET /products/1.json
   12:  def show
   13:    @product = Product.find(params[:id])
=> 14:    debugger
   15:  end
   16:
   17:  # GET /products/new
   18:  def new
   19:    @product = Product.new

# 先看一下 params 的內容
(byebug) params
<ActionController::Parameters {"controller"=>"products", "action"=>"show", "id"=>"1"} permitted: false>

 # 很好,這個 params 看起來像是一個 Hash
 # 而且它是用字串當做 key,讓我們試著用字串來取值...
(byebug) params["id"]
"1"

# 用 Symbol 也可以
(byebug) params[:id]
"1"

# 來看看這個 params 是什麼東西...
(byebug) params.class
ActionController::Parameters

# 繼續來挖看看這個取值的 [] 方法放在哪兒...
(byebug) params.method("[]".to_sym)
#<Method: ActionController::Parameters#[]>

(byebug) params.method("[]".to_sym).source_location
["/Users/user/.rvm/gems/ruby-2.3.0/gems/actionpack-5.0.0.beta3/lib/action_controller/metal/strong_parameters.rb", 400]

抓到了! 就讓我們打開這個 strong_parameters.rb 檔案的第 400 行,繼續追:

def [](key)
  convert_hashes_to_parameters(key, @parameters[key])
end

嗯...這個 @parameters 是怎麼來的? 發現在 initialize 方法裡有寫到:

def initialize(parameters = {})
  @parameters = parameters.with_indifferent_access
  @permitted = self.class.permit_all_parameters
end

原來,@parameters 是呼叫了 Hash 類別的 with_indifferent_access 方法所做出來的。但這個方法並不是 Ruby 內建的,而是 Rails 幫 Hash 做的擴充功能。

Rails 幫 Ruby 的 Hash 加了一個 ActiveSupport::HashWithIndifferentAccess 類別,讓開發者在取存 Hash 的時候可以使用 Symbol (例如:params[:id]) 也可以使用字串 (例如:params["id"]),畢竟在其它程式語言不見得有 Symbol 的設計,這樣的擴充可以讓從別的程式語言轉過來的新朋友比較能習慣。

如果對 ActiveSupport::HashWithIndifferentAccess 的內容有興趣,可以點這裡看看它是怎麼做到的。

我們開 irb 來玩一下:

>> require "active_support/core_ext/hash/indifferent_access"
=> true

# profile 是一般的 Hash
>> profile = {name: "見龍", age: 18, title: "紅寶石鑑定商"}
=> {:name=>"見龍", :age=>18, :title=>"紅寶石鑑定商"}

# 用字串拿不到
>> profile["name"]
=> nil

# 要用 Symbol 才可以
>> profile[:name]
=> "見龍"

# 用 with_indifferent_access 做一個新的 new_profile
>> new_profile = profile.with_indifferent_access
=> {"name"=>"見龍", "age"=>18, "title"=>"紅寶石鑑定商"}

# 用字串可以
>> new_profile["name"]
=> "見龍"

# 用 Symbol 也可以
>> new_profile[:name]
=> "見龍"

所以,下回在存取 Hash 裡的東西的時候,要記得要拿正確的 key 喔 :)

工商服務

實體課程:Ruby on Rails 實戰課程
線上課程:五倍學院線上課程