Rails 中的 Hash 不是真的 Hash?

不知道你有沒有注意過,在 rails 中無意識用到的 Hash 可能跟正常在 ruby 中的 Hash 不太一樣。舉例而言在 controller 中常用的 params 你可以進行以下操作:

params[:keyword]  # => 'test'
params['keyword'] # => 'test'

當你使用 String 或是 Symbol 作為 key 的時候,都會回傳一樣的結果,好像這兩種是等同的一樣。但是在 ruby 中其實這是兩個不同的東西,你可以試著在 irb 中玩玩看:

2.4.1 :001 > h = { a: 'a', 'b' => 'b' }
{
     :a => "a",
    "b" => "b"
}
2.4.1 :002 > h[:a]
"a"
2.4.1 :003 > h['a']
nil
2.4.1 :004 > h[:b]
nil
2.4.1 :005 > h['b']
"b"

這裡就可以看到在 ruby 中,使用 Hash 跟 String 作為 key 是完全不同的兩個東西。那為什麼在 rails 中會是一樣的東西呢?你可以試著看看到底 params 是使用什麼 class:

params.instance_variable_get("@parameters").class
# => ActiveSupport::HashWithIndifferentAccess

ActiveSupport::HashWithIndifferentAccess

所以 rails 事實上是使用了 HashWithIndifferentAccess 這個 class 而不是一般的 class。進一步找到原始碼(hash_with_indifferent_access.rb)可以看到 HashWithIndifferentAccess 其實就是一個繼承 Hash 的一個 class。而其中有一個很重要的 method:

def convert_key(key) # :doc:
  key.kind_of?(Symbol) ? key.to_s : key
end

這個 method 會在 hash 被建立的時候,把所有的 Symbol key 轉換成 String key,並且在當你呼叫 `[]` 的時候:

def [](key)
  super(convert_key(key))
end

Rails 會先把你的 Symbol key 轉換成 String 再回傳給你。這就為什麼在 rails 中常見到看似 Hash 的東西的運作原理其實跟一般的 Hash 不太一樣。

Reference