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