熟悉函数式编程语言的朋友可能很喜欢如下定义:
List e = Empty | Elem e (List e)
这是一种典型的递归表达,非常 简洁,我们把他迁移到 Rust 语言实现:
pub enum List {
Empty,
Elem(i32, List)
}
同样 简洁 的表达,但是在 Rust 中无法通过编译,因为 Rust 默认是 栈内存分配 ,而上述定义由于Elem递归定义,导致无法在编译时确定大小,从而无法通过编译。
我们可以通过简单的修改解决:
pub enum List {
Empty,
Elem(i32, Box<List>)
}
上述定义在 Rust 官方教程书中关于Box类型章节 (链接) 的讲解中可以看到。
Box在 Rust 中是声明对象在 堆内存 分配,这样我们就可以像其他语言(GO, JAVA)那样可以仅持有对象引用指针,而引用指针的大小在编译时是固定大小的,就可以完美解决我们编译问题。
这样我们就可以在 Rust 中定义一个简洁的链表对象。
let a_list = List::Elem(1, Box::new(List::Elem(2, Box::new(List::Empty))))
至此大家可能以为结束了,实际该定义存在一些潜在的问题:
-
我们上面定义的
a_list对象实际头节点会分配在栈中,而其余后续节点则在堆中分配,这导致我们的 List 内存分配不统一 内存不统一会导致一些潜在的消耗,比如我们要对链表进行拆分,就会导致链表头节点从堆内存到栈内存的Copy,同理合并操作会导致一次栈内存到堆内存的Copy,带来潜在开销。但我们通常使用链表的目的就是减少 Copy 操作。显然与我们的目的相悖。 聪明的朋友可能会想到,我们可以用Box对象把a_list定义包裹起来:let a_list = Box::new(List::Elem(1, Box::new(List::Elem(2, Box::new(List::Empty)))))这样的操作显然不合适,我们每次定义链表都需要显式的声明其在堆内存中分配。这个在协作中是很难把握的,我们无法预料使用者的习惯。
-
另一个问题就是内存浪费。由于 Rust 的枚举设计特殊,我们可以在枚举中定义带有不同属性的对象,为了在编译时确定对象大小,枚举对象的大小会取最大对象的长度,其他对象长度做填充处理,保持对象长度的统一。回想我们定义的
List枚举,其中Empty没有字段,Elem包含一个i32类型和一个Box类型。因此,即使我们使用Empty对象,其至少也会有两个字段长度的填充(具体长度不做讨论,我们确定会有填充即可)。这样的话,我们的尾节点必然存在内存浪费。
显然,我们上述链表定义看似优雅,实则问题颇多。这些问题并不是我们能够轻易发现的,可以确定的是,这些问题其实是由于 Rust 语言设计导致的。GO,JAVA 等带 GC 的堆内存语言并不会出现该问题。Rust 的实践难度可见一斑,Rust 在给我们提供高性能的同时,也需要我们更加关注底层细节,只有熟悉底层细节,我们在能更好的掌控这门语言,写出更高效的代码。
至于如何改进该定义,在此不做讨论。大家有兴趣可以阅读原文章,原文中还讨论了 Rust 关于枚举类型零开销优化的细节。本文并未提及。
作者水平有限,如有纰漏,敬请谅解。