【译】Rust中的Sizedness (一)

2,686 阅读9分钟

原文链接: 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::<(i32i32)>());

    // arrays
    assert_eq!(0, size_of::<[i320]>());
    assert_eq!(12, size_of::<[i323]>());

    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: &[i323] = &[123];

    // single-width pointer
    assert_eq!(WIDTH, size_of::<&[i323]>());

    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] = &[123];

    // 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碎碎念

Rust碎碎念
Rust碎碎念