教程文档【免费下载链接】bookThe Rust Programming Language项目地址https://gitcode.com/gh_mirrors/bo/book点击查看免费下载本篇指南聚焦《The Rust Programming Language》第 20 章高级特性中的类型系统进阶主题围绕仓库中的 src/ch20-03-advanced-types.md 展开系统讲解 Rust 类型系统中四类此前只被顺带提及、尚未深入讨论的机制用于类型安全与抽象的 Newtype 模式、用于消除重复的类型别名type关键字、永不返回的 Never 类型!以及动态大小类型DST与Sizedtrait。读完本文你将能够用 Newtype 在编译期防止不同类型值被混用并封装实现细节用类型别名大幅简化冗长的函数签名理解continue、panic!、死循环等永不返回表达式如何参与类型推导掌握str、trait 对象等 DST 必须置于指针之后的黄金法则并正确使用?Sized放宽泛型约束。一、用 Newtype 模式实现类型安全与抽象Newtype 模式包装类型模式我们在讨论为外部类型实现外部 trait时已经接触过其核心思想是用一个只包含单个字段的元组结构体把现有类型包起来从而得到一个全新的、独立的类型。除了为外部 trait 提供实现这一用途外Newtype 模式还能在编译期静态地防止值被混淆并用来标示一个值的单位。仓库中 listings/ch20-advanced-features/listing-20-16/src/lib.rs 给出了用 Newtype 标示单位的经典示例——Millimeters毫米与Meters米都包装了u32use std::ops::Add; struct Millimeters(u32); struct Meters(u32); impl AddMeters for Millimeters { type Output Millimeters; fn add(self, other: Meters) - Millimeters { Millimeters(self.0 (other.0 * 1000)) } }这里Millimeters和Meters虽然内部都是u32但它们是两个不同的类型。因此如果我们编写了一个参数类型为Millimeters的函数任何试图用Meters值或裸u32调用它的程序都无法通过编译——这种区分不是靠程序员小心谨慎而是由编译器在类型检查阶段强制保证的。在涉及物理单位、货币、ID 等容易混淆数值语义的场景中这一特性价值极高。Newtype 模式的第二个价值是抽象掉类型的实现细节新类型可以对外暴露一套与内部私有类型完全不同的公共 API。例如我们可以提供一个People类型来包装存有人员 ID 与姓名映射的HashMapi32, Stringstruct People(HashMapi32, String); impl People { // 对外只暴露添加姓名的公共方法 fn add_name(mut self, name: String) { // 内部自行分配 i32 ID 并插入 HashMap } }使用People的代码只与公共 API 交互完全不需要知道内部用i32ID 关联姓名这件事。Newtype 是一种轻量级实现封装、隐藏实现细节的方式这与第 18 章中讨论的封装与隐藏实现细节一脉相承调用方永远无法直接触碰内部字段从而可以放心重构内部数据结构而不破坏外部代码。二、类型同义词与类型别名Type Aliases与 Newtype 语义相近但行为迥异的另一项特性是类型别名用type关键字给一个已有类型另起一个名字。仓库中 listings/ch20-advanced-features/no-listing-04-kilometers-alias/src/main.rs 演示了如何把i32化名为Kilometersfn main() { type Kilometers i32; let x: i32 5; let y: Kilometers 5; println!(x y {}, x y); }关键区别在于别名Kilometers只是i32的同义词而非像Millimeters、Meters那样独立的新类型。因此Kilometers类型的值与i32类型的值完全等价——我们可以把两种类型的值相加也可以把Kilometers值直接传给参数类型为i32的函数。相应地我们得不到Newtype 模式带来的类型检查收益如果代码里把Kilometers与i32弄混了编译器不会给出任何错误因为它们本来就是同一个类型。主要用途一消除冗长类型的重复书写类型别名的核心用例是减少重复。设想我们在代码中频繁使用如下冗长类型Boxdyn Fn() Send static在函数签名、类型注解中反复手写这个类型既烦琐又易出错。listings/ch20-advanced-features/listing-20-25/src/main.rs 展示了一个到处都是这种长类型的项目片段fn main() { let f: Boxdyn Fn() Send static Box::new(|| println!(hi)); fn takes_long_type(f: Boxdyn Fn() Send static) { // --snip-- } fn returns_long_type() - Boxdyn Fn() Send static { // --snip-- Box::new(|| ()) } }引入别名Thunk后见 listings/ch20-advanced-features/listing-20-26/src/main.rs所有出现该类型的地方都可以替换为短名fn main() { type Thunk Boxdyn 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这个词的本义就是留待稍后求值的代码恰好适合用来命名一个被存储起来的闭包。主要用途二与ResultT, E配合消除重复类型别名还常与ResultT, E搭配使用。以标准库的std::io模块为例I/O 操作经常会失败因此大量函数返回ResultT, E且其中的E几乎总是std::io::Error。仓库中 listings/ch20-advanced-features/no-listing-05-write-trait/src/lib.rs 展示了Writetrait 中这种反复出现的签名use std::fmt; use std::io::Error; pub trait Write { fn write(mut self, buf: [u8]) - Resultusize, 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内部做了如下的别名声明见 listings/ch20-advanced-features/no-listing-06-result-alias/src/lib.rsuse std::fmt; type ResultT std::result::ResultT, std::io::Error;由于该声明位于std::io模块内我们可以直接使用全限定别名std::io::ResultT即把E固定为std::io::Error的ResultT, E。重写后的Writetrait 签名变得干净统一pub trait Write { fn write(mut self, buf: [u8]) - Resultusize; fn flush(mut self) - Result(); fn write_all(mut self, buf: [u8]) - Result(); fn write_fmt(mut self, fmt: fmt::Arguments) - Result(); }类型别名在此带来双重收益既让代码更易书写又让整个std::io模块拥有一致的接口。同时要强调因为别名只是ResultT, E本身所以所有适用于ResultT, E的方法如unwrap、expect以及?运算符都能直接作用于std::io::ResultT。三、永不返回的 Never 类型!Rust 有一个特殊类型!在类型理论术语中被称为空类型empty type因为它没有任何值。Rust 社区更愿意称之为永不返回类型never type因为它出现在函数永远不会返回的返回类型位置。仓库中 listings/ch20-advanced-features/no-listing-07-never-type/src/lib.rs 给出了最直接的例子fn bar() - ! { // --snip-- panic!(); }这段代码读作函数bar返回 never。返回 never 的函数被称为发散函数diverging function。由于无法创建类型!的任何值bar不可能返回——它要么无限循环要么直接panic!终止程序。continue与match分支的类型统一一个没有值的类型有什么用回想第 2 章猜数字游戏中的代码listings/ch02-guessing-game-tutorial/listing-02-05/src/main.rslet guess: u32 match guess.trim().parse() { Ok(num) num, Err(_) continue, };当时我们跳过了其中的细节。第 6 章 “match控制流结构”讲过match的各个分支必须返回相同类型。例如 listings/ch20-advanced-features/no-listing-08-match-arms-different-types/src/main.rs 中的代码就无法编译fn main() { let guess 3; let guess match guess.trim().parse() { Ok(_) 5, Err(_) hello, }; }guess的类型被迫既要是整数又要是字符串而 Rust 要求guess只有一个类型。那么问题来了continue返回的到底是什么为什么在猜数字游戏里一个分支返回u32、另一个分支以continue结尾却能合法通过编译答案正如你可能猜到的continue的值就是!类型。当 Rust 计算guess的类型时它会同时考察两个分支——前者产生u32值后者产生!值。因为!不可能拥有任何值Rust 判定整个match的类型为u32。这种行为的形式化描述是!类型的表达式可以被强制转换为任意其他类型。match分支可以以continue结尾是因为continue不返回值而是把控制流移回循环顶部因此在Err情况下我们根本不会给guess赋值。panic!与unwrap中的 Never 类型Never 类型对panic!宏同样意义重大。回想对OptionT值调用、用来取出值否则 panic的unwrap函数见 listings/ch20-advanced-features/no-listing-09-unwrap-definition/src/lib.rsenum OptionT { Some(T), None, } use crate::Option::*; implT OptionT { pub fn unwrap(self) - T { match self { Some(val) val, None panic!(called Option::unwrap() on a None value), } } }这里发生了与猜数字游戏match完全相同的过程Rust 看到val的类型是T而panic!的类型是!因此整个match表达式的类型就是T。这段代码能够工作是因为panic!不产生任何值——它会直接终止程序在None分支中我们不会从unwrap返回值所以代码合法。循环表达式的类型最后一个类型为!的表达式是循环。仓库中 listings/ch20-advanced-features/no-listing-10-loop-returns-never/src/main.rs 展示了这一情形fn main() { print!(forever ); loop { print!(and ever ); } }这个循环永远不会结束因此整个loop表达式的值就是!。但要注意一旦在循环中加入break循环就可能终止表达式也就不再是!类型了。四、动态大小类型与SizedtraitRust 需要了解类型的某些细节例如为某个特定类型的值分配多少空间。这给类型系统留下了一个初学者容易困惑的角落动态大小类型dynamically sized types常简称为DST或unsized types。这类类型允许我们编写使用只有在运行时才能知道大小的值的代码。str是 DST为什么不能直接持有我们整本书都在用的str就是一个 DST——注意是单独的str不是str。在许多场景如存储用户输入的文本中字符串有多长只有到运行时才知道。这意味着我们既不能创建str类型的变量也不能让函数接收str类型的参数。仓库中 listings/ch20-advanced-features/no-listing-11-cant-create-str/src/main.rs 的代码就无法编译fn main() { let s1: str Hello there!; let s2: str Hows it going?; }Rust 必须知道要为每种类型的值分配多少内存且同类型的所有值必须占用相同内存。如果允许上面的代码s1需要 12 字节、s2需要 15 字节二者大小不同这违背了类型系统的基本前提——因此 Rust 不允许创建持有动态大小类型的变量。解决之道把 DST 放在指针后面答案其实我们早已知道把s1、s2的类型写成字符串切片str而不是str。回顾第 4 章的“字符串切片”一节切片数据结构只保存起始位置和长度。因此T是一个仅存有T内存地址的单一值而字符串切片则是两个值——str的地址和它的长度。这样字符串切片值的大小在编译期就是已知的恰好是两个usize那么大。无论它指向的字符串有多长切片本身的大小恒定。这概括了 Rust 中使用动态大小类型的通用方式动态大小的信息会附带一份额外的元数据来记录其大小而 DST 的黄金法则是——必须始终把动态大小类型的值放在某种指针之后。str可以与各种指针组合例如Boxstr或Rcstr。其实你早已见过另一个 DST——trait每个 trait 都是可以用 trait 名称指代其类型的动态大小类型。在第 18 章“使用 trait 对象抽象共享行为”中我们提到要把 trait 用作 trait 对象就必须把它放在指针之后如dyn Trait或Boxdyn TraitRcdyn Trait同样可行。Sizedtrait 与隐式约束为了处理 DSTRust 提供了Sizedtrait 来判断一个类型的大小在编译期是否已知。凡是编译期大小已知的类型Rust 都会自动为其实现Sized。此外Rust 会隐式地为每个泛型函数加上Sized约束。也就是说像 listings/ch20-advanced-features/no-listing-12-generic-fn-definition/src/lib.rs 这样的泛型函数定义fn genericT(t: T) { // --snip-- }实际会被编译器当作如下写法处理见 listings/ch20-advanced-features/no-listing-13-generic-implicit-sized-bound/src/lib.rsfn genericT: Sized(t: T) { // --snip-- }默认情况下泛型函数只适用于编译期大小已知的类型。要放宽这一限制可以使用特殊的?Sized语法见 listings/ch20-advanced-features/no-listing-14-generic-maybe-sized/src/lib.rsfn genericT: ?Sized(t: T) { // --snip-- }?Sized的含义是T可能是Sized也可能不是它覆盖了泛型类型默认必须编译期定长的限制。需要特别注意的是?Trait这种语法只对Sized有效不能用于其他 trait。另一个值得留意的细节上面的函数把参数t的类型从T换成了T。因为T可能不是Sized我们必须通过某种指针来使用它——这里选择的是引用。这也再次印证了那条黄金法则动态大小类型永远要待在指针后面。五、结语与延伸本章四个主题共同勾勒出 Rust 类型系统严格与灵活并存的气质Newtype 模式通过创建独立新类型把单位、ID 等语义差异上升为编译期错误并顺手实现轻量级封装类型别名在保持类型同一性的前提下用短名消除冗长签名的重复书写是 API 一致性设计如std::io::ResultT的有力工具Never 类型!用没有值的类型巧妙打通了continue、panic!、死循环与普通值之间的类型统一支撑起发散函数与?运算符等机制动态大小类型与Sizedtrait则揭示了切片、trait 对象背后指针 元数据的设计本质以及泛型默认定长、?Sized放宽约束的取舍。想要亲自验证这些特性的读者可以直接运行仓库 listings/ch20-advanced-features 目录下对应的 Cargo 示例如no-listing-04-kilometers-alias、listing-20-25、listing-20-26尝试把?Sized换成Sized观察编译器行为或把 Newtype 值传给i32参数体会类型错误信息——实践永远是理解类型系统最好的途径。赞分享教程文档【免费下载链接】bookThe Rust Programming Language项目地址https://gitcode.com/gh_mirrors/bo/book点击查看免费下载相关推荐Rust 实战深入掌握 newtype 模式、类型别名Type Alias与 DST 动态大小类型Rust 实战深入掌握 newtype 模式、类型别名Type Alias与 DST 动态大小类型 在 Rust 中类型系统是保证内存安全与程序正确性的文档教程示例工程Rust By Practice 精讲newtype 模式、类型别名与 DST / 非 Sized 类型Rust By Practice 精讲newtype 模式、类型别名与 DST / 非 Sized 类型 本文基于 rust by practice 仓库中文档教程示例工程Rust By Example 类型系统指南Casting、字面量、类型推断与类型别名实战Rust By Example 类型系统指南Casting、字面量、类型推断与类型别名实战 Rust 的类型系统是这门语言安全与效率的基石它 不做任何隐式类文档教程上一篇DDrawCompat技术解析让经典DirectX游戏在现代Windows系统重获新生的兼容性解决方案下一篇喜马拉雅音频下载器终极指南免费保存VIP专辑到本地收藏创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考