Rust 中的零向量:并非看起来那么简单
乍一看,创建一个填充为零的向量能有多难?在 Python 中,一行代码就能搞定:[0] * n。在 JavaScript 中,则是 Array(n).fill(0)。但当你转向 Rust 时,即使是这样一个简单的任务,也会打开一个系统编程、内存管理和零成本抽象的全新世界。
Stack Overflow 上关于 "Creating a vector of zeros for a specific size" 的问题已经积累了数千次浏览。这并非偶然:在这个简单的请求背后,隐藏着对 Rust 如何处理内存、初始化和安全性的深刻理解。今天,我们将深入 Rust 的底层,剖析创建零向量的所有方法,更重要的是,理解内存中实际发生了什么。
为什么这比在 Python 中更复杂?
在 Python 或 JavaScript 中,你只需创建一个数组,解释器就会替你完成所有脏活累活。而 Rust 是一门系统级语言。它不允许内存保持未初始化状态(除非你使用 unsafe)。每个字节要么被显式填充,要么被标记为“未定义行为”。
想象一下:你订购了 100 个空盒子。在 Python 中,他们会直接给你送来 100 个空盒子。而在 Rust 中,快递员会问:“每个盒子里要放什么?”——如果你不说,他根本就不会来。这保证了你永远不会意外地读取到上一个程序留在内存中的“垃圾”。
方法 #1:经典方法 vec![0; size]
最直接且推荐的方法是使用 vec! 宏:
let zeros: Vec<i32> = vec![0; 10];println!("{:?}", zeros); // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]简洁、优雅、易懂。但底层发生了什么?让我们逐步分析。
vec![0; 10] 的底层原理
当你编写 vec![0; 10] 时,Rust 会执行以下操作:
- 在堆上分配内存,用于存放 10 个
i32类型的元素。每个i32占用 4 个字节,总共 40 个字节。 - 检查该类型是否实现了
Clonetrait。i32数字实现了它,因此编译器可以为每个元素复制值0。 - 在
alloc::vec::from_elem内部启动一个复制循环。如果类型是简单的标量类型,编译器会将该循环优化为memset。 - 返回一个
Vec,其长度为 10,容量也为 10。
关键点在于:内存不会保持未初始化状态。Rust 保证在你访问向量之前,每个字节都会被零填充。
方法 #2:vec![Default::default(); size]
如果你的类型不是数字,但实现了 Default trait,你可以使用泛型方法:
#[derive(Default)]struct Point { x: f64, y: f64,}
let points: Vec<Point> = vec![Default::default(); 5];println!("{:?}", points[0]); // Point { x: 0.0, y: 0.0 }对于原始类型,Default::default() 会给出 0、0.0、false 或 '\\0'。对于自定义结构体,则是你在 #[derive(Default)] 中定义的内容。
性能如何?
让我们编写一个简单的基准测试(在实际代码中请使用 criterion):
use std::time::Instant;
fn main() { let n = 1_000_000;
let start = Instant::now(); let v1 = vec![0u8; n]; let dur1 = start.elapsed();
let start = Instant::now(); let v2 = vec![0u64; n]; let dur2 = start.elapsed();
println!("u8: {:?}", dur1); println!("u64: {:?}", dur2);}在