Rust 的类型系统有一些我们曾经提到但没有讨论过的功能。首先我们从一个关于为什么 newtype 与类型一样有用的更宽泛的讨论开始。接着会转向

  1. newtype
  2. 类型别名(type aliases)
  3. 一个类似于 newtype 但有着稍微不同的语义的功能
  4. ! 类型
  5. 动态大小类型

    tuple struct:newtype

    newtype 模式是为了类型安全和抽象而使用的。比如:

  6. 静态的确保某值(以及类型)不被混淆、用来表示一个值的单元。比如重载 + 运算符时,”对不同单位的值相加” 的例子:MillimetersMeters 结构体都在 newtype 中封装了 u32 值。如果编写了一个有 Millimeters 类型参数的函数,不小心使用 Meters 或普通的 u32 值来调用该函数的程序是不能编译的。

  7. 抽象掉一些类型的实现细节:例如,封装类型可以暴露出与直接使用其内部私有类型时所不同的公有 API,以便限制其功能。
  8. 安全地“打破”孤儿原则:间接地为外部类型实现外部 trait,而且不影响运行时效率。

下面这个例子同时反映了后两个场景,同时也说明 newtype 利用 Rust 类型系统的抽象,可以以优雅的方式完成很多复杂的事情:

  1. // crate name: my_crate
  2. // src/lib.rs
  3. use std::collections::{hash_map::Values, HashMap};
  4. use std::ops::{Deref, DerefMut};
  5. pub struct PeopleUnencapsulated(HashMap<u32, String>);
  6. // 未封装的 newtype:利用 Deref、DerefMut trait,
  7. // 可以直接把 PeopleUnencapsulated 当作 HashMap<u32, String> 使用
  8. impl PeopleUnencapsulated {
  9. pub fn new() -> Self { Self(HashMap::new()) }
  10. pub fn names(&self) -> Values<u32, String> { self.0.values() }
  11. }
  12. impl Deref for PeopleUnencapsulated {
  13. type Target = HashMap<u32, String>;
  14. fn deref(&self) -> &Self::Target { &self.0 }
  15. }
  16. impl DerefMut for PeopleUnencapsulated {
  17. fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
  18. }
  19. // 隐藏实现细节的封装:只暴露给使用者有用的 API
  20. pub struct People(HashMap<u32, String>);
  21. impl People {
  22. pub fn new() -> Self { Self(HashMap::new()) }
  23. pub fn names(&self) -> Values<u32, String> { self.0.values() }
  24. pub fn insert(&mut self, id: u32, name: String) -> Option<String> { self.0.insert(id, name) }
  25. }
  1. // src/main.rs
  2. use my_crate::newtype::{People, PeopleUnencapsulated};
  3. fn main() {
  4. // 打破孤儿原则
  5. let mut p = PeopleUnencapsulated::new();
  6. p.insert(0, "Alice".to_string());
  7. println!("{:?}", p.names());
  8. println!("{:?}", p.values());
  9. // 对类型进行轻量级封装
  10. let mut p = People::new();
  11. p.insert(0, "Anna".to_string());
  12. println!("{:?}", p.names());
  13. // not working anymore:
  14. // println!("{:?}", p.values());
  15. }
  16. // 打印结果:
  17. // ["Alice"]
  18. // ["Alice"]
  19. // ["Anna"]

type:type alias

