为什么Rust
英文文档普遍将【枚举值】记作variant
而不是enum value
?
在阅读各类Rust
英文技术资料时,你是否也曾经困惑过:为何每逢【枚举值】的概念出现时,作者都会以variant
一词指代之?就字面含义而言,enum value
岂不是更贴切与易理解。简单地讲,这馁馁地是Rust
技术优越性·宣传软文的广告梗,而且是很高端的内行梗。Rustacean
们看了往往报以会心一笑 — 似乎优秀尽在不言中。至于梗在何处,请耐心听我娓娓道来!
在C++
语境下,variant
意味着什么
首先,当variant
被记作variant member
时,根据C++ 11
标准,它指的就是C union
数据结构中的字段。C union
允许在同一段内存上,每次保存数据类型不同的值。但在程序运行期间,C union
却并不支持内省·窥知它正保存哪种类型的值。程序员需要自己记忆这些代码细节与保持读写类型的一致。variant
的字面含义“变异体”也贴切地暗示了该语法项的“飘忽不定,与琢磨不透”。
其次,当variant
被记作std::variant
时,根据C++ 17
标准,它便是Tagged Union
的语法糖。比如,std::variant
就支持运行时“穷举”找出正被用来保存数据的“活跃”类型。例程1核心代码如下:
// 声明一个`int`, `float`与`std::string`类型的【共用体】。
std::variant<int, float, std::string> intFloatString;
// 初始化为`float`值。
intFloatString = 100.0f;
// 开始穷举该【共用体】,以找出它正保存什么类型的值
if (std::holds_alternative<int>(intFloatString))
std::cout << "正在保存整数!\n";
else if (std::holds_alternative<float>(intFloatString))
std::cout << "正在保存浮点数\n"; // 程序执行后,这一条日志将被输出
else if (std::holds_alternative<std::string>(intFloatString))
std::cout << "正在保存字符串\n";
个人观点,std::variant
最多算是对飙Rust enum
语言核心特性的C++
标准库实现版本。请注意被加粗的两个关键词“语言核心”与“标准库”。所以,即便Rust enum
与std::variant
功能相同,前者也是发自语言内核的“天赋技能”,而后者仅是来自标准库的“后天补丁”。和人一样,“先天就聪明”与“后天人为训练”是两码事!他们的成长上限不同,文章后续会再有提到。
Rust Union
并没有带来新改善
为了优化互操作性,Rust
也有与C union
概念对等的数据结构Union
。但程序对Rust Union
实例任何读操作都是unsafe
的,因为rustc
不能编译时保证对相同Union
实例的任何一次读写操作都采用了正确的数据类型。例如,对同一个Rust Union
实例,先用f32
写,再以String
读就会导致程序panic
,因为f32
字节序列不符合UTF-8
编码规则例程2。真是“十步一Crash
,五步一UB (i.e. Undefined Behavior)
”呀!难!
此外,对称于C union
实例在切换至新活跃“字段”时不会自动释放旧“活跃”字段的内存占用,rustc
直接禁止将Drop trait
实现类(比如,String
)作为Rust Union
数据结构的成员字段(,除非该字段被显示地标定为内存自理)例程3。这简单粗暴的作法也真的没谁了!
Rust enum
带来的创新
enum value
在C/Cpp
语法规则中只能是int
或unsigned int
类型。即便是在语法限制更为宽松的计算机语言中,enum value
至少也得是数据类型相同的值。但,在Rust
中,同一个枚举类enum
的不同枚举值enum value
被允许存储类型不同的数据。于是,
- 相比于
C union
,Rust enum
包含分辨因子discriminant
和支持(穷举)匹配。经由match
表达式(穷举)匹配全部枚举值,程序必定能找到正确的数据类型读取enum
枚举项内的值。 - 相较于
C enum
,Rust enum
能够在每个枚举值enum value
内保存不同类型的值。- 更重要的是,当
Rust enum
实例切换到新枚举值时,旧枚举值内保存对象的析构函数Drop::drop(&mut self)
会被自动调用和释放内存例程4。这兑现了rustc
的内存安全承诺。
因此,Rust enum
是C enum
与C union
的概念集合体(即,Rust enum = C enum+ C union
)。更准确地讲,Rust enum = C struct + C enum+ C union
。其中,
C struct
作为容器,起到了收拢命名空间的作用。C enum
记忆正保存哪个枚举值C union
存储不同类型具体的值
虽然Rust enum
在功能上无限接近于C++ 17
标准库中的std::variant
数据结构,但Rust enum
更高级,因为Rust enum
是语言内核特性,而不是来自标准库的后天补丁(数据结构)。于是,即便为了适配硬件条件简陋的嵌入式设备,我们不得不开启#![no_std]
模式和弃掉整个【标准库】,
Rust
程序依旧安全、精简和高性能。- 而
C++
程序员必定要为重构代码而哭晕在工位上。
综上所述,将枚举值记作variant
是Rust
团队向全世界潜移默化地输出Rust enum
技术优越性观念的手段。即,
- 兼容多类型 — 同
C union
,和引入variant
别名。 - 可穷举匹配 — 同
C enum
- 自动析构旧值 —
Rust
内存安全保证 - 语言内核支持 —
Rust
对要求弃掉【标准库】的嵌入式编程友好
然后,再追问一句:“都这么好了,你还不来上手试试吗?”。这是多有技术格的广告梗呀!
当然,任何事物都有正反两面,既然Rust enum
如此地特立独行,那么其它计算机语言应该如何布局内存来描述被FFI
导入的Rust enum
实例呢,又或许Rust enum
就从此无缘FFI
了?这注定不是轻松的工作。于是,才有了文章的最后一节:不怕,以C ABI
为中间格式。
FFI
导入Rust enum
【枚举类】从Rust
向C
的ABI
映射关系决定了其它计算机语言(比如,nodejs
)如何FFI
导入Rust
枚举值,因为Rust
是以C ABI
为中间协议实现跨语言互操作的。
ABI
话题本身就是一个Esoteric Topic
。在这里,我仅点到为止地聊两句:若Rust
程序
- 在编译时·经由
rustc
链接*.rlib
链接库文件,那就采用Rust ABI
实现互操作。- 在运行时·链接由【
rustc
加dylib
包类型】编译出的*.dll / *.dylib / *.so
链接库文件,那也采用Rust ABI
实现互操作。
Windows
的*.dll
Mac
的*.dylib
-nix
的*.so
- 在运行时·链接非以上两种类型的任何链接库文件,那都采用
C ABI
实现互操作。所以,Rust
与其它任何计算机语言都是经由C ABI
协议联通的。
似乎文字还是缺乏描述力,那就承接上图的例子,观察nodejs
如何FFI
导入由Rust
端输出的Result
枚举值。就java / python / ruby
而言,其底层原理也是一样。
简单地讲,就FFI
的调用端来说,其只见struct
与union
,而不见enum
,因为Rust enum
的穷举匹配能力被转变成了tag
索引字段的整数比较操作。tag
索引字段的
- 字段名
tag
是硬编码的C ABI
约定,改不了。 - 字段值是始于
0
的整数。 - 字段值大小反映的是
Rust
枚举值在声明时的词法次序。
最后,js
调用端的完整代码如下
const [ffi, ref, Union, Struct] = ['ffi', 'ref', 'ref-union', 'ref-struct'].map(require);
const Result = Struct({ // 虽然完全看不出数据源是`enum`,但`Tagged Union`风却直扑面门。
tag: ref.types.uint8,
union: new Union({
ciphertext: Struct({
password: 'string',
nonce: 'string'
}),
errCode: Struct({
err_code: ref.types.uint8
})
})
});
const core = ffi.Library(dllPath, {
calcNonce: ['string', []],
encryptPassword: [Result, ['string', 'string']]
});
const result = core.encryptPassword('12222', core.calcNonce());
switch (result.tag) { // 枚举值匹配·转变成了·索引值比较
case 0: // 加密成功,和输出密文密码
console.log('password=', result.union.ciphertext.password, 'nonce=', result.union.ciphertext.nonce);
break;
case 1: // 加密失败,和输出错误码
default:
console.log('err_code=', result.union.errCode.err_code);
break;
}
结束语
前不久有网友私信我和热烈讨论了这个技术点。太有意义了!事后,我汇总·提炼聊天内容,和进一步做了概念延伸(于是,才有FFI
一节)。最终,写下这篇文章与大家分享。希望那位网友看到这篇文章也能帮我点赞与发评论暖场。更请路过的神仙哥哥与仙女妹妹们指导与纠错,共同进步。谢谢!
哎,Rust
是真难学,我好像又进入“瓶颈”状态了。头疼!
评论区
写评论其实可能只是因为想要借鉴 OCaml 的 Variants 概念吧!另外,大家可以试一试 OCaml 哦,我在入坑 Rust 之前试了一下,感觉超酷的!
Rust enum >= C struct + C enum+ C union 说到点子上了
👇
ManonLoki: 每一篇文章其实都是在围绕这ffi来写,从这里看出来作者对于ffi这一块是深耕了很久,这次到了枚举,下次有机会是否方便讲讲数组的ffi映射?
棒棒的
每一篇文章其实都是在围绕这ffi来写,从这里看出来作者对于ffi这一块是深耕了很久,这次到了枚举,下次有机会是否方便讲讲数组的ffi映射?
写得越来越棒了
#是也乎,( ̄▽ ̄) 先赞再读, 这种日常发现问题/解决问题/分享问题的姿势太正义了 ;-)