[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
文章同步發表於 Medium。