高级类型
Rust 类型系统具有一些我们目前已经提到但尚未提及的功能
但被讨论过。我们将从一般性地讨论 newtype 开始,同时研究原因
new类型可用作类型。然后,我们将继续讨论类型别名,这是一个功能
类似于 newtypes,但语义略有不同。我们还将讨论
类型和动态大小的类型。!
使用 newtype 模式实现类型安全和抽象
注意:本节假设您已经阅读了前面的部分 “使用 newtype 模式来实现 external trait 类型。
newtype 模式对于我们讨论过的任务之外的任务也很有用
far,包括静态强制值永远不会混淆,以及
指示值的单位。您看到了一个使用 newtypes 来
表示示例 19-15 中的单位:回想一下 and 结构体将值包装在 newType 中。如果我们编写了一个带有
parameter 类型为
意外尝试使用值为 type 或
平原。Millimeters
Meters
u32
Millimeters
Meters
u32
我们还可以使用 newtype 模式来抽象出一些实现 details 的类型:新类型可以公开一个不同于 私有内部类型的 API。
newtypes 还可以隐藏内部实现。例如,我们可以提供一个类型来包装存储人员 ID 的 a
与他们的名称相关联。使用的代码只会与
public API,例如向集合中添加名称字符串的方法;该代码不需要知道我们为 names 分配了 ID
内部。newtype 模式是一种实现封装的轻量级方法
来隐藏实现细节,我们在“封装
隐藏实现
详细信息“部分。People
HashMap<i32, String>
People
People
i32
使用类型别名创建类型同义词
Rust 提供了声明类型别名以给出现有类型的功能
另一个名字。为此,我们使用关键字。例如,我们可以创建
别名设置为如下所示:type
Kilometers
i32
fn main() { type Kilometers = i32; let x: i32 = 5; let y: Kilometers = 5; println!("x + y = {}", x + y); }
现在,别名是 ;与我们在示例 19-15 中创建的 and 类型不同,它不是一个单独的
new 类型。具有 type 的值将被视为与
type 的值 :Kilometers
i32
Millimeters
Meters
Kilometers
Kilometers
i32
fn main() { type Kilometers = i32; let x: i32 = 5; let y: Kilometers = 5; println!("x + y = {}", x + y); }
因为 和 是相同的类型,所以我们可以将两者的值相加
类型,我们可以将值传递给接受参数的函数。但是,使用这种方法,我们没有获得类型检查的好处
我们从前面讨论的 newType 模式中得到。换句话说,如果我们
mix up 和 values 的
一个错误。Kilometers
i32
Kilometers
i32
Kilometers
i32
类型同义词的主要用例是减少重复。例如,我们 可能有一个像这样的 longy 类型:
Box<dyn Fn() + Send + 'static>
将这个冗长的类型写在函数签名中并作为类型注释 over the code 可能很烦人且容易出错。想象一下,有一个充满 代码类似于示例 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-25 中,我们引入了一个以 verbose 类型命名的别名,并且
可以用较短的 alias 替换该类型的所有用法。Thunk
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(|| ()) } }
这段代码更容易读写!为 选择有意义的名称 Type alias 也可以帮助传达您的意图(Thunk 是代码的代名词 以便稍后进行评估,因此它是 被存储)。
类型别名也常与类型一起使用,用于减少
重复。考虑 standard 库中的 module。I/O (输入输出)
作通常会返回 A 来处理作
无法正常工作。这个库有一个代表所有
可能的 I/O 错误。中的许多函数将返回 is ,例如
性状:Result<T, E>
std::io
Result<T, E>
std::io::Error
std::io
Result<T, E>
E
std::io::Error
Write
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>;
}
这重复了很多次。因此,具有此类型
alias 声明:Result<..., Error>
std::io
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<()>;
}
因为这个声明在模块中,所以我们可以完全使用
限定别名 ;即,填写为 的 a 。trait 函数签名以
如下所示:std::io
std::io::Result<T>
Result<T, E>
E
std::io::Error
Write
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<()>;
}
类型别名在两个方面提供帮助:它使代码更易于编写,并且它提供
US 在所有 .因为它是一个别名,所以它是
Just another ,这意味着我们可以使用任何与它一起使用的方法,以及像 Operator 这样的特殊语法。std::io
Result<T, E>
Result<T, E>
?
Never 返回的 Never 类型
Rust 有一个名为 Rust 的特殊类型,在类型论术语中被称为空类型,因为它没有值。我们更喜欢将其称为 never 类型,因为当函数永远不会时,它代替了返回类型。
返回。下面是一个示例:!
fn bar() -> ! {
// --snip--
panic!();
}
此代码被读取为“the function returns never”。返回
never 称为发散函数。我们无法创建该类型的值,因此永远不可能返回。bar
!
bar
但是,您永远无法为其创建值的类型有什么用呢?从 示例 2-5,部分猜数字游戏;我们复制了一些 在示例 19-26 中。
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
let mut guess = String::new();
// --snip--
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
当时,我们跳过了这段代码中的一些细节。在第 6 章的 “match
Control Flow Operator” 一节中,我们讨论了 arms 必须全部返回相同的类型。因此,对于
示例,以下代码不起作用:match
fn main() {
let guess = "3";
let guess = match guess.trim().parse() {
Ok(_) => 5,
Err(_) => "hello",
};
}
此代码中的 type of 必须是一个整数和一个字符串,
而 Rust 要求 它们只有一个类型。那么 return 是什么呢?我们是如何从一只手臂返回 a 并拥有另一只手臂的
以示例 19-26 结尾?guess
guess
continue
u32
continue
正如您可能已经猜到的那样,具有价值。也就是说,当 Rust
计算 的类型 ,它查看两个 match 分支,前者具有
value of 的 value 和后者的 value.因为永远不能有
value 时,Rust 决定 的类型为 。continue
!
guess
u32
!
!
guess
u32
描述此行为的正式方式是,类型的表达式可以
被强迫进入任何其他类型。我们允许用 because 结束这个 arm 不返回值;相反,它会移动控制
回到循环的顶部,所以在这种情况下,我们永远不会给 分配一个值。!
match
continue
continue
Err
guess
never 类型对宏也很有用。回想一下我们在 values 上调用以产生值或 panic 的函数
此定义:panic!
unwrap
Option<T>
enum Option<T> {
Some(T),
None,
}
use crate::Option::*;
impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"),
}
}
}
在这段代码中,发生的事情与示例 19-26: Rust 中发生的事情相同
看到 具有 type 和 type ,因此结果
的表达式为 。此代码之所以有效,是因为不会生成值;它将结束程序。在这种情况下,我们不会
从 返回一个值,因此此代码有效。match
val
T
panic!
!
match
T
panic!
None
unwrap
具有该类型的最后一个表达式是 :!
loop
fn main() {
print!("forever ");
loop {
print!("and ever ");
}
}
在这里,循环永无止境,表达式的值也是如此。但是,此
如果我们包含一个 ,则不是真的,因为循环会终止
当它到达 .!
break
break
动态大小的类型和 Sized
特征
Rust 需要知道有关其类型的某些详细信息,例如要 allocate 的值。这留下了它的类型的一个角落 系统一开始有点令人困惑:动态大小类型的概念。 有时称为 DST 或未调整大小的类型,这些类型允许我们编写 使用我们只能在运行时知道其大小的值进行编码。
让我们深入研究一个名为 的动态大小的类型的详细信息,该类型
我们在整本书中一直在使用。没错,不是 ,而是 on
它自己的是 DST。我们无法知道字符串在 runtime 之前有多长,这意味着
我们不能创建 类型 的变量 ,也不能采用 类型的参数。请考虑以下代码,该代码不起作用:str
&str
str
str
str
fn main() {
let s1: str = "Hello there!";
let s2: str = "How's it going?";
}
Rust 需要知道为特定
type,并且一个 type 的所有值都必须使用相同数量的内存。如果 Rust
允许我们编写此代码,则这两个值需要占用
相同的空间。但是它们的长度不同:需要 12 个字节的
储存和需要 15.这就是为什么无法创建变量的原因
持有动态大小的类型。str
s1
s2
那么我们该怎么办呢?在这种情况下,您已经知道答案:我们制作类型
的 和 a 而不是 a 。从 “String 中召回
Slices“部分,其中
structure 只存储切片的起始位置和长度。所以
虽然 a 是存储 所在位置的内存地址的单个值,但 a 是两个值:地址 及其
长度。因此,我们可以在编译时知道值的大小:它是
长度是 的两倍。也就是说,我们总是知道 a 的大小,没有
无论它引用的字符串有多长。一般来说,这是进入
在 Rust 中使用了哪些动态大小的类型:它们有一个额外的
元数据,用于存储动态信息的大小。黄金法则
动态大小的类型是我们必须始终将动态大小的值
类型。s1
s2
&str
str
&T
T
&str
str
&str
usize
&str
我们可以与各种指针组合:例如,或 .事实上,您以前已经见过这种情况,但动态
sized type: traits.每个 trait 都是一个动态大小的类型,我们可以用
使用 trait 的名称。在第 17 章的“使用 trait 对象
允许不同的值
Types“ 部分,我们提到要使用 trait 作为 trait 对象,我们必须
将它们放在指针后面,例如 or ( 也可以)。str
Box<str>
Rc<str>
&dyn Trait
Box<dyn Trait>
Rc<dyn Trait>
为了使用 DST,Rust 提供了 trait 来确定
类型的大小在编译时是已知的。此 trait 是自动实现的
对于在编译时已知大小的所有内容。此外,Rust
隐式地将 Bound On 添加到每个泛型函数。也就是说,一个
泛型函数定义如下:Sized
Sized
fn generic<T>(t: T) {
// --snip--
}
实际上被视为我们编写了以下内容:
fn generic<T: Sized>(t: T) {
// --snip--
}
默认情况下,泛型函数将仅适用于具有已知大小 编译时。但是,您可以使用以下特殊语法来放宽此 限制:
fn generic<T: ?Sized>(t: &T) {
// --snip--
}
绑定在 上的 trait 表示 “ 可能是也可能不是 ”,而 this
表示法会覆盖泛型类型必须具有已知大小的默认值
编译时。具有此含义的语法仅适用于 ,而不适用于任何其他特征。?Sized
T
Sized
?Trait
Sized
另请注意,我们将参数的类型从 切换到 。
因为类型可能不是 ,所以我们需要在某种
指针。在本例中,我们选择了一个参考。t
T
&T
Sized
接下来,我们将讨论函数和闭包!
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准