欢迎光临散文网 会员登陆 & 注册

[值与类型]“枚举类型的底层原理是什么”:关于类型和抽象

2021-04-17 15:59 作者:useStrict  | 我要投稿

本文可以看作是对知乎问题“枚举的底层原理是什么?”的回答。

至于为什么这篇回答没有出现在知乎上,是因为我暂时没有知乎账号能用了。嗯。


枚举是什么?

枚举是一种基于集合的类型,例如`enum E {A, B, C}`就定义了类型E,E是枚举,其具有可能取值A, B, C。

然而如果把所有“基于集合的类型”称为枚举,那么所有类型便都是枚举。自然数是1及其后继数的传递闭包,Function是一切能够被调用的值的集合,PromiseLike(Thenable)是一切then属性为Function的值的集合,诸如此类。

所以应该说,枚举是一种,显式通过集合的,定义类型的方式。定义枚举时,我们会显式得给出“外延”来确定取值集合,而定义结构化类型时,我们则通过给出“内涵”来间接确定取值集合。

虽然在很多抽象层次较低的语言中,枚举类型{‘a', 1}并不是合法类型,但很多抽象层次较高的强类型语言(比如hs,但我不是hs粉),以及某著名弱类型语言TypeScript中,定义这样的类型是可行的。

在TypeScript中,我们可以这样定义类型E。这样定义的E是枚举吗?是的。只不过它没有使用TypeScript的内建枚举类型。

(TypeScript的类型系统无限好,可惜是个假的)

“又不是不能用”,此时一位路过的全干工程师说到,“你看看你们hs的生态”


所以枚举的“底层原理”是?

定义枚举时我们直接给出了取值集合,所以实际上,我们在定义枚举时,也直接定义了它的“底层原理”。我们不再需要任何其他的“底层原理”。

或者说,“底层原理”并不是一个好问题,因为枚举的原理就是“取值集合”本身。然而通常来说,初学者真正好奇的,不是“枚举”,而是“枚举值”。因此一个更好的问题是,“枚举值”可以有怎样的“可能实现”。

但是在回答这一问题之前,我们首先需要回答,为什么这一问题是重要的。而这一问题的重要性,依赖于枚举取值的“抽象性”。


抽象枚举值

当我们定义了枚举类型`enum E {A, B, C}`时,我们实际上并未给出“A是什么,B是什么,C是什么”。因而,这里的取值A, B, C,并不是“具体值”,而只是我们随意定义的“抽象值”。

当我们使用,诸如1,'str',true的具体值时,我们同时也指定了这类具体值的实现,然而在使用抽象值时,我们实际上没有指定实现。在let a: E = E.A中,我们并不知道语言/运行时/平台会如何实现它,所以我们自然就会问,“它是怎样被实现的”?


编码:抽象值的具体实现

在回答这一问题之前,我们先考虑另一个问题:既然计算机本质上只能存储二进制串,它又是怎样表示字符,怎样绘制图形,怎样显示出颜色的?

这类问题有个显而易见的回答:通过二进制来表示字符,图形,颜色。

当然我们不会满足。那么下一个问题是,如何?

答案是:通过定义集合映射

人类历史上第一个被广泛应用的字符编码,ASCII,在二进制串0b110000和阿拉伯数字字符'0'之间建立了映射。如果一个程序遵守ASCII,那么它就会在把0b0110000看作字符'0',并在每次需要表示字符'0'时,用0b0110000表示。ASCII在128个字符和所有7位二进制串之间建立了双射(一一映射),因此我们能在它的基础上表示这128个字符

在抽象值和二进制串之间建立映射,使得抽象值能够具体化,这样的过程便是“编码”

人类目前广泛使用的字符编码Unicode,力图在所有可能字符和二进制串之间建立映射,虽然这种映射在理论上是可数无穷对可数无穷,不过在任意时间人类社会永远只会使用有限的字符。Unicode将二进制串集合划分为若干“平面”(其实这个词也可以翻译成位面),并试图为人类历史上曾出现的每一个字符在某个平面分配一个值。

而Unicode的最常用的具体实现,UTF-8,则是将变长二进制串映射到Unicode二进制串,这也是编码的一种形式。


那么回到枚举,我们如何通过编码为枚举值提供实现呢?

在enum E {A, B, C}中,我们可以将A编码为0b00,B为0b01,C为0b11,这样便完成了编码。实际上,这种映射可以是任意的,我们也可以将A映射为0b000100010100,B为0b010100010100,C为0b100000010000

这就是为什么,询问枚举值的“底层原理”无意义的原因。



作为抽象类型的“数”

作为Unicode的字符集仅是抽象值,甚至,Unicode为每个字符分配的U+XXXX值也仅是抽象值,那么,“number”类型又如何呢?

二进制作为一种数制,在数学上天然是一种表示整数的方法。然而,计算机中的,或者说在内存中的二进制,实际上仅仅是作为“纸带”的内存的字母表。内存使用{0, 1}作为字母表,但实际上使用{A, B}也没有区别。

实际上,计算机中的“二进制”,只是意味着其字母表仅含有两个字母,并不意味着内存或计算机真的必须使用这种数制。

另外我们知道历史上曾经有使用{-, 0 , +}作为字母表的计算机,显然这种计算机也并不必须使用平衡三进制。

即使我们仅采用“二进制映射”,即将二进制串按照二进制数制映射到整数,由于定长二进制运算的同余性,即在4位二进制串运算中,整数A被表示为A mod 16,我们依然需要为每个二进制串在同余关系中选择一个位置。比如0x000既可以是0,又可以是16,0x1000既可以是8,又可以是-8,0x1001可以是9,而0x0111也可以-9。因此,采用“二进制映射”的二进制整数,实际上依然是抽象的。它们不仅可以被映射为signed和unsigned,甚至这种映射方式也并不唯一。

另外,二进制映射也并不是编码整数的唯一方式,比如格雷码就经常用于通信领域以帮助减少误码的影响,而BCD(Binary Coded Decimal‎)码则在需要快速整数-字符转换,表示大整数,或精度要求较高的十进制小数时非常有用。很多数据库管理系统提供的Decimal类型就是使用BCD编码的。

整数尚且如此,实数就更复杂了。因为整数至少是可数的,而即使是(0, 1),以有限的二进制串也无法建立双射。我们惯例上采用IEEE 754作为浮点数实现,但实际上我们可以有多得多的实数表示方式(比如BCD码)。

不过有趣的是,整数运算并未设置NaN,当然这也部分地因为二进制映射不允许NaN,而IEEE 754则不仅设置了不止一个NaN,还设置了±Infinity,这也是“基于控制流的错误处理”和“基于返回值的错误处理”之间的某种有趣权衡吧。前者更“方便”,因为错误会自动导致控制流发生变化,而后者更“灵活”,因为错误与控制流无关。


不只作为集合的类型

本文开头提到,类型是“值的集合”,但类型不仅是值的集合,它通常同时也给出了对其中所有值的实现。当我们在Rust中写出u8时,我们不只是在说{x∈N|0≤x≤255},我们也在说将0x00映射到0,0x01映射到1……0xff映射到255。当我们写出f32时,我们也在声明“使用IEEE 754”。

当然,如果现在我们的目标平台并未采用IEEE 754,或者并未采用ASCII,或者使用BCD或格雷码作为整数运算的实现,那么支持这一平台的编译器通常会使用“平台实现”,而非“默认实现”。

不过总的来说,作为连接“抽象实现”和“具体实现”的桥梁的编程语言和编译器,其自身实际上并没有建造这一桥梁的材料。对于“基本类型”和“内建对象”,编译器尚可以使用“平台实现”和“标准库/运行时”来提供,然而对于自定义类型,其实现便需要程序员自己来提供了。

所以,类型不只是值的集合,而是值的集合及其实现。编译型语言通常选择将相同实现的值归类为同一类型,而将不同实现的值归类为不同类型,这样做可以避免运行期类型标注,从而减少开销和对语言运行时库的依赖。不过C++和Rust也分别支持RTTI和trait object作为补充。

什么?Java是编译型语言?

另外,基于类的OO也正是在这一基础上得以实现的。

不过enum实际上是介于基本类型和自定义类型之间。大多数语言内建的enum是一种“基本高阶类型”,它为所有可能的枚举类型提供了默认实现。


为什么不要跨抽象层级思考

上一节提到,基本类型对应于平台实现,是编译器在平台实现的基础上提供的抽象,这也就是说,基本类型是比平台实现更高的抽象层级。

有一道著名nt面试题,问(0.1f + 0.2f)与(0.3f)的关系当然在IEEE 754下,这三个数字都无法精确表示,而大多数语言下,0.1 + 0.2 == 0.3都会给出false。

那么这一问题应该如何回答呢?可以像这样:

float(number)类型的判等实现是精确判等,然而其值实现是不精确的。

这一回答实际上并未牵扯到IEEE 754,因为任何实数实现都只能为有限的实数值提供精确表示,同时,大多数实数实现无法提前得知允差,因此只能提供精确判等。

高抽象层级的使用者不需要了解IEEE 754,甚至也不需要了解符号-指数-尾数的浮点数结构,因为首先IEEE 754甚至浮点数并不是实数的唯一可能实现,好的抽象必须能够做到实现无关以便在需要时切换实现,其次,抽象的好处之一就是屏蔽底层实现细节,以降低心智负担。

当然,还有一点也很重要,甚至说更重要。如果使用者预设了IEEE 754,并使用某种coerce或reinterpret cast手段去直接修改数字(比如0x5F3759DF),然而平台实现却不是IEEE 754,那么这样就会造成错误了。

在这一意义上,程序员不应该去问枚举的背后是什么。如果你需要enum到number,或者enum到string的映射,显式得去实现这两者是更好的选择。

[值与类型]“枚举类型的底层原理是什么”:关于类型和抽象的评论 (共 条)

分享到微博请遵守国家法律