Skip to content

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 分支,编译器也可以判定其为“完整匹配”。

Released under the MIT License