ch19-04-advanced-types.md
commit a90f07f1e9a7fc75dc9105a6c6f16d5c13edceb0
Rust 的类型系统有一些我们曾经提到但没有讨论过的功能。首先我们从一个关于为什么 newtype 与类
型一样有用的更宽泛的讨论开始。接着会转向类型别名(type aliases),一个类似于 newtype 但有着稍
微不同的语义的功能。我们还会讨论 ! 类型和动态大小类型。
这一部分假设你已经阅读了之前的 ”newtype 模式用于在外部类型上实现外部 trait” 部分。
为了类型安全和抽象而使用 newtype 模式
newtype 模式可以用于一些其他我们还未讨论的功能,包括静态的确保某值不被混淆,和用来表示一个
值的单元。实际上示例 19-15 中已经有一个这样的例子:Millimeters 和 Meters 结构体都在 newtype
中封装了 u32 值。如果编写了一个有 Millimeters 类型参数的函数,不小心使用 Meters 或普通的 u32
值来调用该函数的程序是不能编译的。
另一个 newtype 模式的应用在于抽象掉一些类型的实现细节:例如,封装类型可以暴露出与直接使用其
内部私有类型时所不同的公有 API,以便限制其功能。
newtype 也可以隐藏其内部的泛型类型。例如,可以提供一个封装了 HashMap<i32, String> 的 People
类型,用来储存人名以及相应的 ID。使用 People 的代码只需与提供的公有 API 交互即可,比如向 People
集合增加名字字符串的方法,这样这些代码就无需知道在内部我们将一个 i32 ID 赋予了这个名字了。newtype 模式是一种实现第十七章 ” 封装隐藏了实现细节” 部分所讨论的隐藏实现细节的封装的轻量级方法。
类型别名用来创建类型同义词
连同 newtype 模式,Rust 还提供了声明 类型别名(type alias)的能力,使用 type 关键字来给予现有
类型另一个名字。例如,可以像这样创建 i32 的别名 Kilometers:
fn main() {
type Kilometers = i32;
let x: i32 = 5;
let y: Kilometers = 5;
println!(“x + y = {}”, x + y);
}
这意味着 Kilometers 是 i32 的 同义词(synonym);不同于示例 19-15 中创建的 Millimeters 和 Meters
类型。Kilometers 不是一个新的、单独的类型。Kilometers 类型的值将被完全当作 i32 类型值来对待:
fn main() {
type Kilometers = i32;
let x: i32 = 5;
let y: Kilometers = 5;
println!(“x + y = {}”, x + y);
}
因为 Kilometers 是 i32 的别名,他们是同一类型,可以将 i32 与 Kilometers 相加,也可以将 Kilometers
传递给获取 i32 参数的函数。但通过这种手段无法获得上一部分讨论的 newtype 模式所提供的类型检查的好处。
类型别名的主要用途是减少重复。例如,可能会有这样很长的类型:
Box<dyn Fn() + Send + 'static>
在函数签名或类型注解中每次都书写这个类型将是枯燥且易于出错的。想象一下如示例 19-24 这样全是
如此代码的项目:
fn main() {
let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!(“hi”));
fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
// --snip–
}
fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
// --snip–
Box::new(|| ())
}
}
示例 19-24: 在很多地方使用名称很长的类型
类型别名通过减少项目中重复代码的数量来使其更加易于控制。这里我们为这个冗长的类型引入了一个
叫做 Thunk 的别名,这样就可以如示例 19-25 所示将所有使用这个类型的地方替换为更短的 Thunk:
fn main() {
type Thunk = Box<dyn Fn() + Send + 'static>;
let f: Thunk = Box::new(|| println!(“hi”));
fn takes_long_type(f: Thunk) {
// --snip–
}
fn returns_long_type() -> Thunk {
// --snip–
Box::new(|| ())
}
}
示例 19-25: 引入类型别名 Thunk 来减少重复
这样读写起来就容易多了!为类型别名选择一个好名字也可以帮助你表达意图(单词 thunk 表示会在之
后被计算的代码,所以这是一个存放闭包的合适的名字)。
类型别名也经常与 Result<T, E> 结合使用来减少重复。考虑一下标准库中的 std:: io 模块。I∕O 操作通常
会返回一个 Result<T, E>,因为这些操作可能会失败。标准库中的 std:: io :: Error 结构体代表了所有可
能的 I∕O 错误。std:: io 中大部分函数会返回 Result<T, E>,其中 E 是 std:: io :: Error,比如 Write trait
中的这些函数:
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>;
}
这里出现了很多的 Result <…, Error>。为此,std:: io 有这个类型别名声明:
use std::fmt;
type Result = std::result::Result<T, std::io::Error>;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}
因为这位于 std:: io 中,可用的完全限定的别名是 std:: io :: Result —— 也就是说,Result<T, E> 中
E 放入了 std:: io :: Error。Write trait 中的函数最终看起来像这样:
use std::fmt;
type Result = std::result::Result<T, std::io::Error>;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}
类型别名在两个方面有帮助:易于编写 并在整个 std:: io 中提供了一致的接口。因为这是一个别名,它
只是另一个 Result<T, E>,这意味着可以在其上使用 Result<T, E> 的任何方法,以及像 ? 这样的特殊语法。