兩種錯法的代價不對稱時,過濾策略要讓失敗倒向可補救的那一邊
過濾一組候選裝置有兩個方向:列出確定要的(白名單),或列出確定不要的(黑名單)。兩個方向的正確率可以完全相同,而它們錯的時候代價不同,這個不對稱決定該選哪一個。
兩種誤判落在不同的人身上
以印表機候選清單為例:
白名單誤擋——某台印表機的宣告不符合條件,它從清單裡消失。使用者看到的是一份沒有自己印表機的清單,沒有任何操作可以把它找回來,店家當下不能出單。這個失敗使用者無法補救,而且它會打擊到一整批同型號的機器。
黑名單誤放——某個不該出現的裝置留在清單裡。使用者要主動選中它才會出事。這個失敗需要人為誤選才會實現,且範圍限於選錯的那一台機器。
兩種誤判的代價不對稱時,讓失敗落在使用者有辦法補救的那一種誤判上。所以這裡選黑名單:列出確定不是印表機的用途(HID、集線器、儲存、音訊、視訊),其餘一律放行。
這個選擇的附帶效果正好解掉類別代碼的兩層問題留下的難題——宣告成 255 供應商自訂的熱感印表機不在排除清單裡,所以它留得住;而觸控面板是 HID,被精準擋掉。白名單擋掉 255 的同時擋掉的是使用者真正要用的機器。
判斷不出來的時候放行
排除清單需要一條配套規則:取不到判斷依據時放行,不當成不合格。
藍牙印表機沒有 USB 類別欄位,這個欄位對它而言不存在。把「讀不到值」與「值不符合」歸成同一類處置,會讓整個藍牙路徑的裝置消失——而它們是完全合法的目標。
這條規則的一般形態是:過濾條件的射程要寫清楚,射程外的對象不套用該條件。 這裡的射程是「USB 裝置」,所以條件寫成「USB 裝置且用途落在排除清單裡才排除」,而不是「用途不在允許清單裡就排除」。兩種寫法對 USB 裝置的行為相同,對藍牙裝置的行為相反。
同一條規則也管裝置型號的推斷:程式從機器簽章字串判斷廠牌、遇到認不得的機型時,正確的輸出是「不確定」而不是猜一個。不確定的處置是引導使用者去設定外接裝置,而不是硬套某個廠牌的專屬流程——硬套的失敗形態是靜默的(呼叫了一個不存在的服務、或送出對方看不懂的指令),而引導的失敗形態是使用者知道自己要做什麼。
藍牙與 USB 是兩套彼此獨立的分類體系
USB 類別代碼只存在於 USB 裝置。傳統藍牙有自己的一套——Bluetooth Class of Device,用主類別加服務位元描述,印表機落在 Imaging 大類底下的一個位元。
兩套體系的數值沒有對應關係,把 USB 的類別代碼拿去比對藍牙裝置是型別上成立而語意上無意義的比對。而藍牙那一套本身的可靠度也有限:國產藍牙熱感機把 Class of Device 填得不正確的情形存在(同樣是現場回報、非本文核對過的清單),據以過濾就會誤擋。
這裡的處置是把過濾條件限定在 USB 裝置,藍牙一律放行。這個決定有一個具體的依據:機器內部的硬體(觸控面板、內部集線器上的裝置)不會出現在藍牙已配對清單裡,所以藍牙路徑上根本沒有「選到自己的螢幕」這種風險,過濾在那裡沒有要防的對象。這兩個理由各自獨立、任一個成立就足以支持同一個處置:宣告不可靠說的是在藍牙上過濾會做錯,而內部硬體不走藍牙說的是在藍牙上根本不必過濾。
型別對而語意錯的比對不會報錯
過濾條件寫對了,清單仍然可能是空的——同一個症狀有第二個成因,而它落在跨層傳遞的數值表示法上。排查「清單為什麼空」時這兩個成因都要考慮,所以它跟前面幾節屬於同一個問題的不同分支,而不是另一個主題。
原生端把 USB 的 vendor id 以十進位字串交給上層時,寫成 0xABCD 的那個值到上層手上是 "43981"。用十六進位字面值寫比對規則的程式不會編譯失敗、不會拋出例外,比對單純不成立——與過濾條件寫錯的表現完全一樣。
這一類的鑑別方式是把值印出來看,而不是重讀比對邏輯。比對邏輯讀起來是對的(那個十六進位值確實是該裝置的 vendor id),錯的是兩端對「這個欄位長什麼樣」的假設不一致,而那個假設沒有寫在任何一邊的程式碼裡。上面的數值是佔位的——每一層實際用什麼表示法由那一層自己決定,要知道自己這一套是哪一種,只有把值印出來看。跨層邊界上的每個識別碼欄位都值得問一次:它在這一端是什麼型別、什麼進位、什麼大小寫。
判斷標準
選白名單還是黑名單,先問兩種誤判分別落在誰身上、以及他有沒有辦法自己補救。選的方向要讓誤判落在有辦法自己補救的人身上。
過濾條件要寫出自己的射程,射程外的對象不套用;取不到判斷依據時放行,並把「不確定」當成一個要輸出的答案,不是一個要猜掉的空缺。
跨層邊界上的識別碼比對失效時,先印值再讀邏輯——型別對而語意錯的比對在程式碼上看起來正確。