Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

19.5 高级类型

19.5.1 使用 newtype 模式实现类型安全和抽象

19.2. 高级 trait:关联类型、默认泛型参数和运算符重载、完全限定语法、supertrait 和 newtype 中,我们已经使用 newtype 模式为 Vec 实现了 Display(更具体地说,是在 19.2.6,使用 newtype 模式在外部类型上实现外部 trait)。

19.2. 高级 trait:关联类型、默认泛型参数和运算符重载、完全限定语法、supertrait 和 newtype(19.2.2「默认泛型参数和运算符重载」)中,我们还写过 MillimetersMeters 结构体,分别存储毫米和米的值。因为这两个值不能直接加减,这就避免了单位被误混用。

我们还可以用 newtype 模式抽象其它特性:

19.5.2 类型别名

Rust 提供了声明类型别名的能力,从而给现有类型另一个名字,这有点像泛型。

要使用类型别名,使用 type 关键字。例如:

#![allow(unused)]
fn main() {
type Kilometers = i32;
}

我们把 Kilometers 称为 i32同义词。你可以像使用 i32 一样使用 Kilometers

fn main() {
    type Kilometers = i32;

    let x: i32 = 5;
    let y: Kilometers = 5;

    println!("x + y = {}", x + y);
}
  • 因为 Kilometersi32 是相同类型,所以我们可以把这两种类型的值相加。

类型同义词的主要用例是减少重复。例如,我们可能有这样一个很长的类型:

#![allow(unused)]
fn main() {
Box<dyn Fn() + Send + 'static>
}

在整个代码库的函数签名和类型标注中反复写这么长的类型,既繁琐又容易出错。例如:

#![allow(unused)]
fn main() {
    let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));

    fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
        // ...
    }

    fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
        // ...
    }
}

类型别名通过减少重复,让这段代码更容易管理,而且有意义的名字更能表达意图。我们可以把上面的代码改写成:

#![allow(unused)]
fn main() {
    type Thunk = Box<dyn Fn() + Send + 'static>;

    let f: Thunk = Box::new(|| println!("hi"));

    fn takes_long_type(f: Thunk) {
        // ...
    }

    fn returns_long_type() -> Thunk {
        // ...
    }
}

类型别名也经常与 Result<T, E> 一起使用,以减少重复。例如:

#![allow(unused)]
fn main() {
use std::fmt;
use std::io::Error;

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>;
}
}

I/O 操作通常返回 Result<T, E> 来处理失败。std::io::Error 表示所有可能的 I/O 错误。std::io 中的许多函数返回 Result<T, E>,其中 Estd::io::Error

Result<..., Error> 重复了很多次,所以 std::io 使用了类型别名:

#![allow(unused)]
fn main() {
type Result<T> = std::result::Result<T, std::io::Error>;
}

于是 Write trait 的方法签名看起来像这样:

#![allow(unused)]
fn main() {
use std::fmt;

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<()>;
}
}

类型别名在这里有两个作用:

19.5.3 Never 类型

Rust 有一个特殊类型叫 !。在类型理论中,它被称为空类型,因为它没有值。我们更喜欢称它为 never 类型,因为它出现在函数的返回类型位置。

例如:

#![allow(unused)]
fn main() {
fn bar() -> ! {
	
}
}

这段代码的含义是:“函数 bar 永不返回。”永不返回的函数叫做发散函数

那么 never 类型有什么用?我们用 2.4. 猜数游戏Pt.4 循环询问 中的一段代码:

#![allow(unused)]
fn main() {
let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};
}

这样写没问题。但如果我们写成这样呢?

#![allow(unused)]
fn main() {
let guess = match guess.trim().parse() {
    Ok(_) => 5,
    Err(_) => "hello",
};
}

这段代码会失败,因为 match 的两个分支返回了不同类型。Rust 是强类型语言,所以必须知道值的确切类型。guess 可能是 i32&str,但 Rust 要求 guess 只能有一种类型。

换句话说,在这种形式下,match 的所有分支都必须返回相同类型

再回看正确的代码:Ok 分支返回 u32,那 Err 分支中的 continue 返回什么类型?如果它是单元类型 (),也就是没有返回值,Rust 就无法判断 guess 究竟是 u32 还是 ()

这就是 never 类型发挥作用的地方:continue 的返回类型是 !。换句话说,当 Rust 检查 guess 的类型时,它会查看两个 match 分支。第一个分支返回 u32,第二个分支返回 !。因为 ! 永远不会产生值,Rust 就知道 guessu32

