内容提要
Rust 中数组长度是类型的一部分,[i32; 3] 与 [i32; 4] 是不同类型;[i32] 是动态大小类型,不能单独作参数,只能藏在指针后。&[i32; 3] 是 8 字节瘦指针,&[i32] 是 16 字节胖指针(地址加长度)。函数参数应优先写 &[T] 或 &mut [T],可通吃数组、Vec 和子区间;只有需严格限定长度时才用 &[T; N]。数组到切片靠编译器内置的非固定大小转换,Vec 到切片靠 Deref 强制转换,二者机制不同。
延伸解读
为什么函数参数应优先用 &[T]
文章指出,&[T] 作为胖指针携带长度信息,能同时接收数组、Vec 和子区间,一个签名覆盖多种输入。而 &[T; N] 虽无拷贝,却把长度锁死在类型里,无法处理不同长度的数据。因此,除非业务逻辑严格要求固定长度,否则 &[T] 或 &mut [T] 是更通用、更灵活的选择,这也是标准库和第三方库的常见做法。
瘦指针与胖指针:长度信息存在哪
&[i32; 3] 是 8 字节瘦指针,长度由类型签名中的“3”在编译期确定;&[i32] 是 16 字节胖指针,运行时额外存储元素个数。文章用 size_of 验证了这一点,并总结为“瘦指针靠类型记住长度,胖指针靠值记住长度”。这一差异直接决定了 &[T; N] 只能接受固定长度,而 &[T] 能适配任意长度。
与 C++、Go 对比:Rust 切片的定位
文章将 Rust 的 &[i32] 与 C++20 的 std::span 类比,二者都是“指针加长度”的视图;而 Go 的切片是 24 字节描述符,包含容量且可扩容。Rust 把“视图”和“所有者”拆开:&[T] 只借用不拥有,扩容交给 Vec。同时,Rust 的借用检查器提供无悬垂、无别名写的安全保证,这是 C++ 和 Go 所没有的。
两种自动转换机制不要混为一谈
数组引用 &[i32; 3] 能变成 &[i32],靠的是编译器内置的非固定大小转换(Unsized Coercion),把瘦指针升级为胖指针并补上长度;而 &Vec 能变成 &[i32],靠的是 Deref 强制转换。文章强调很多教程把两者混为一谈,但它们的触发机制不同,理解这一点有助于准确判断类型转换发生的场景。
Q&A
Rust 中 [i32; 3] 和 [i32; 4] 为什么不能互相赋值?
因为数组的长度是类型的一部分,[i32; 3] 和 [i32; 4] 是两种完全不同的类型,就像 i32 和 String 一样不能互相替代。
为什么 fn foo(s: [i32]) 会编译失败?
[i32] 是动态大小类型(DST),编译器无法在编译期知道它占多少字节,而函数参数的大小必须编译期确定,所以不能直接作为参数,只能藏在指针后面使用。
&[i32; 3] 和 &[i32] 在内存占用上有什么区别?
&[i32; 3] 是瘦指针,只存首地址,在 64 位系统下占 8 字节;&[i32] 是胖指针,由数据首地址(8 字节)和元素个数(8 字节)组成,共占 16 字节。
函数参数应该用 &[i32] 还是 &[i32; 3]?
优先使用 &[i32] 或 &mut [i32],因为它可以接收数组、Vec 和子区间等多种输入;只有需要严格限定数组长度时才用 &[i32; 3]。
&[i32; 3] 和 &Vec 为什么都能传给 &[i32] 参数?
两者机制不同:&[i32; 3] 到 &[i32] 靠编译器内置的非固定大小转换(Unsized Coercion),把瘦指针升级为胖指针并补上长度;&Vec 到 &[i32] 靠 Deref 强制转换。
Rust 的 &[i32] 和 Go 的切片、C++ 的 std::span 有什么不同?
Rust 的 &[i32] 是 16 字节的借用视图,只有指针和长度,不能扩容,但有借用检查器保证安全;Go 的切片是 24 字节描述符(指针、长度、容量),可以 append 扩容但无借用检查;C++20 的 std::span 语义上最接近 &[i32],但无安全保证。