[Ruby] Hash 的 Default Value

假設今天你要創建一個 hash,你想要這個 hash 的鍵(key)都有個預設值(default value),你會怎麼寫呢?

聽起來很簡單,我本來也一直覺得很簡單,啊不就 Hash.new(“YOUR DEFAULT VALUE”) 就好?如此一來,每個新建立的鍵,都會預先有一個值是 “YOUR DEFAULT VALUE”。比如說

writers = Hash.new('jennycodes') # set default value to 'jennycodes'
writers['jenny']  
=> 'jennycodes' # default value
writers['aguy'] = 'dontcare'
writers['aguy']
=> 'dontcare' # assigned value

So far, so fine.

假如我們今天想要建立一個 hash,其中每一個 key 都對到一個 array,不用 default value 的話我們會寫出這樣一個 function:

h = Hash.new()
def add(hsh, key, new_val)
# Create a new array first unless the key already exists.
unless hsh[key]
hsh[key] = Array.new
end
  # add the new value to array
hsh[key] << new_val
end

當然這是正確且合理的,但是 Ruby 怎麼可能讓我們寫得這麼冗?精通 Ruby 的我們當然想到直接用 default value 來解決。

# pry
h = Hash.new([])    # Now we pass a new array as the default value.
h[1] << 'a'
=>["a"] # Good.
h[2] << 'b'
=>["a", "b"] # Weird. Isn't it supposed to be ['b']?
h[3] << 'c'
=> ["a", "b", "c"] # Weirder.
h
=> {} # Where are all the values?
h.default           
=> ["a", "b", "c"]

為什麼會這樣?那些值都跑到哪裡去了?其實,當我們下了諸如 h[1] << 'a'的指令時,以程式的角度來看是這樣:

尋找 h 中有沒有一個 key 叫做 1 ?沒有 → 拿出預設值 [] → 把 ‘a’ 加入這個預設值[]中 → 打包收工。

這個過程中,並沒有任何賦值的動作,只是修改了 default value 而已。所以事實上現在 h 的預設值已經不是 [],而是 [‘a’, ‘b’, ‘c’],正如 h.default 中所看到的。而最後查看 h 本身仍然是空的,因為我們並沒有把值傳給任何的 key。

解法一:記得加上等於

# pry
h = Hash.new([])   # Still pass an empty array as default value
h[1] += ['a']
=> ["a"]
h[2] += ['b']
=> ["b"]
h[3] += ['c']
=> ["c"]
h
=> {1=>["a"], 2=>["b"], 3=>["c"]}
h.default 
=> []

賦值的動作其實就相當於 = ,而 h[1] += [‘a’] 其實就是 h[1] = h[1] + [‘a’] 的省略寫法,可以看到中間有個賦值的動作發生了,因此這個鍵就會被存在 h 裡。同時,因為h[1] + [‘a’]這個動作是對 h[1] 做的,所以並不會影響到 default value。

所以這個解法是「創造一個 default value,並且不改變它」(one default value without mutation)。

解法二:雜湊預設區塊 Hash Default Block

前面 h = Hash.new([]) 我們是傳一個引數給 Hash.new 作為雜湊預設物件(default object),如果今天我們不傳引數,而是傳一個 block 給 Hash.new,像是 h = Hash.new{‘some block’},那麼會發生兩件事:
1. 區塊會以「指向雜湊的址參器」(reference to the hash)以及「當前鍵」(current key)(即將被我們存取的鍵)作為參數,對區塊賦值。
2. 區塊回傳值(block’s return value)會變成雜湊鍵的當前值(key’s current value)。

直接看例子比較清楚:

# pry
h = Hash.new{ |hsh, key| hsh[key] = [] }   
# Still pass an empty array, but this time to a block
h[1] << 'a'
=> ["a"]
h[2] << 'b'
=> ["b"]
h[3] << 'c'
=> ["c"]
h
=> {1=>["a"], 2=>["b"], 3=>["c"]}
h.default 
=> nil

這個寫法跟上個寫法的差別是使用預設區塊每一個新的 key 得到的都會是一個新的值,而不是一個已經存在的 default value。所以 h.default 才會顯示 nil。我們的確直接改變這個值( << 的部分),但是因為每個新的鍵都拿到全新的 array,所以依然一切安好。

下面是一個很容易犯的錯,不要學:

# WARNING: THIS IS WRONG
# pry
h = Hash.new{ [] }   # Create a new array in the block. 
# Sounds reasonable, right?
h[1] << 'a'       
=> ["a"] # Good.
h[2] << 'b'
=> ["b"] # Good.
h[3] << 'c'
=> ["c"] # Good.
h
=> {} # Here.
h.default 
=> nil

h 為什麼會是空的?前面講到「區塊回傳值會變成雜湊鍵的當前值」,但是這不代表它有對雜湊鍵賦值如果我們沒有指明 h[key] 來接收 [] 這個 block 的回傳值,那麼它就不會存進這個鍵裡面。所以 h 只會一次次呼叫新的區塊給每一個鍵。

哪種解法好?

都好,只是個人覺得用解法二(雜湊預設區塊)寫比較容易閱讀,寫起來也比較直觀。但是一定要記得正確的寫法,不然到時候出 bug 別說你沒有被警告過!

參考資料

深入淺出 Ruby:A Brain-Friendly Guide

class Hash - Documentation for Ruby 2.6.0
docs.ruby-lang.org
Ruby hash default value behavior
stackoverflow.com

文章同步發表於 Medium


  • Find me at