类型别名type alias ):使用 type 关键字来给予现有类型另一个名字。

  1. 现有类型和它的类型别名是同一类型
  2. 主要用途是减少重复,让长内容的类型易于编写 在整个项目中提供一致的接口 ``` type Kilometers = i32; type Thunk = Box<dyn Fn() + Send + ‘static>; type Result = std::result::Result; // 这个定义已经存在于 std::io::Result

// 这意味着可以在其上使用 Result 的任何方法,以及像 ? 这样的特殊语法 pub trait Write { fn write(&mut self, buf: &[u8]) -> Result; fn flush(&mut self) -> Result<()>;

  1. fn write_all(&mut self, buf: &[u8]) -> Result<()>;
  2. fn write_fmt(&mut self, fmt: Arguments) -> Result<()>;

}

  1. `Self` 可以看做所在 `impl` 块的类型别名:

enum VeryVerboseEnumOfThingsToDoWithNumbers { Add, Subtract, }

impl VeryVerboseEnumOfThingsToDoWithNumbers { fn run(&self, x: i32, y: i32) -> i32 { match self { Self::Add => x + y, Self::Subtract => x - y, } } }

  1. # `!`:never type
  2. `!` 是一个特殊类型,在类型理论术语中,它被称为 _empty type_ ,因为它没有值。Rust 中更倾向于称之为 _never type_ 。<br />never type 的作用:在函数从不返回的时候充当返回值(实际上并没有值,可以认为是占位的返回值)。从不返回给调用者的函数被称为 **发散函数** _diverging functions_

![feature(never_type)]

fn main() { let x: ! = panic!(“This call never returns.”); println!(“You will never see this line!”); }

  1. 从另一个角度看, never type 可以强转为任何其他类型。<br />never type 的使用场景:循环、`panic!`、终止进程
  2. 1. `loop``continue` 的值是`!`。<br />
  3. 在下面的例子中,`Ok`返回`u32`类型的值,而`Err``!`类型,不返回值。因为`!`并没有一个值,Rust 决定`guess`的类型是`u32`。(`match` 的分支必须返回相同的类型)

![allow(unused)]

fn main() { let guess = “3”; loop { let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; break; } }

  1. 这里,循环永远也不结束,所以此 loop 表达式的值是 `!`

print!(“forever “);

loop { print!(“and ever “); }

  1. 注意:`break` 不是 `!`,它通常放在 loop 中进行终止循环,使用方法:`break;` 从循环中返回 `()` unit 类型;`break expr;` 带表达式的话返回表达式的类型与值。
  2. 2. `panic!` 的值是 `!`。逻辑同上:Rust 知道 `val` `T` 类型,`panic!` `!` 类型,所以整个 `match` 表达式的结果是 `T` 类型。这能工作是因为 `panic!` 并不产生一个值;它会终止程序。对于 `None` 的情况,`unwrap` 并不返回一个值,所以这些代码是有效。

impl Option { pub fn unwrap(self) -> T { match self { Some(val) => val, None => panic!(“called Option::unwrap() on a None value”), } } }

  1. 3. 终止进程的功能:`exit()` ,其值是 `!`
  2. # DST 与 `Sized` trait
  3. **动态大小类型** _dynamically sized types_ ,或“DST unsized types”):允许我们处理只有在运行时才知道大小的类型。<br />Rust 需要知道应该为特定类型的值分配多少空间,同时所有同一类型的值必须使用相同数量的内存。如果类型是动态的(编译时无法确定具体类型),其内存空间如何分配呢?<br />让我们深入研究一个贯穿本书都在使用的动态大小类型的细节:`str`,注意不是 `&str`,而是 `str` 本身。<br />`str` 是一个 DST;直到运行时我们都不知道字符串有多长。因为直到运行时都不能知道大其小,也就意味着不能创建 `str` 类型的变量,也不能获取 `str` 类型的参数。

// 报错: let s1: str = “Hello there!”; let s2: str = “How’s it going?”;

  1. 如果允许编写这样的代码,也就意味着这两个 `str` 需要占用完全相同大小的空间,不过它们有着不同的长度。这也就是为什么不可能创建一个存放动态大小类型的变量的原因。解决方式就是:**把 DST 放置在指针后面**,因为指针的大小总是固定的。

// 报错: let s1: &str = “Hello there!”; let s2: &str = “How’s it going?”;

  1. 此时 `s1` `s2` 的类型是 `&str`,也就是 slice 类型。slice 数据结储存了开始位置和 slice 的长度 —— 所以 `&str` 则是 _两个_ 值(额外的元信息):`str` 的地址和其长度,于是 Rust 在编译时可以知道的大小:它是 `usize` 长度的两倍。也就是说,我们总是知道 `&str` 的大小,而无论其引用的字符串是多长。<br />由此我们可以理解:
  2. -
  3. Rust 中动态大小类型 DST 的常规用法:他们有一些额外的元信息来储存动态信息的大小。
  4. -
  5. 动态大小类型 DST 的黄金规则:必须将动态大小类型的值置于某种指针之后(或者说 **DST 必须结合指针**)。这里的指针包括 `&``Box``Rc` 等等任何类型的指针。
  6. -
  7. 此外,每一个 trait 都是一个可以通过 trait 名称来引用的动态大小类型。将 trait 用于 trait 对象时,必须将他们放入指针之后,比如 `&dyn Trait` `Box<dyn Trait>``Rc<dyn Trait>`
  8. ---
  9. `Sized` trait
  10. 1. 是为了处理 DSTRust 用来决定一个类型的大小在编译时可知的 trait。编译器在编译时给知道大小的类型 自动实现 `Sized` trait
  11. 1. Rust 隐式的为每一个泛型函数增加了 `Sized` bound`fn generic<T>(t: T) { }` 实际上被当作 `fn generic<T: Sized>(t: T) { }` 处理。
  12. 1. 泛型函数默认只能用于在编译时已知大小的类型。然而可以使用如下特殊语法来放宽这个限制:

fn generic(t: &T) { // —snip— }

`` ?Sizedtrait bound 表示“T可能是也可能不是Sized的”。这个语法只能用于Sized,而不能用于其他 trait。<br />另外注意我们将t参数的类型从T变为了&T:因为其类型可能不是Sized` 的,所以需要将其置于某种指针之后。在这个例子中选择了引用。