never 类型对 panic! 宏的作用方式相同。看看 unwrap 的定义:

#![allow(unused)]
fn main() {
impl<T> Option<T> {
    pub fn unwrap(self) -> T {
        match self {
            Some(val) => val,
            None => panic!("called `Option::unwrap()` on a `None` value"),
        }
    }
}
}

Rust 看到 val 的类型是 T,而 panic! 的类型是 !,所以整个 match 表达式的返回值是 T。之所以可行,是因为 panic! 不返回值;它会结束程序。

实际上,loop 也是 !,因为无限循环永不结束,所以永远不会产生返回值。然而,如果包含 break,情况就不再如此,因为循环到达 break 时会终止。

19.5.4 动态大小类型与 Sized trait

Rust 需要了解其类型的某些细节,例如为特定类型的值分配多少空间。这使得动态大小类型这个概念有点令人困惑。它们有时被称为 DSTunsized types。这些类型让我们可以编写处理大小仅在运行时才知道的值的代码。

我们可以用 str(不是 &str,也不是 String)作为动态大小类型的例子:

#![allow(unused)]
fn main() {
let s1: str = "Hello there!";
let s2: str = "How's it going?";
}

在运行时之前我们无法知道字符串有多长,这意味着我们无法创建类型为 str 的变量,所以上面的代码无法工作。

Rust 需要知道为特定类型的任何值分配多少内存,并且同一类型的所有值必须使用相同数量的内存。如果 Rust 允许我们写上面的代码,那么这两个 str 值就必须占用相同空间。但它们长度不同:s1 需要 12 字节存储,而 s2 需要 15 字节。这就是我们无法创建存储动态大小类型的变量的原因。

那该怎么办?一般来说,把 s1s2 的类型改成 &str 而不是 str,就能解决问题:

#![allow(unused)]
fn main() {
let s1: &str = "Hello there!";
let s2: &str = "How's it going?";
}

切片数据结构只存储切片的起始位置和长度。所以虽然 &T 是包含一个内存地址的单个值,但 &str 是两个值(如 4.5. 切片(Slice) 中讨论的):

  • str 的地址(usize
  • str 的长度(usize

因此,我们可以在编译时知道 &str 值的大小:它是 usize 大小的两倍。换句话说,无论它引用的字符串有多长,我们总是知道 &str 的大小。

一般来说,在 Rust 中使用动态大小类型的最佳方式,是给它们额外的元数据来存储动态大小信息。动态大小类型的黄金法则是:我们必须始终把它们放在某种指针后面

我们可以把 str 与各种指针组合,例如 Box<str>Rc<str>。Trait 实际上也是动态大小类型。为了处理动态大小类型,Rust 提供了 Sized trait 来判断类型的大小在编译时是否已知。所有在编译时大小已知的类型都会自动实现这个 trait。此外,Rust 会隐式给每个泛型函数加上 Sized trait。

这意味着像这样的泛型函数:

#![allow(unused)]
fn main() {
fn generic<T>(t: T) {
    // ...
}
}

实际上被写成:

#![allow(unused)]
fn main() {
fn generic<T: Sized>(t: T) {
    // ...
}
}

默认情况下,泛型函数只适用于编译时大小已知的类型。 但我们可以用特殊的 ?Sized 语法放宽这个限制:

#![allow(unused)]
fn main() {
fn generic<T: ?Sized>(t: &T) {
    // ...
}
}
  • ?Sized 表示“T 可能实现也可能不实现 Sized trait”,也就是说 T 可能是也可能不是动态大小类型。这个记号移除了“泛型类型在编译时必须有已知大小”这一默认要求。?Trait 语法只适用于 Sized trait,不适用于其它 trait。
  • 我们把参数 t 的类型从泛型 T 改成了 &T。因为该类型可能没有实现 Sized,也就是可能是动态大小类型,所以我们需要用指针包裹动态大小类型。

使用动态大小类型的最佳场景是与 trait 配合。有时我们希望某些数据实现某些 trait 或某个生命周期,但不知道具体类型,这时就可以使用指针包裹的动态类型。例如:

#![allow(unused)]
fn main() {
type Job = Box<dyn FnOnce() + Send + 'static>;
}

这个例子同时使用了类型别名指针包裹的动态类型Job 可以是任何实现了 FnOnce()Send 以及 'static 生命周期的类型。