高级类型
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 模式是一种实现第 17 章 “封装隐藏了实现细节”
部分所讨论的隐藏实现细节的封装的轻量级方法。
类型别名用来创建类型同义词
连同 newtype 模式,Rust 还提供了声明 类型别名(type alias)的能力,使用 type
关键字来给予现有类型另一个名字。例如,可以像这样创建 i32
的别名 Kilometers
:
type Kilometers = i32;
这意味着 Kilometers
是 i32
的 同义词(synonym);不同于示例 19-15 中创建的 Millimeters
和 Meters
类型。Kilometers
不是一个新的、单独的类型。Kilometers
类型的值将被完全当作 i32
类型值来对待:
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 这样全是如此代码的项目:
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(|| ())
}
类型别名通过减少项目中重复代码的数量来使其更加易于控制。这里我们为这个冗长的类型引入了一个叫做 Thunk
的别名,这样就可以如示例 19-25 所示将所有使用这个类型的地方替换为更短的 Thunk
:
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(|| ())
}
这样读写起来就容易多了!为类型别名选择一个好名字也可以帮助你表达意图(单词 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::io::Error;
use std::fmt;
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
有这个类型别名声明:
type Result<T> = std::result::Result<T, std::io::Error>;
因为这位于 std::io
中,可用的完全限定的别名是 std::io::Result<T>
—— 也就是说,Result<T, E>
中 E
放入了 std::io::Error
。Write
trait 中的函数最终看起来像这样:
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
中提供了一致的接口。因为这是一个别名,它只是另一个 Result<T, E>
,这意味着可以在其上使用 Result<T, E>
的任何方法,以及像 ?
这样的特殊语法。
从不返回的 never type
Rust 有一个叫做 !
的特殊类型。在类型理论术语中,它被称为 empty type,因为它没有值。我们更倾向于称之为 never type。这个名字描述了它的作用:在函数从不返回的时候充当返回值。例如:
fn bar() -> ! {
// --snip--
}
这读 “函数 bar
从不返回”,而从不返回的函数被称为 发散函数(diverging functions)。不能创建 !
类型的值,所以 bar
也不可能返回值。
不过一个不能创建值的类型有什么用呢?如果你回想一下示例 2-5 中的代码,曾经有一些看起来像这样的代码,如示例 19-26 所重现的:
# let guess = "3";
# loop {
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
# break;
# }
当时我们忽略了代码中的一些细节。在第 6 章 “match
控制流运算符”
部分,我们学习了 match
的分支必须返回相同的类型。如下代码不能工作:
let guess = match guess.trim().parse() {
Ok(_) => 5,
Err(_) => "hello",
}
这里的 guess
必须既是整型 也是 字符串,而 Rust 要求 guess
只能是一个类型。那么 continue
返回了什么呢?为什么示例 19-26 中会允许一个分支返回 u32
而另一个分支却以 continue
结束呢?
正如你可能猜到的,continue
的值是 !
。也就是说,当 Rust 要计算 guess
的类型时,它查看这两个分支。前者是 u32
值,而后者是 !
值。因为 !
并没有一个值,Rust 决定 guess
的类型是 u32
。
描述 !
的行为的正式方式是 never type 可以强转为任何其他类型。允许 match
的分支以 continue
结束是因为 continue
并不真正返回一个值;相反它把控制权交回上层循环,所以在 Err
的情况,事实上并未对 guess
赋值。
never type 的另一个用途是 panic!
。还记得 Option<T>
上的 unwrap
函数吗?它产生一个值或 panic。这里是它的定义:
impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"),
}
}
}
这里与示例 19-34 中的 match
发生了相同的情况:Rust 知道 val
是 T
类型,panic!
是 !
类型,所以整个 match
表达式的结果是 T
类型。这能工作是因为 panic!
并不产生一个值;它会终止程序。对于 None
的情况,unwrap
并不返回一个值,所以这些代码有效。
最后一个有着 !
类型的表达式是 loop
:
print!("forever ");
loop {
print!("and ever ");
}
这里,循环永远也不结束,所以此表达式的值是 !
。但是如果引入 break
这就不为真了,因为循环在执行到 break
后就会终止。
动态大小类型和 Sized
trait
因为 Rust 需要知道例如应该为特定类型的值分配多少空间这样的信息其类型系统的一个特定的角落可能令人迷惑:这就是 动态大小类型(dynamically sized types)的概念。这有时被称为 “DST” 或 “unsized types”,这些类型允许我们处理只有在运行时才知道大小的类型。
让我们深入研究一个贯穿本书都在使用的动态大小类型的细节:str
。没错,不是 &str
,而是 str
本身。str
是一个 DST;直到运行时我们都不知道字符串有多长。因为直到运行时都不能知道其大小,也就意味着不能创建 str
类型的变量,也不能获取 str
类型的参数。考虑一下这些代码,他们不能工作:
let s1: str = "Hello there!";
let s2: str = "How's it going?";
Rust 需要知道应该为特定类型的值分配多少内存,同时所有同一类型的值必须使用相同数量的内存。如果允许编写这样的代码,也就意味着这两个 str
需要占用完全相同大小的空间,不过它们有着不同的长度。这也就是为什么不可能创建一个存放动态大小类型的变量的原因。
那么该怎么办呢?你已经知道了这种问题的答案:s1
和 s2
的类型是 &str
而不是 str
。如果你回想第 4 章 “切片 slice 类型”
部分,slice 数据结构储存了开始位置和 slice 的长度。
所以虽然 &T
是一个储存了 T
所在的内存位置的单个值,&str
则是 两个 值:str
的地址和其长度。这样,&str
就有了一个在编译时可以知道的大小:它是 usize
长度的两倍。也就是说,我们总是知道 &str
的大小,而无论其引用的字符串是多长。这里是 Rust 中动态大小类型的常规用法:他们有一些额外的元信息来储存动态信息的大小。这引出了动态大小类型的黄金规则:必须将动态大小类型的值置于某种指针之后。
可以将 str
与所有类型的指针结合:比如 Box<str>
或 Rc<str>
。事实上,之前我们已经见过了,不过是另一个动态大小类型:trait。每一个 trait 都是一个可以通过 trait 名称来引用的动态大小类型。在第 17 章 “为使用不同类型的值而设计的 trait 对象”
部分,我们提到了为了将 trait 用于 trait 对象,必须将他们放入指针之后,比如 &dyn Trait
或 Box<dyn Trait>
(Rc<dyn Trait>
也可以)。
为了处理 DST,Rust 有一个特定的 trait 来确定一个类型的大小是否在编译时可知:这就是 Sized
trait。这个 trait 自动为编译器在编译时就知道其大小的类型实现。另外,Rust 隐式的为每一个泛型函数增加了 Sized
bound。也就是说,对于如下泛型函数定义:
fn generic<T>(t: T) {
// --snip--
}
实际上被当作如下处理:
fn generic<T: Sized>(t: T) {
// --snip--
}
泛型函数默认只能用于在编译时已知大小的类型。然而可以使用如下特殊语法来放宽这个限制:
fn generic<T: ?Sized>(t: &T) {
// --snip--
}
?Sized
trait bound 与 Sized
相对;也就是说,它可以读作 “T
可能是也可能不是 Sized
的”。这个语法只能用于 Sized
,而不能用于其他 trait。
另外注意我们将 t
参数的类型从 T
变为了 &T
:因为其类型可能不是 Sized
的,所以需要将其置于某种指针之后。在这个例子中选择了引用。
接下来,让我们讨论一下函数和闭包!
推荐几款学习编程的免费平台
免费在线开发平台(https://docs.ltpp.vip/LTPP/)
探索编程世界的新天地,为学生和开发者精心打造的编程平台,现已盛大开启!这个平台汇集了近4000道精心设计的编程题目,覆盖了C、C++、JavaScript、TypeScript、Go、Rust、PHP、Java、Ruby、Python3以及C#等众多编程语言,为您的编程学习之旅提供了一个全面而丰富的实践环境。
在这里,您不仅可以查看自己的代码记录,还能轻松地在云端保存和运行代码,让编程变得更加便捷。平台还提供了私聊和群聊功能,让您可以与同行们无障碍交流,分享文件,共同进步。不仅如此,您还可以通过阅读文章、参与问答板块和在线商店,进一步拓展您的知识边界。
为了提升您的编程技能,平台还设有每日一题、精选题单以及激动人心的编程竞赛,这些都是备考编程考试的绝佳资源。更令人兴奋的是,您还可以自定义系统UI,选择视频或图片作为背景,打造一个完全个性化的编码环境,让您的编程之旅既有趣又充满挑战。
免费公益服务器(https://docs.ltpp.vip/LTPP-SHARE/linux.html)
作为开发者或学生,您是否经常因为搭建和维护编程环境而感到头疼?现在,您不必再为此烦恼,因为一款全新的免费公共服务器已经为您解决了所有问题。这款服务器内置了多种编程语言的编程环境,并且配备了功能强大的在线版VS Code,让您可以随时随地在线编写代码,无需进行任何复杂的配置。
随时随地,云端编码
无论您身在何处,只要有网络连接,就可以通过浏览器访问这款公共服务器,开始您的编程之旅。这种云端编码的便利性,让您的学习或开发工作不再受限于特定的设备或环境。
丰富的编程语言支持
服务器支持包括C、C++、JavaScript、TypeScript、Go、Rust、PHP、Java、Ruby、Python3以及C#等在内的多种主流编程语言,满足不同开发者和学生的需求。无论您是初学者还是资深开发者,都能找到适合自己的编程环境。
在线版VS Code,高效开发
内置的在线版VS Code提供了与本地VS Code相似的编辑体验,包括代码高亮、智能提示、代码调试等功能,让您即使在云端也能享受到高效的开发体验。
数据隐私和安全提醒
虽然服务器是免费的,但为了保护您的数据隐私和安全,我们建议您不要上传任何敏感或重要的数据。这款服务器更适合用于学习和实验,而非存储重要信息。
免费公益MYSQL(https://docs.ltpp.vip/LTPP-SHARE/mysql.html)
作为一名开发者或学生,数据库环境的搭建和维护往往是一个复杂且耗时的过程。但不用担心,现在有一款免费的MySQL服务器,专为解决您的烦恼而设计,让数据库的使用变得简单而高效。
性能卓越,满足需求
虽然它是免费的,但性能绝不打折。服务器提供了稳定且高效的数据库服务,能够满足大多数开发和学习场景的需求。
在线phpMyAdmin,管理更便捷
内置的在线phpMyAdmin管理面板,提供了一个直观且功能强大的用户界面,让您可以轻松地查看、编辑和管理数据库。
数据隐私提醒,安全第一
正如您所知,这是一项公共资源,因此我们强烈建议不要上传任何敏感或重要的数据。请将此服务器仅用于学习和实验目的,以确保您的数据安全。
免费在线WEB代码编辑器(https://docs.ltpp.vip/LTPP-WEB-IDE/)
无论你是开发者还是学生,编程环境的搭建和管理可能会占用你宝贵的时间和精力。现在,有一款强大的免费在线代码编辑器,支持多种编程语言,让您可以随时随地编写和运行代码,提升编程效率,专注于创意和开发。
多语言支持,无缝切换
这款在线代码编辑器支持包括C、C++、JavaScript、TypeScript、Go、Rust、PHP、Java、Ruby、Python3以及C#在内的多种编程语言,无论您的项目需要哪种语言,都能在这里找到支持。
在线运行,快速定位问题
您可以在编写代码的同时,即时运行并查看结果,快速定位并解决问题,提高开发效率。
代码高亮与智能提示
编辑器提供代码高亮和智能提示功能,帮助您更快地编写代码,减少错误,提升编码质量。
免费二维码生成器(https://docs.ltpp.vip/LTPP-QRCODE/)
二维码(QR Code)是一种二维条码,能够存储更多信息,并且可以通过智能手机等设备快速扫描识别。它广泛应用于各种场景,如:
企业宣传
企业可以通过二维码分享公司网站、产品信息、服务介绍等。
活动推广
活动组织者可以创建二维码,参与者扫描后可以直接访问活动详情、报名链接或获取电子门票。
个人信息分享
个人可以生成包含联系方式、社交媒体链接、个人简历等信息的二维码。
电子商务
商家使用二维码进行商品追踪、促销活动、在线支付等。
教育
教师可以创建二维码,学生扫描后可以直接访问学习资料或在线课程。
交通出行
二维码用于公共交通的票务系统,乘客扫描二维码即可进出站或支付车费。 功能强大的二维码生成器通常具备用户界面友好,操作简单,即使是初学者也能快速上手和生成的二维码可以在各种设备和操作系统上扫描识别的特点。