这一节我们介绍一些比较高级的类型特性,包括上一节讲到的newtype模式、类型别名、never类型、动态大小类型
使用newtype模式实现类型安全与抽象
上一节中我们使用newtype模式跳过了”孤儿规则“的限制,我们还可以使用newtype模式可以为类型的某些细节进行封装。例如,newtype可以暴露出一个与内部私有类型不同的公共API,从而限制用户可以访问的功能,下面实现一个只能添加成员的MyVec结构体:
struct MyVec<T>(Vec<T>);
impl<T> MyVec<T> {
fn new() -> Self {
MyVec(vec![])
}
// 只实现添加,不提供删除方法,所以不能删除
fn push(&mut self, item: T) {
self.0.push(item)
}
}
let mut arr = MyVec::new();
arr.push(1);
arr.push(2);
arr.push(3);
newtype模式还可以被用来隐藏内部实现。例如,我们可以提供People类型来封装一个用于存储人物ID及其名称的HashMap<u32,String>。People类型的用户只能使用我们提供的公共API,比如一个添加名称字符串到People集合的方法,而调用该方法的代码不需要了解我们在内部赋予了名称一个对应的ID,未来ID生成规则我们可以随意改变,而不会影响到使用者:
use std::collections::HashMap;
struct People(HashMap<u32, String>);
impl People {
fn new() -> Self {
People(HashMap::new())
}
fn add(&mut self, name: String) {
// 根据名字简单生成一个id
let id: u32 = name.as_bytes().iter().map(|&x| x as u32).sum();
// 存入到HashMap
self.0.insert(id, name);
}
}
let mut people = People::new();
people.add(String::from("xiaoming"));
// {860: "xiaoming"}
使用类型别名创建同义类型
使用过TS的同学一定知道,使用type关键字可以为现有的类型生成另外的名称:
type Kilometers = u32;
let x: u32 = 5;
let y: Kilometers = 6;
println!("{}", x + y);
// 11
类型别名最主要的用途是减少代码字符重复:
type Thunk = Box<dyn Fn()>;
// 1. Thunk作为参数
fn takes_long_type(f: Thunk) {
f()
}
let f: Thunk = Box::new(|| println!("hi"));
takes_long_type(f); // "hi"
// 2. Thunk作为返回值
fn returns_long_type() -> Thunk {
Box::new(|| println!("hello"))
}
let f2 = returns_long_type();
f2(); // "hello"
对于Result<T, E>类型我们常常使用类型别名来减少代码重复,比如在std::io模块中的方法在返回值中返回Result<T, E>处理失败:
use std::io::Error;
use std::fmt;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
fn flush(&mut self) -> Result<(), Error>;
fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}
我们使用类型别名来处理上面重复出现的Result<..., Error>:
// 因为所有的E都是std::io::Error类型,
// 而T在不同的方法中返回的类型是不同的,
// 所以我们只需要把T类型作为类型参数传入即可
type Result<T> = std::result::Result<T, std::io::Error>;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}
永不返回的Never类型
rust有一个名为!的特殊类型,它在类型系统中的术语为空类型(empty type),因为它没有任何的值。我们倾向于叫它never类型,因为它在从不返回的函数中充当返回值的类型,比如下面函数bar永远不会返回值:
fn bar() -> ! {
}
continue的返回类型也是!:
let mut x = 0;
loop {
let y: u32 = if x == 1 {
x
} else {
x += 1;
continue;
};
println!("y: {}", y);
}
上面代码会陷入死循环,这不是重点,我们看y的类型是u32,if分支中的x类型正确,而continue的返回值是!,这里的重点是类型!的表达式可以被强制转换为其他的任意类型,所以允许u32作为y的类型,不然肯定报错了。
另外,panic!宏的实现使用了never类型,这里以Option<T>的unwrap函数为例:
impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"),
}
}
}
上面代码中编译器知道val是T类型,panic!是!类型,这里!被转换为T类型,所以整个match表达式的结果是T类型。
loop的返回类型是!:
let x /* ! */ = loop {
print!("loop");
};
// 如果loop中存在break,那么x的类型就是空元祖: ()
let x /* () */ = loop {
break;
};
上面x变量后的注释中是被编译器推断的类型,可以在vscode编辑器里看到,大家可以去尝试下
动态大小类型和Sized trait
rust在编译时必须知道所有类型的大小,而类型系统中存在动态类型大小的概念,这些类型只有在运行时才能知道大小
str类型
str就是一个动态大小类型。只有在运行时才能确定字符串的长度,所以无法创建一个str类型的变量,或者使用str类型来作为函数的参数:
let s1: str = "abc"; // 报错,在编译时不知道s1大小
let s2: str = "abcd"; // 报错,在编译时不知道s2大小
fn foo(s3: str) { // 报错,在编译时不知道s3的大小
}
rust在编译阶段会根据类型分配内存,如果每个str拥有相同的内存,那么上面的s1和s2应该拥有等量的内存,但实际上两个字符长的长度是不同的,我们一般使用指针来解决str类型的问题:
let s1: &str = "abc";
let s2: &str = "abcd";
fn foo(s3: &str) {
}
将str改为引用类型&str就可以编译通过了,原因在于每一个引用的大小是固定的,都各自包含一个指向数据在内存中的起始位置和数据的长度
除了&引用以外,使用智能指针也可以在编译期间确定大小:
use std::rc::Rc;
let b: Box<str> = Box::from("abc");
let r: Rc<str> = Rc::from("abc");
Sized trait
rust还提供了一个特殊的Sized trait来确定一个类型的大小在编译时是否可知,编译时可计算出大小的类型会自动实现这个trait,rust还会为每一个泛型函数隐式地添加Sized约束:
fn generic<T>(t: T) {
}
// 编译后
fn generic<T: Sized>(t: T) {
}
泛型函数默认只能用于在编译时已知大小的类型。可以在Sized前面加?来放宽这个限制:
fn generic<T: ?Sized>(t: &T) {
}
?Sized的意思是:不确定T是不是Sized的。这个语法只能被用在Sized上,而不能被用于其他trait。另外,参数t类型由T修改为了&T。因为t类型可能不是Sized的,所以我们需要将它放置在某种指针的后面。在上面使用了引用,当然也可以智能指针。
封面图:跟着Tina画美国
关注「码生笔谈」公众号,阅读更多最新章节