[Rust笔记] 为什么Rust英文文档普遍将【枚举值】记作variant而不是enum value?

为什么Rust英文文档普遍将【枚举值】记作variant而不是enum value

在阅读各类Rust英文技术资料时,你是否也曾经困惑过:为何每逢【枚举值】的概念出现时,作者都会以variant一词指代之?就字面含义而言,enum value岂不是更贴切与易理解。简单地讲,这馁馁地是Rust技术优越性·宣传软文的广告梗,而且是很高端的内行梗。Rustacean们看了往往报以会心一笑 — 似乎优秀尽在不言中。至于梗在何处,请耐心听我娓娓道来!

C++语境下,variant意味着什么

首先,当variant被记作variant member时,根据C++ 11标准,它指的就是C union数据结构中的字段。C union允许在同一段内存上,每次保存数据类型不同的值。但在程序运行期间,C union却并不支持内省·窥知它正保存哪种类型的值。程序员需要自己记忆这些代码细节与保持读写类型的一致。variant的字面含义“变异体”也贴切地暗示了该语法项的“飘忽不定,与琢磨不透”。

679b7bc675b5fd683170c7099fce6dab.png

其次,当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 enumstd::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)”呀!难!

7904f32af3bea8a1f6087d0d56fa5258.png

此外,对称于C union实例在切换至活跃“字段”时会自动释放“活跃”字段的内存占用,rustc直接禁止Drop trait实现类(比如,String)作为Rust Union数据结构的成员字段(,除非该字段被显示地标定为内存自理)例程3。这简单粗暴的作法也真的没谁了!

Rust enum带来的创新

enum valueC/Cpp语法规则中只能是intunsigned int类型。即便是在语法限制更为宽松的计算机语言中,enum value至少也得是数据类型相同的值。但,在Rust中,一个枚举类enum不同枚举值enum value被允许存储类型不同的数据。于是,

  • 相比于C unionRust enum包含分辨因子discriminant和支持(穷举)匹配。经由match表达式(穷举)匹配全部枚举值,程序必定能找到正确的数据类型读取enum枚举项内的值。

  • 相较于C enum

    • Rust enum能够在每个枚举值enum value内保存不同类型的值。

    • 更重要的是,当Rust enum实例切换到枚举值时,枚举值内保存对象的析构函数Drop::drop(&mut self)会被自动调用和释放内存例程4。这兑现了rustc的内存安全承诺。

因此,Rust enumC enumC union的概念集合体(即,Rust enum = C enum+ C union)。更准确地讲,Rust enum = C struct + C enum+ C union。其中,

  • C struct作为容器,起到了收拢命名空间的作用。

  • C enum记忆正保存哪个枚举值

  • C union存储不同类型具体的值

e1fdd1a059f7a180919fc4cce07f6e13.png

虽然Rust enum在功能上无限接近于C++ 17标准库中的std::variant数据结构,但Rust enum更高级,因为Rust enum语言内核特性,而不是来自标准库的后天补丁(数据结构)。于是,即便为了适配硬件条件简陋的嵌入式设备,我们不得不开启#![no_std]模式和弃掉整个【标准库】

  • Rust程序依旧安全、精简和高性能。

  • C++程序员必定要为重构代码而哭晕在工位上。

综上所述,将枚举值记作variantRust团队向全世界潜移默化地输出Rust enum技术优越性观念的手段。即,

  • 兼容多类型 — 同C union,和引入variant别名。

  • 可穷举匹配 — 同C enum

  • 自动析构旧值 — Rust内存安全保证

  • 语言内核支持 — Rust对要求弃掉【标准库】的嵌入式编程友好

然后,再追问一句:“都这么好了,你还不来上手试试吗?”。这是多有技术格的广告梗呀!

当然,任何事物都有正反两面,既然Rust enum如此地特立独行,那么其它计算机语言应该如何布局内存来描述被FFI导入的Rust enum实例呢,又或许Rust enum就从此无缘FFI了?这注定不是轻松的工作。于是,才有了文章的最后一节:不怕,以C ABI为中间格式。

FFI导入Rust enum

【枚举类】从RustCABI映射关系决定了其它计算机语言(比如,nodejs)如何FFI导入Rust枚举值,因为Rust是以C ABI为中间协议实现跨语言互操作的。

ABI话题本身就是一个Esoteric Topic。在这里,我仅点到为止地聊两句:若Rust程序

  • 在编译时·经由rustc链接*.rlib链接库文件,那就采用Rust ABI实现互操作。

  • 在运行时·链接由【rustcdylib包类型】编译出的*.dll / *.dylib / *.so链接库文件,那也采用Rust ABI实现互操作。

    • Windows*.dll

    • Mac*.dylib

    • -nix*.so

  • 在运行时·链接非以上两种类型的任何链接库文件,那都采用C ABI实现互操作。所以,Rust与其它任何计算机语言都是经由C ABI协议联通的。

似乎文字还是缺乏描述力,那就承接上图的例子,观察nodejs如何FFI导入由Rust端输出的Result枚举值。就java / python / ruby而言,其底层原理也是一样。

01a1236801ccbb061327678fc21018c0.png

简单地讲,就FFI的调用端来说,其只见structunion,而不见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是真难学,我好像又进入“瓶颈”状态了。头疼!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值