“
原文链接: https://github.com/pretzelhammer/rust-blog/blob/master/posts/sizedness-in-rust.md
原文标题: Sizedness in Rust
公众号: Rust碎碎念
介绍
Sizedness是Rust中需要理解的最重要的概念中最不起眼的一个。它经常以微妙的方式贯穿于众多其他的语言特性之中,并且仅以"x doesn't have size known at compile time"这种每个Rustacean都熟悉的错误信息的方式出现。在本文中,我们将会探讨从确定大小类型(sized type),到不确定大小类型(unsized type),再到零大小类型(zero-sized type)等各种类型的sizedness,同时对它们的用例,优势,痛点以及解决方法进行评估。
下面是我所使用的短语表格以及它们的意思:
词语 | 含义 |
---|---|
sizedness | 确定大小(sized)或不确定大小(unsized)的特性 |
sized type | 在编译期可以确定大小的类型 |
1) unsized type or 2) DST |
动态大小的类型,例如,在编译期无法确定的大小 |
?sized type | 可能是确定大小也可能是不确定大小的类型 |
unsized coercion | 强制把确定大小类型(sized type)转为不确定大小类型(unsized type) |
ZST | 零大小(zero-sized)类型,比如,某类型的实例大小为0字节 |
width | 指针宽度的测量单位 |
1) thin pointer or 2) single-width pointer |
1个宽度(width)的指针 |
1) fat pointer or 2) double-width pointer |
2个宽度(width)的指针 |
1) pointer or 2) reference |
指向某种宽度的指针,宽度由上下文确定 |
slice | 指向某个数据的动态大小视图(view)的双宽度(double-width)指针 |
Sizedness
在Rust里,如果一个类型的字节大小在编译期可以确定,那么这个类型就是确定大小(sized)的。确定类型的大小(size)对于能够在栈(stack)上为实例分配足够的空间是十分重要的。确定大小类型(sized type)可以通过传值(by value)或者传引用(by reference)的方式来传递。如果一个类型的大小不能在编译期确定,那么它就被称为不确定大小类型(unsized type)或者DST,即动态大小类型(Dynamically-Sized Type)。因为不确定大小类型(unsized type)不能存放在栈上,所以它们只能通过传引用(by reference)的方式来传递。下面是确定大小(size)和不确定大小(unsized)类型的一些例子:
use std::mem::size_of;
fn main() {
// primitives
assert_eq!(4, size_of::<i32>());
assert_eq!(8, size_of::<f64>());
// tuples
assert_eq!(8, size_of::<(i32, i32)>());
// arrays
assert_eq!(0, size_of::<[i32; 0]>());
assert_eq!(12, size_of::<[i32; 3]>());
struct Point {
x: i32,
y: i32,
}
// structs
assert_eq!(8, size_of::<Point>());
// enums
assert_eq!(8, size_of::<Option<i32>>());
// get pointer width, will be
// 4 bytes wide on 32-bit targets or
// 8 bytes wide on 64-bit targets
const WIDTH: usize = size_of::<&()>();
// pointers to sized types are 1 width
assert_eq!(WIDTH, size_of::<&i32>());
assert_eq!(WIDTH, size_of::<&mut i32>());
assert_eq!(WIDTH, size_of::<Box<i32>>());
assert_eq!(WIDTH, size_of::<fn(i32) -> i32>());
const DOUBLE_WIDTH: usize = 2 * WIDTH;
// unsized struct
struct Unsized {
unsized_field: [i32],
}
// pointers to unsized types are 2 widths
assert_eq!(DOUBLE_WIDTH, size_of::<&str>()); // slice
assert_eq!(DOUBLE_WIDTH, size_of::<&[i32]>()); // slice
assert_eq!(DOUBLE_WIDTH, size_of::<&dyn ToString>()); // trait object
assert_eq!(DOUBLE_WIDTH, size_of::<Box<dyn ToString>>()); // trait object
assert_eq!(DOUBLE_WIDTH, size_of::<&Unsized>()); // user-defined unsized type
// unsized types
size_of::<str>(); // compile error
size_of::<[i32]>(); // compile error
size_of::<dyn ToString>(); // compile error
size_of::<Unsized>(); // compile error
}
确定确定大小类型(sized types)的大小的方式是直观的:所有的基本类型和指针拥有确定大小,仅由基本类型和指针或者内嵌的结构体(structs),元组(tuples),枚举(enums)以及数组(arrays)组成的结构体(struct)、元组(tuple)、枚举(enum)和数组(array),我们可以在考虑填充和对齐所需的额外字节数的情况下,递归地把字节数加起来。同样比较直观的原因,我们不能确定不确定大小类型(unsized type)的大小:切片(slice)可以有任意数量的元素从而在运行时的大小也是任意的,trait对象可以被任意数量的结构体(struct)或枚举(enum)实现,因此在运行时也可以是任意的大小。
Pro tips
- 在Rust中,指向数组的动态大小视图(dynamically sized views)被称为切片(slice)。例如,一个
&str
是一个"字符串切片(string slice)" ,一个&[i32]
是一个"i32切片"。 - 切片(slice)是双宽度(double-width)的,因为他们存储了一个指向数组的指针和数组中元素的数量。
- trait对象指针是双宽度(double-width)的,因为他们存储了一个指向数据的指针和一个指向vtale的指针。
- 不确定大小(unsized) 结构体指针是双宽度的,因为他们存储了一个指向结构体数据的指针和结构体的大小(size)。
- 不确定大小(unsized) 结构体只能拥有有1个不确定大小(unsized)字段(field)而且它必须是结构体里的最后一个字段(field)。
为了彻底说明不确定大小类型(unsized type)的双宽度(double-width)指针,下面是带有注释的比较数组(array)和切片(slice)代码示例:
use std::mem::size_of;
const WIDTH: usize = size_of::<&()>();
const DOUBLE_WIDTH: usize = 2 * WIDTH;
fn main() {
// data length stored in type
// an [i32; 3] is an array of three i32s
let nums: &[i32; 3] = &[1, 2, 3];
// single-width pointer
assert_eq!(WIDTH, size_of::<&[i32; 3]>());
let mut sum = 0;
// can iterate over nums safely
// Rust knows it's exactly 3 elements
for num in nums {
sum += num;
}
assert_eq!(6, sum);
// unsized coercion from [i32; 3] to [i32]
// data length now stored in pointer
let nums: &[i32] = &[1, 2, 3];
// double-width pointer required to also store data length
assert_eq!(DOUBLE_WIDTH, size_of::<&[i32]>());
let mut sum = 0;
// can iterate over nums safely
// Rust knows it's exactly 3 elements
for num in nums {
sum += num;
}
assert_eq!(6, sum);
}
这是另外一个带有注释的比较结构体和trait对象的代码示例:
use std::mem::size_of;
const WIDTH: usize = size_of::<&()>();
const DOUBLE_WIDTH: usize = 2 * WIDTH;
trait Trait {
fn print(&self);
}
struct Struct;
struct Struct2;
impl Trait for Struct {
fn print(&self) {
println!("struct");
}
}
impl Trait for Struct2 {
fn print(&self) {
println!("struct2");
}
}
fn print_struct(s: &Struct) {
// always prints "struct"
// this is known at compile-time
s.print();
// single-width pointer
assert_eq!(WIDTH, size_of::<&Struct>());
}
fn print_struct2(s2: &Struct2) {
// always prints "struct2"
// this is known at compile-time
s2.print();
// single-width pointer
assert_eq!(WIDTH, size_of::<&Struct2>());
}
fn print_trait(t: &dyn Trait) {
// print "struct" or "struct2" ?
// this is unknown at compile-time
t.print();
// Rust has to check the pointer at run-time
// to figure out whether to use Struct's
// or Struct2's implementation of "print"
// so the pointer has to be double-width
assert_eq!(DOUBLE_WIDTH, size_of::<&dyn Trait>());
}
fn main() {
// single-width pointer to data
let s = &Struct;
print_struct(s); // prints "struct"
// single-width pointer to data
let s2 = &Struct2;
print_struct2(s2); // prints "struct2"
// unsized coercion from Struct to dyn Trait
// double-width pointer to point to data AND Struct's vtable
let t: &dyn Trait = &Struct;
print_trait(t); // prints "struct"
// unsized coercion from Struct2 to dyn Trait
// double-width pointer to point to data AND Struct2's vtable
let t: &dyn Trait = &Struct2;
print_trait(t); // prints "struct2"
}
关键点(Key Takeaway)
- 只有确定大小类型(sized type)的实例可以被放到栈上,也就是,可以通过值传递
- 不确定大小类型(unsized type)的实例不能被放置在栈上并且必须通过引用来传递
- 不确定大小类型(unsized type)的指针是双宽度(double-width)的,因为除了指向数据之外,他们还需要做一些额外的记录来追踪数据的长度或者指向一个vtable
本文禁止转载,谢谢配合!欢迎关注我的微信公众号: Rust碎碎念