Rust Tips No.2 Sized,?Sized

169 阅读6分钟

我想聊的内容基本全部基于Rust的基本库,应该说大部分聊的是core库。

本期聊的内容是Sized,这是一个平时基本用不到,但是最基础的概念。

Rust基本库

Rust的基本库分为三层,core,alloc和std。

  1. core包含rust最核心的内容,该库除了编译器不依赖任何外部环境,包括堆内存。

  2. alloc在core基础上添加堆内存的支持,从名字就可以看出来,该库还依赖compiler_builtins,一个公开的库,在crates.io上有,可以查看


impl<T> Box<T> {

pub fn new(x: T) -> Self {

#[rustc_box]

Box::new(x)

}

}

所有的堆分配都会最终到这里,分配堆内存。

我们熟悉的Box,Vec都是在这个alloc库中。

  1. std 标准库,依赖alloc,和许多其他的库,比如现在标准库HashMap就是依赖hashbrown库实现的,就是crates.io上那个。

具体来说是std依赖于alloc,alloc依赖于core,core只依赖编译器。

一般来说写应用是使用std,写嵌入式一般只依赖alloc,如果是写内核,那就只能依赖core了。

marker

在core库中有一个叫marker的mod,里面包含的内容主要有:

  • Sized,Copy,Send,Sync,Unpin五个Trait

  • PhantomData,PhantomPinned两个Struct

  • Derive Copy的宏。

为什么这些内容在这个mod中呢?这个mod中的内容与编译器的行为息息相关,我们看到的每一个Trait,它们的大小都是零,目的只是给所有的类型添加一个开关。

为什么叫marker,因为对于每一个类型,以上五个Trait编译的时候都能得到是或者否的答案。

这些内容我们大概可以分为四个部分

  1. Sized

  2. Copy,Derive Copy

  3. Send,Sync,PhantomData

  4. Unpin,PhantomPinned

我会逐步的聊这几个部分,本期先聊Sized。

数据与内存

我们使用的所有变量,都是数据,数据要保存在内存中,在保存的时候都需要一个大小,就是占多大的内存,这个大小编译器会自动计算,对于能够计算出大小的类型,都会标注为Sized。

也就是是说编译器自动为所有能够计算大小的类型实现了该Trait。


struct A(u32);

let a1:A;

let a2:&A;

上面的a1,a2是两种不同的类型,编译器在编译的时候能够计算到它们的长度,所以每一个都自动实现了Sized。

也就是说编译器自动为我们做了下面的事情。


impl Sized for A;

impl Sized for &A;

是的,A,&A完完全全是两个东西,但是为啥我们在a1或者a2 上都可以通过 . 来调用它们的函数呢?这个涉及到Deref,刨坑待填。

所以Sized 是一个开关,编译器自动帮所有类型确定,那么既然是开关,开就是有固定长度,关就是没有固定长度,那么什么是没有固定长度呢?

DST(dynamically sized types)

在Rust中,在编译的时候不能确定长度的类型,有一个专有名词叫DST。

我们日常的开发一直在与DST打交道,Rust中DST主要是下面这三种:

  1. str

  2. [T]

  3. dyn Trait

编译的时候这三种类型的大小是无法确定的,所以它们的Sized是关闭的,没有实现,所以很多代码我们是编译不过的,例如下面这种:


let a1: str = "test";

let a2: [u32] = [0u32;1];

let a3: dyn AsRef<str> = String::from("test");

那么DST怎么用呢?我们只能定义具有Sized Trait的变量,SDT没有,但是对DST的引用有,所以像下面这样就可以编译。


let a1: &str = "test";

let a2: &[u32] = &[0u32; 1];

let a3: &dyn AsRef<str> = &String::from("test");

动态类型没有大小无法实例化,动态类型的引用有大小,所以可以实例化。

那么编译器是怎么识别以上的错误的呢?

编译器的做法很简单,就是给所有需要类型的地方都加上Sized的需要,比如:


struct A<T> {

t: T,

}

// 编译器眼里是这样的

struct A<T:Sized>{

t:T,

}

// 所以

let a:A<u32>; // 可以

let b:&A<u32>;// 可以

let c:A<str>; // 不可以

let d:&A<str>;// 不可以

那么既然Sized是一个开关,能不能手动关闭呢?

答案是不能。Sized是编译器计算出来的,我们不能让一个有固定长度的类型让它没有长度,反之亦然。

但是有一种情况,是可以调整的,就是上面情况中的 d 变量。

?Sized

范型的参数我们可以理解为是一种类型过滤器,就是A结构体可以接受除了DST以外的所有类型,那么我们可不可以把这个除DST以外这个条件也去掉呢?

就是所有的类型 A 结构体都可以接受。

答案是可以的。


struct A<T:?Sized> {

t: T,

}

let a:A<u32>;

let b:&A<u32>;

// let c:A<str>; // 依旧不可以

let d:&A<str>; // 现在可以了

这种情况我们日常用的不多,但是标准库的有些类型是这样设计的,比如Borrow,Cell,


pub struct Cell<T: ?Sized> {

value: UnsafeCell<T>,

}

  


let a:&Cell<str>; // 是的,你可以这样写。

一些包含借用的结构体,都是包含这个?Sized,比如后面讲的Pin,实现中是必须有这个标志才能实现。

Sized在Trait定义中的另外一种用法

比如我们要建立一个界面库,可能需要这么写:


trait View {

fn clone(&self)->Self;

fn show(&self);

}

let v:&dyn View; // 不可以

这里不可以的原因出在这个clone函数上,当建立dyn View这个DST的时候,需要建立类似C++的虚表,而虚表中无法添加这个clone函数。

因为我们无法在编译的时候知道clone的返回值的大小。

这个代码要编译通过需要这么写:


trait View {

fn clone(&self)->Self where Self:Sized;

fn show(&self);

}

let v:&dyn View;

建立虚表的时候要求&self是DST,这里的逻辑是手动给Self加一个Sized的过滤器,把这个函数从虚表中过滤掉。

这样做的结果可以编译过了,虚表中只有show这个函数,没有clone函数了,这样就能编译过了。

当然附带的结果是v.clone()无法调用了。

总结

Sized就是一个开关,Rust编译器会自动帮我们在类型上打开或者关闭这个开关,在特殊的情况下,我们可以通过代码手动的打开或者关闭这个开关的过滤器来实现具体的目的。

Rust中有许多的逻辑都是通过空的Trait这样的开关来过滤和检查某些合法性,这样在编译器就可以检查出程序不合理的地方。

最后补上Sized的定义:


pub trait Sized {

// Empty.

}

是的,空空如也,什么都没有,在编译的时候它存在,在运行的时候它不存在,对程序的性能的影响是零,也是Rust实现零成本抽样固定套路。

下一期讨论Copy,一个与Sized套路一样,但是逻辑完全相反的故事。