Rust常用特型之Sized特型

在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。字符串文字值hellobig是对字符串切片str的引用,而这两个切片分别占用5和3字节。但是&str是固定大小的,它是个两字节的胖指针。同理数组切片类型[T]也是unsized,一个共享的[u8]的引用&[u8]可以指向任意大小的切片 。因为str[T]类型用于声明可变大小的值,所以他们是unsized类型。

另一类常见的unsized类型为dyn类型,也就是特型对象。特型对象是一个指向实现了某个特型的值的指针。例如,类型&dyn std::io::WriteBox<dyn std::io::Write> 是指向某些实现了Write特型的引用。

这里要澄清一下,特型对象和对特型对象的引用之间的关系就和切片及切片的引用一样,有些混淆,特型对象是指dyn write, 而&dyn Write是对特型对象的引用。前者是unsized类型,因为实现Write的对象是多种多样的,大小不固定的。而后者是Sized类型,因为它其实是一个胖指针。

Rust 并不能使用变量存储unsize类型的值或者将它们作为函数参数传递。你只能通过引用来处理他们,例如&strBox<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类型。如果一个结构体的最后一个字段(只有最后一个字段)是无固定大小类型,那么这个结构就是无固定大小类型。如下例:

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>,示例如下:

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>参数的函数。

fn display(boxed: &RcBox<dyn Display>) {
println!("For your enjoyment: {}", &boxed.value);
}
display(&boxed_lunch);

它会产生如下输出

For your enjoyment: lunch

总结:

  • Sized 类型代表在编译时知道其固定大小的类型,Rust中绝大多数类型为Sized类型。Unsized类型则相反,主要有切片类型,特型对象以及Unsized Struct类型。
  • 结构体只有最后一个字段为unsized时才整体为unsized struct,如果不是最后一个字段,不允许出现unsized类型。
  • 涉及到特型对象时,有时不能直接建立某个unsized type的值,此时可以先创建一个普通值,然后将普通引用转换为涉及特型对象的胖指针引用。
  • ?Sized用在泛型函数的类型变量约束中,它代表可以是Sized,也可以是Unsized。还有一种!Sized几乎不会见,代表UnSized。Rust自动为所有泛型中的类型变量约束实现了T:Sized,所以平常可以不写略过。
  • 9
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AiMateZero

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值