[值與類型]“枚舉類型的底層原理是什么”:關(guān)于類型和抽象
本文可以看作是對知乎問題“枚舉的底層原理是什么?”的回答。
至于為什么這篇回答沒有出現(xiàn)在知乎上,是因為我暫時沒有知乎賬號能用了。嗯。
枚舉是什么?
枚舉是一種基于集合的類型,例如`enum E?{A, B, C}`就定義了類型E,E是枚舉,其具有可能取值A(chǔ), B, C。
然而如果把所有“基于集合的類型”稱為枚舉,那么所有類型便都是枚舉。自然數(shù)是1及其后繼數(shù)的傳遞閉包,F(xiàn)unction是一切能夠被調(diào)用的值的集合,PromiseLike(Thenable)是一切then屬性為Function的值的集合,諸如此類。
所以應(yīng)該說,枚舉是一種,顯式通過集合的,定義類型的方式。定義枚舉時,我們會顯式得給出“外延”來確定取值集合,而定義結(jié)構(gòu)化類型時,我們則通過給出“內(nèi)涵”來間接確定取值集合。
雖然在很多抽象層次較低的語言中,枚舉類型{‘a(chǎn)', 1}并不是合法類型,但很多抽象層次較高的強類型語言(比如hs,但我不是hs粉),以及某著名弱類型語言TypeScript中,定義這樣的類型是可行的。
在TypeScript中,我們可以這樣定義類型E。這樣定義的E是枚舉嗎?是的。只不過它沒有使用TypeScript的內(nèi)建枚舉類型。
(TypeScript的類型系統(tǒng)無限好,可惜是個假的)
“又不是不能用”,此時一位路過的全干工程師說到,“你看看你們hs的生態(tài)”
所以枚舉的“底層原理”是?
定義枚舉時我們直接給出了取值集合,所以實際上,我們在定義枚舉時,也直接定義了它的“底層原理”。我們不再需要任何其他的“底層原理”。
或者說,“底層原理”并不是一個好問題,因為枚舉的原理就是“取值集合”本身。然而通常來說,初學者真正好奇的,不是“枚舉”,而是“枚舉值”。因此一個更好的問題是,“枚舉值”可以有怎樣的“可能實現(xiàn)”。
但是在回答這一問題之前,我們首先需要回答,為什么這一問題是重要的。而這一問題的重要性,依賴于枚舉取值的“抽象性”。
抽象枚舉值
當我們定義了枚舉類型`enum?E?{A, B, C}`時,我們實際上并未給出“A是什么,B是什么,C是什么”。因而,這里的取值A(chǔ), B, C,并不是“具體值”,而只是我們隨意定義的“抽象值”。
當我們使用,諸如1,'str',true的具體值時,我們同時也指定了這類具體值的實現(xiàn),然而在使用抽象值時,我們實際上沒有指定實現(xiàn)。在let a: E = E.A中,我們并不知道語言/運行時/平臺會如何實現(xiàn)它,所以我們自然就會問,“它是怎樣被實現(xiàn)的”?
編碼:抽象值的具體實現(xiàn)
在回答這一問題之前,我們先考慮另一個問題:既然計算機本質(zhì)上只能存儲二進制串,它又是怎樣表示字符,怎樣繪制圖形,怎樣顯示出顏色的?
這類問題有個顯而易見的回答:通過二進制來表示字符,圖形,顏色。
當然我們不會滿足。那么下一個問題是,如何?
答案是:通過定義集合映射。
人類歷史上第一個被廣泛應(yīng)用的字符編碼,ASCII,在二進制串0b110000和阿拉伯數(shù)字字符'0'之間建立了映射。如果一個程序遵守ASCII,那么它就會在把0b0110000看作字符'0',并在每次需要表示字符'0'時,用0b0110000表示。ASCII在128個字符和所有7位二進制串之間建立了雙射(一一映射),因此我們能在它的基礎(chǔ)上表示這128個字符。
在抽象值和二進制串之間建立映射,使得抽象值能夠具體化,這樣的過程便是“編碼”。
人類目前廣泛使用的字符編碼Unicode,力圖在所有可能字符和二進制串之間建立映射,雖然這種映射在理論上是可數(shù)無窮對可數(shù)無窮,不過在任意時間人類社會永遠只會使用有限的字符。Unicode將二進制串集合劃分為若干“平面”(其實這個詞也可以翻譯成位面),并試圖為人類歷史上曾出現(xiàn)的每一個字符在某個平面分配一個值。
而Unicode的最常用的具體實現(xiàn),UTF-8,則是將變長二進制串映射到Unicode二進制串,這也是編碼的一種形式。
那么回到枚舉,我們?nèi)绾瓮ㄟ^編碼為枚舉值提供實現(xiàn)呢?
在enum E?{A, B, C}中,我們可以將A編碼為0b00,B為0b01,C為0b11,這樣便完成了編碼。實際上,這種映射可以是任意的,我們也可以將A映射為0b000100010100,B為0b010100010100,C為0b100000010000。
這就是為什么,詢問枚舉值的“底層原理”無意義的原因。
作為抽象類型的“數(shù)”
作為Unicode的字符集僅是抽象值,甚至,Unicode為每個字符分配的U+XXXX值也僅是抽象值,那么,“number”類型又如何呢?
二進制作為一種數(shù)制,在數(shù)學上天然是一種表示整數(shù)的方法。然而,計算機中的,或者說在內(nèi)存中的二進制,實際上僅僅是作為“紙帶”的內(nèi)存的字母表。內(nèi)存使用{0, 1}作為字母表,但實際上使用{A, B}也沒有區(qū)別。
實際上,計算機中的“二進制”,只是意味著其字母表僅含有兩個字母,并不意味著內(nèi)存或計算機真的必須使用這種數(shù)制。
另外我們知道歷史上曾經(jīng)有使用{-, 0 , +}作為字母表的計算機,顯然這種計算機也并不必須使用平衡三進制。
即使我們僅采用“二進制映射”,即將二進制串按照二進制數(shù)制映射到整數(shù),由于定長二進制運算的同余性,即在4位二進制串運算中,整數(shù)A被表示為A mod 16,我們依然需要為每個二進制串在同余關(guān)系中選擇一個位置。比如0x000既可以是0,又可以是16,0x1000既可以是8,又可以是-8,0x1001可以是9,而0x0111也可以-9。因此,采用“二進制映射”的二進制整數(shù),實際上依然是抽象的。它們不僅可以被映射為signed和unsigned,甚至這種映射方式也并不唯一。
另外,二進制映射也并不是編碼整數(shù)的唯一方式,比如格雷碼就經(jīng)常用于通信領(lǐng)域以幫助減少誤碼的影響,而BCD(Binary?Coded Decimal?)碼則在需要快速整數(shù)-字符轉(zhuǎn)換,表示大整數(shù),或精度要求較高的十進制小數(shù)時非常有用。很多數(shù)據(jù)庫管理系統(tǒng)提供的Decimal類型就是使用BCD編碼的。
整數(shù)尚且如此,實數(shù)就更復雜了。因為整數(shù)至少是可數(shù)的,而即使是(0, 1),以有限的二進制串也無法建立雙射。我們慣例上采用IEEE 754作為浮點數(shù)實現(xiàn),但實際上我們可以有多得多的實數(shù)表示方式(比如BCD碼)。
不過有趣的是,整數(shù)運算并未設(shè)置NaN,當然這也部分地因為二進制映射不允許NaN,而IEEE 754則不僅設(shè)置了不止一個NaN,還設(shè)置了±Infinity,這也是“基于控制流的錯誤處理”和“基于返回值的錯誤處理”之間的某種有趣權(quán)衡吧。前者更“方便”,因為錯誤會自動導致控制流發(fā)生變化,而后者更“靈活”,因為錯誤與控制流無關(guān)。
不只作為集合的類型
本文開頭提到,類型是“值的集合”,但類型不僅是值的集合,它通常同時也給出了對其中所有值的實現(xiàn)。當我們在Rust中寫出u8時,我們不只是在說{x∈N|0≤x≤255},我們也在說將0x00映射到0,0x01映射到1……0xff映射到255。當我們寫出f32時,我們也在聲明“使用IEEE 754”。
當然,如果現(xiàn)在我們的目標平臺并未采用IEEE 754,或者并未采用ASCII,或者使用BCD或格雷碼作為整數(shù)運算的實現(xiàn),那么支持這一平臺的編譯器通常會使用“平臺實現(xiàn)”,而非“默認實現(xiàn)”。
不過總的來說,作為連接“抽象實現(xiàn)”和“具體實現(xiàn)”的橋梁的編程語言和編譯器,其自身實際上并沒有建造這一橋梁的材料。對于“基本類型”和“內(nèi)建對象”,編譯器尚可以使用“平臺實現(xiàn)”和“標準庫/運行時”來提供,然而對于自定義類型,其實現(xiàn)便需要程序員自己來提供了。
所以,類型不只是值的集合,而是值的集合及其實現(xiàn)。編譯型語言通常選擇將相同實現(xiàn)的值歸類為同一類型,而將不同實現(xiàn)的值歸類為不同類型,這樣做可以避免運行期類型標注,從而減少開銷和對語言運行時庫的依賴。不過C++和Rust也分別支持RTTI和trait object作為補充。
什么?Java是編譯型語言?
另外,基于類的OO也正是在這一基礎(chǔ)上得以實現(xiàn)的。
不過enum實際上是介于基本類型和自定義類型之間。大多數(shù)語言內(nèi)建的enum是一種“基本高階類型”,它為所有可能的枚舉類型提供了默認實現(xiàn)。
為什么不要跨抽象層級思考
上一節(jié)提到,基本類型對應(yīng)于平臺實現(xiàn),是編譯器在平臺實現(xiàn)的基礎(chǔ)上提供的抽象,這也就是說,基本類型是比平臺實現(xiàn)更高的抽象層級。
有一道著名nt面試題,問(0.1f?+ 0.2f)與(0.3f)的關(guān)系當然在IEEE 754下,這三個數(shù)字都無法精確表示,而大多數(shù)語言下,0.1 + 0.2 == 0.3都會給出false。
那么這一問題應(yīng)該如何回答呢?可以像這樣:
float(number)類型的判等實現(xiàn)是精確判等,然而其值實現(xiàn)是不精確的。
這一回答實際上并未牽扯到IEEE 754,因為任何實數(shù)實現(xiàn)都只能為有限的實數(shù)值提供精確表示,同時,大多數(shù)實數(shù)實現(xiàn)無法提前得知允差,因此只能提供精確判等。
高抽象層級的使用者不需要了解IEEE 754,甚至也不需要了解符號-指數(shù)-尾數(shù)的浮點數(shù)結(jié)構(gòu),因為首先IEEE 754甚至浮點數(shù)并不是實數(shù)的唯一可能實現(xiàn),好的抽象必須能夠做到實現(xiàn)無關(guān)以便在需要時切換實現(xiàn),其次,抽象的好處之一就是屏蔽底層實現(xiàn)細節(jié),以降低心智負擔。
當然,還有一點也很重要,甚至說更重要。如果使用者預設(shè)了IEEE 754,并使用某種coerce或reinterpret?cast手段去直接修改數(shù)字(比如0x5F3759DF),然而平臺實現(xiàn)卻不是IEEE 754,那么這樣就會造成錯誤了。
在這一意義上,程序員不應(yīng)該去問枚舉的背后是什么。如果你需要enum到number,或者enum到string的映射,顯式得去實現(xiàn)這兩者是更好的選擇。