这一节我们介绍一些比较高级的类型特性,包括上一节讲到的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
永远不会返回值:
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画美国
关注「码生笔谈」公众号,阅读更多最新章节
评论