Appearance
8.2 Never Type
如果我们考虑一个类型在机器层面的表示方式,一个类型占用的 bit 位数可以决定它能携带多少信息。假设我们用 bits_of(T)
代表类型 T 占用的 bit 位数,那么 2^bits_of(T) == Cardinality(T)
。反过来,我们可以得出结论,存储一个类型需要的 bit 位数等于 bits_of(T) == log2(Cardinality(T))
。比如说,bool 类型的基数为 2
,那么在内存中表示这个类型需要的 bit 位应该为 bits_of(bool) == log2(2) == 1
,也就是 1
个 bit 就足够表达。
我们前面已经看到了,在代数类型系统中有一些比较特殊的类型。
像 unit 类型和没有成员的空 struct 类型,都可以类比为代数中的数字 1
。这样的类型在内存中实际需要占用的空间为bits_of(()) == log2(1) == 0
。也就是说,这样的类型实际上是 0
大小的类型。这样的性质有很多好处,比如,Rust 里面 HashSet 的定义方式为:
rust
pub struct HashSet<T, S = RandomState> {
map: HashMap<T, (), S>,
}
也就是说,只要把 HashMap 中存的键值对中的“值”指定为 unit 类型就可以了。这个设计和我们的思维模型是一致的。所谓的 HashSet,就是只有 key 没有 value 的 HashMap。如果我们没有真正意义上的 0
大小类型,这个设计是无法做到如此简洁的。
没有任何成员的空 enum 类型,都可以类比为代数中的数字 0
,例如:
rust
enum Never {}
这个 Never
没有任何成员。如果声明一个这种类型的变量,let e = Never::???;
,我们都不知道该怎么初始化,因为 Rust 根本就没有提供任何语法为这样的类型构造出变量。
这样的类型在 Rust 类型系统中的名字叫作 never type,它们有一些属性是其他类型不具备的:
它们在运行时根本不可能存在,因为根本没有什么语法可以构造出这样的变量;
Cardinality(Never) == 0
;考虑它需要占用的内存空间
bits_of(Never)== log2(0) == -∞
,也就是说逻辑上是不可能存在的东西;处理这种类型的代码,根本不可能执行;
返回这种类型的代码,根本不可能返回;
它们可以被转换为任意类型。
这些有什么意义呢,我们来看以下代码:
rust
loop {
...
let x : i32 = if cond { 1 } else { continue; };
...
}
我们知道,在 Rust 中,if-else 也是表达式,而且每个分支表达式类型必须一致。这种有 continue 的情况,类型检查是怎样通过的呢?如果我们把 continue 语句的类型指定为 never 类型,那么一切就都顺理成章了。因为 never 类型可以转换为任意类型,所以,它可以符合与 if 分支的类型相一致的规定。它根本不可能返回,所以执行到 else 分支的时候,接下来根本不会执行对变量 x
的赋值操作,会进入下一次的循环。如果这个分支大括号内部 continue 后面还有其他代码,编译器可以很容易判断出来,它后面的代码是永远不可能执行的死代码。一切都在类型系统层面得到了统一。
所以说,never 类型是 Rust 类型系统中不可缺少的一部分。与 unit 类型类似,一般我们用空 tuple() 代表 unit 类型,Rust 里面其实也有一个专门的类型来表示 never,也就是我们前面提到过的感叹号!
。 所谓的“diverging function”就是一个返回 never type 的函数。在早期版本中,Rust 的做法是把 diverging function 做特殊处理,使得它在分支结构表达式的类型检查能够通过,而没有把它当成真正意义上的类型。后来,有一个 RFC 1216 对这个 never type 做了完整的设计,才真正将它提升为一个类型。而且,直到编写本书时候的 1.19 版本,这个功能的完整实现也还没有完全做完。
除了在数学形式上的统一,以及显而易见的对分支结构表达式的类型检查有好处之外,一个完整的 never type 对于 Rust 还有一些其他的现实意义。下面举几个例子来说明。
场景一:可以使得泛型代码兼容 diverging function
比如,一个这样的泛型方法接受一个泛型函数类型作为参数,可是:
rust
fn main() {
fn call_fn<T, F: Fn(i32)->T> (f: F, arg: i32) -> T { f(arg) }
// 如果不把 ! 当成一个类型,那么下面这句话会出现编译错误,因为只有类型才能替换类型参数
call_fn(std::process::exit, 0);
}
场景二:更好的死代码检查
rust
let t = std::thread::spawn(|| panic!("nope"));
t.join().unwrap();
println!("hello");
如果我们有完整的 never 类型支持,那么编译器应该可以推理出闭包的返回类型是!
,而不是()
,因此 t.join().unwrap()
会产生一个!
类型,编译器因此可以检查出 println
永远不可能执行。
场景三:可以用更好的方式表达“不可能出现的情况”
标准库中有一个 trait 叫 FromStr
,它有一个关联类型代表错误:
rust
pub trait FromStr {
type Err;
fn from_str(s: &str) -> Result<Self, Self::Err>;
}
如果某些类型调用 from_str
方法永远不会出错,那么这个 Err 类型可以指定为!
。
rust
struct T(String);
impl FromStr for T {
type Err = !;
fn from_str(s: &str) -> Result<T, !> {
Ok(T(String::from(s)))
}
}
对于错误处理,我们可以让 Result 退化成没有错误的情况:
rust
use std::str::FromStr;
use std::mem::{size_of, size_of_val};
struct T(String);
impl FromStr for T {
type Err = !;
fn from_str(s: &str) -> Result<T, !> {
Ok(T(String::from(s)))
}
}
fn main() {
let r: Result<T, !> = T::from_str("hello");
println!("Size of T: {}", size_of::<T>());
println!("Size of Result: {}", size_of_val(&r));
// 将来甚至应该可以直接用 let 语句进行模式匹配而不发生编译错误
// 因为编译器有能力推理出 Err 分支没必要存在
// let Ok(T(ref s)) = r;
// println!("{}", s);
}
这里其实根本不需要考虑 Err 的情况,因为 Err 的类型是!
,所以哪怕 match
语句中只有 Ok
分支,编译器也可以判定其为“完整匹配”。