Strong parameter 參數與 Array 之:老師我可以換位置嗎?

An-Chieh Kuo (郭安傑/關節)
7 min readJun 24, 2018

--

在故事開始前

Rails 多對多關聯設定與存取的文章中,我偷懶的在未測試的狀況下將Strong parameter 的參數位置進行對調,結果被王綠島抓包沒辦法使用XD。

params.require(:post).permit( #other_stuff, category_ids:[]) # 正確的params.require(:post).permit( category_ids:[], #other_stuff) # 改錯的

為什麼只是改參數位置就不能用?

找了資料後發現 permit 會把全部的參數當作 Array 去操作,所以 permit(:a, :b, #other stuff) 的輸入會被轉換成 [:a, :b, #other stuff],也就是可以把 permit(:a, :b, #other stuff)permit([:a, :b, #other stuff])當作是同樣的東西。

我們知道可以用[] 作為建構子,裡面放 Object 並用逗號隔開就能產生 Array,下面測試看看:

$ [:a, :b, c: []]  #=> [:a, :b, {:c=>[]}]$ [:a, b: [], :c]  #syntax error, unexpected ']', expecting =>
[:a, b: [], :c]
^
$ [:a, b: [], c: []] #=> [:a, {:b=>[], :c=>[]}]$ [:a, {b: []}, :c] #=> [:a, {:b=>[]}, :c]

第一行使用 [:a, :b, c: []] 會得到一個 Array ,Array 內包含了三個Object,分別是 :a:b 兩個 Symbol 以及一個 Hash {:c=>[]},代表可以用這個方法直接建立一個包含 Hash 的 Array。

第二行使用 [:a, b: [], :c] 會得到 syntax error ,而且告訴我們 :c 後面應該要是 => 而非 ] ,所以代表程式預期 :c 的位置不單單只是個 Symbol,而是要一組 key-value pair。

第三行依照剛剛的推論輸入[:a, b: [], c: []]後發現,雖然在 [] 建構子中用逗號隔開 :ab: []c: [],但得到的並不是包含一個symbol跟兩個 Hash 的Array,而是他把b: [], c: []視為一個 Hash。

不過只要用大括號明確定義你的 Hash 的範圍就不會有這個困擾,例如第四行的結果。把這個作法套用到 permit 的參數內,params.require(:post).permit(category_ids:[], #other_stuff)只要改成params.require(:post).permit({category_ids:[]}, #other_stuff)就能夠使用了!

到這邊應該算是解決了 Strong parameter 的參數換位問題。 但又點出了另一個疑問…

Ruby 怎麼解讀程式碼

為什麼用 [:a, b: [], :c] 會 syntax error,用[:a, b: [], c: []]會得到[:a, {:b=>[], :c=>[]}],我認為有兩個原因:

  1. 編譯器解讀程式碼時是由左向右讀
  2. Ruby 的語法糖衣:括號可省略

首先是編譯器由左向右解讀程式碼,同樣以[:a, b: [], c: []]舉例來說:

第一階段:遇到 [ ,知道有個 Array ,並預期會有描述 Array 內容的語法出現。

第一階段:遇到[,所以認為現在是要建立一個 Array ,而且預期接下來會碰到的是 index 為 0 時所對應的值 / Object,然後最後會以 ] 來結束 Array 宣告。

第二階段:遇到 :a, ,並預期會有描述下一個 index 對應值的語法出現。

第二階段:遇到:a,。因為 Array 是以逗號隔開各個 index 對應的內容,所以可以確定 index 為0時所對應的值為 :a,並預期接下來會碰到的是 index 為 1 時所對應的值 / Object,然後最後會以 ] 來結束 Array 宣告。

第三階段:遇到 b: ,發現是個 Hash ,並預期會有描述 Hash value 的語法出現。

第三階段:遇到 b:, 這語法代表 Ruby 中 Hash 的 key ,表示現在在描述的是一個 Hash,也就是 index 為 1 時所對應的 Object 是一個 Hash!既然 key 出現了那後面應該要有 value,所以預期接下來會碰到的是 b: 所對應的value。然後要等 Hash 的東西講完了才會回到 Array ,所以最後會以 ] 來結束 Array 宣告。

第四階段:遇到 [], ,發現後面全部都會變成 Hash的!

第四階段:遇到 [], ,因為 Hash 也是以逗號隔開內容,所以可以確定 b: 所對應的value為 []。問題是 Hash 跟 Array 一樣都是以逗號隔開內容,又因為在前面省略了 { ,沒辦法以 } 來結束 Hash 的宣告,導致後面的東西都會被認為是 Hash 的內容物! 所以程式會預期接下來會碰到的是 Hash 裡面的另外一組 key-value pair 。直到最後碰到 ] 來結束 Array ,才會順便終結 Hash 的宣告。以[:a, b: [], c: []]的案例來說後面全部都符合預期,所以平安無事。

現在再回去看前面跑出 syntax error 的語法:

$ [:a, b: [], :c]  #syntax error, unexpected ']', expecting =>
[:a, b: [], :c]
^

:c 的部分符合第四階段預期的 key-value pair 中的 key,但是沒有出現指派 value 的 => 運算子,反而是出現 ] 直接結束掉 Array,不符合程式預期的結果,就噴錯誤了。

再看看有使用大括號的作法:

$ [:a, {b: []}, :c] #=> [:a, {:b=>[]}, :c]

因為有使用大括號,所以能夠以 {} 來限制 Hash 的範圍,才能夠結束定義 Hash 的步驟,重新回到定義 Array 內容的流程上。

所以下次用 [] 建立 Array ,又不想把 Hash 放最後,記得要用大括號 {} 包起來,不然後面的東西會被 Hash 搶光光囉!

Hash:我全都要

註:上面編譯器解讀方式舉例是簡化過的,省略了一些東西,詳細內容可參閱參考資料2。(老實說我沒能力全部理解內容,有講錯還請多指教)

參考資料

  1. strong_parameters.rbpermit 會把全部的參數當作 Array 去操作
  2. Ruby Hacking Guide-Chapter 9:說明編譯器中 Parser 與 Scanner 的運作模式,也就是程式在解讀程式碼的方法。
  3. How Ruby Parses and Compiles Your Code:跟參考資料2講的是差不多的東西,不過有附圖舉例說明。
  4. Rails 多對多關聯設定與存取:本篇前傳(誤)

--

--