在Rust标准库中,存在很多常用的工具类特型,它们能帮助我们写出更具有Rust风格的代码。
一个Sized 类型是指 它所有的值在内存中有相同的大小,反之没有相同大小就是UnSized 类型。
Rust中几乎所有的类型都是Sized
的,例如每个u64在内存中都是8个字节,每个(f32,f32,f32)
元组的大小是12字节。甚至枚举也是Sized的。枚举的内存大小一般为能容纳最大字节数量变量的一个固定值。虽然Vec<T>
它拥有一个分配在堆上的大小可变的缓冲区,但是Vec
值本身是指向这个缓冲区的指针,同时包含了容量和长度,所以Vec<T>
也是Sized 类型。
所有Sized 类型实现了 std::marker::Sized
特型,这个特型为一个空特型 ,不包含任何方法或者关联类型。Rust 自动为所有能应用它的类型自动实现了它,你不能手动去实现Sized
特型。Sized
唯一使用的场景为类型变量约束, 一个类似T:Sized
的约束需要类型T
的大小在编译时是已知的。这一类的特型被称为marker
特型,因为Rust语言使用它们来标记某些类型具有特定的行为。
然而,Rust也有少部分类型为unsized
类型,他们的值占用的空间并不是相同的。例如,字符串切片类型str
(注意,没有前面的&),是unsized
。字符串文字值hello
和big
是对字符串切片str
的引用,而这两个切片分别占用5和3字节。但是&str
是固定大小的,它是个两字节的胖指针。同理数组切片类型[T]
也是unsized
,一个共享的[u8]
的引用&[u8]
可以指向任意大小的切片 。因为str
和[T]
类型用于声明可变大小的值,所以他们是unsized
类型。
另一类常见的unsized
类型为dyn
类型,也就是特型对象。特型对象是一个指向实现了某个特型的值的指针。例如,类型&dyn std::io::Write
和Box<dyn std::io::Write>
是指向某些实现了Write
特型的引用。
这里要澄清一下,特型对象和对特型对象的引用之间的关系就和切片及切片的引用一样,有些混淆,特型对象是指dyn write
, 而&dyn Write
是对特型对象的引用。前者是unsized
类型,因为实现Write
的对象是多种多样的,大小不固定的。而后者是Sized
类型,因为它其实是一个胖指针。
Rust 并不能使用变量存储unsize
类型的值或者将它们作为函数参数传递。你只能通过引用来处理他们,例如&str
和Box<dyn Write>
,这些引用本身是sized
类型。一个指向unsized
值的指针是一个胖指针,它们占两个word宽,前者指向一个切片地址并同时附带切片的长度,后者指向一个指向特型对象地址和实现函数的虚拟表。
这里两个words的意思是常规指针使用一个word,他们使用两个words,是常规指针的两倍。
如果是切片的引用,那么它指针的第一个word是指向切片数据地址,第二个word是指向切片长度。
如果是特型对象的引用,那么它指针的第一个word是指向实现特型的值的地址,第二个word是指向虚拟函数表(vtable)。
特型对象和切片是有些类似的,他们都缺乏使用自身的必要信息。例如你无法在不知道切片长度的时候去索引切片,或者说你在无法知道具体特型对象具体实现的时候去调用特型对象的方法。这两种情况下,都使用胖指针来补充底层类型所缺乏的信息,例如切片长度或者虚拟函数表。缺失的静态信息被动态信息替代。
由于unsized
类型受到较大限制,因此平常使用泛型函数时应该严格约束T:Sized
。实际上,这种约束太常见了,以致Rust把它作为了一个默认隐式实现。如果你写struct S<T>
,Rust知道你的意思其实为struct S<T:Sized>
。如果你真实意思并不是这样,你必须手动写出如下定义struct S<T: ?Sized>
。 这里?Sized
语法代表可以/可能不是Sized
类型。例如你写了struct S<T:?Sized>
,你就可以使用S<str>
或者S<dyn Write>
。此时S<str>
或者S<dyn Write>
的Box就是一个胖指针,而S<i32>
或者S<String>
的Box就是普通指针。
这里最后一句的意思是如果你的类型中使用/拥有/包含了unsized
类型,那个你的类型也是unsized
的,使用它时只能通过引用,此时引用为一个胖指针。
这里如果一个类型中同时包含了两个unsized
类型的值,那么它的Box是一个胖指针能搞定的么?
这里结构体只有最后一个字段才能是unsized
类型,所以一个结构体无法拥有两个unsized
值,所以一个胖指针仍然能搞定。
尽管有这些限制,unsized
类型使得Rust的类型系统更丝滑。阅读标准库文档,你会经常发现类型变量的约束为?Sized
,这通常意味着该类型的值只是被指向,因此允许相关代码可以和处理普通值一样处理切片和特型对象。
这句话的意思是如果类型变量约束为默认的Sized
,你就只能处理普通值而无法处理切片和特型对象了。所以使用?Sized
既可以处理普通值,也可以处理切片和特型对象,提高了灵活性。
除了切片类型和特型对象,这里还有一类unsized
类型。如果一个结构体的最后一个字段(只有最后一个字段)是无固定大小类型,那么这个结构就是无固定大小类型。如下例:
rust
struct RcBox<T: ?Sized> {
ref_count: usize,
value: T,
}
上面的代码用来实现类似引用计数的一个功能。它的具体实现是使用一个结构体存储引用计数和类型T的值。当然上面的代码是简化版本。上述结构体的value
字段的类型是T,Re<T>
是可计数的引用,Rc<T>
解引用一个指针到该字段,ref_count
字段记录引用的数量。
真实的RcBox
是标准库的一个内部实现细节,因此无法被外部使用。但是我们假定一下使用我们前面的定义,你可以在sized
类型上使用RcBox
,例如RcBox<String>
,这样该结构也是一个有固定大小的结构类型。你也可以在RcBox
上应用unsized
类型,例如RcBox<dyn std::fmt::Display>
,这样,RcBox<dyn Display>
就是一个unsized
结构类型。
你无法直接建立一个RcBox<dyn Display>
值,间接的方法是,你先创建一个普通的,sized RcBox
,它的value字段的类型是一个实现了Display
特型的类型,例如String
。因此,这个临时值为RcBox<String>
,然后Rust可以上你从它的普通引用&RcBox<String>
转化成一个胖指针引用&RcBox<dyn Display>
,示例如下:
rust
let boxed_lunch: RcBox<String> = RcBox {
ref_count: 1,
value: "lunch".to_string()
};
use std::fmt::Display;
let boxed_displayable: &RcBox<dyn Display> = &boxed_lunch;
这种转换可以在把值传递给函数时隐式发生,所以你可以将&RcBox<String>
传递给一个接收&RcBox<dyn Display>
参数的函数。
rust
fn display(boxed: &RcBox<dyn Display>) {
println!("For your enjoyment: {}", &boxed.value);
}
display(&boxed_lunch);
它会产生如下输出
bash
For your enjoyment: lunch
总结:
- Sized 类型代表在编译时知道其固定大小的类型,Rust中绝大多数类型为
Sized
类型。Unsized
类型则相反,主要有切片类型,特型对象以及Unsized Struct
类型。 - 结构体只有最后一个字段为
unsized
时才整体为unsized struct
,如果不是最后一个字段,不允许出现unsized
类型。 - 涉及到特型对象时,有时不能直接建立某个
unsized type
的值,此时可以先创建一个普通值,然后将普通引用转换为涉及特型对象的胖指针引用。 - ?Sized用在泛型函数的类型变量约束中,它代表可以是
Sized
,也可以是Unsized
。还有一种!Sized
几乎不会见,代表UnSized
。Rust自动为所有泛型中的类型变量约束实现了T:Sized
,所以平常可以不写略过。