diff --git a/.DS_Store b/.DS_Store index 85533d11..1b0d25b5 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/assets/baidu_verify_code-vTMwnHyCKW.html b/assets/baidu_verify_code-vTMwnHyCKW.html new file mode 100644 index 00000000..8c850e35 --- /dev/null +++ b/assets/baidu_verify_code-vTMwnHyCKW.html @@ -0,0 +1 @@ +ce435f7affacb4ad10f3745ba9540432 \ No newline at end of file diff --git a/release b/release index 557826ee..d8c70a5d 100755 --- a/release +++ b/release @@ -4,6 +4,7 @@ mdbook build ## copy CNAME info to book dir cp CNAME ./book/ +cp ./assets/*.html ./book/ ## init git repo cd book diff --git a/src/SUMMARY.md b/src/SUMMARY.md index d1780e6a..195cad65 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -19,32 +19,37 @@ - [数值类型](basic/base-type/numbers.md) - [字符、布尔、元类型](basic/base-type/char-bool.md) - [语句与表达式](basic/base-type/statement-expression.md) - - [函数 todo](basic/base-type/function.md) + - [函数](basic/base-type/function.md) - [所有权和借用](basic/ownership/index.md) - [所有权](basic/ownership/ownership.md) - [引用与借用](basic/ownership/borrowing.md) - - [字符串与切片](basic/string-slice.md) - [复合类型](basic/compound-type/intro.md) + - [字符串与切片](basic/scompound-type/tring-slice.md) - [元组](basic/compound-type/tuple.md) - [结构体](basic/compound-type/struct.md) - - [枚举 todo](basic/compound-type/enum.md) - - [数组 todo](basic/compound-type/array.md) - - [类型转换 todo](basic/type-converse.md) + - [枚举](basic/compound-type/enum.md) + - [数组](basic/compound-type/array.md) + - [流程控制](basic/flow-control.md) + - [模式匹配](basic/match-pattern/intro.md) + - [match和if let](basic/match-pattern/match-if-let.md) + - [解构Option](basic/match-pattern/option.md) + - [模式和匹配](basic/match-pattern/pattern-match.md) + - [全模式列表](basic/match-pattern/all-patterns.md) - [方法Method(todo)](basic/method.md) - - [格式化输出(todo)](basic/formatted-output.md) - - [流程控制(todo)](basic/flow-control.md) + - [泛型(todo)](basic/generitic.md) + - [特征(todo)](basic/trait.md) + - [类型转换 todo](basic/type-converse.md) - [返回、异常和错误(todo)](basic/exception-error.md) - - [模式匹配(todo)](basic/match-pattern.md) - - [文档注释(todo)](basic/comment.md) - - [包和模块(todo)](basic/crate-module.md) + - [进阶语法 todo](advance/intro.md) + - [集合类型(todo)](advance/collection.md) + - [格式化输出(todo)](advance/formatted-output.md) + - [文档注释(todo)](advance/comment.md) + - [包和模块(todo)](advance/crate-module.md) - [生命周期(todo)](advance/lifetime.md) - - [泛型(todo)](advance/generitic.md) - - [特征(todo)](advance/trait.md) - [迭代器(todo)](advance/interator.md) - - [集合类型(todo)](advance/collection.md) - [函数式编程(todo)](advance/functional-programing.md) - [智能指针(todo)](advance/smart-pointer.md) - [全局变量](advance/global-variable.md) @@ -127,6 +132,8 @@ - [基本用法](networking/async/tokio/basic.md) - [异步消息流](networking/async/tokio/stream.md) +- [常见陷阱 todo](traps/intro.md) + - [代码规范 doing](style-guide/intro.md) - [命名规范](style-guide/naming.md) - [代码风格(todo)](style-guide/code.md) diff --git a/src/basic/comment.md b/src/advance/comment.md similarity index 100% rename from src/basic/comment.md rename to src/advance/comment.md diff --git a/src/basic/crate-module.md b/src/advance/crate-module.md similarity index 100% rename from src/basic/crate-module.md rename to src/advance/crate-module.md diff --git a/src/basic/formatted-output.md b/src/advance/formatted-output.md similarity index 100% rename from src/basic/formatted-output.md rename to src/advance/formatted-output.md diff --git a/src/basic/base-type/numbers.md b/src/basic/base-type/numbers.md index 65f4e541..ac41ca2b 100644 --- a/src/basic/base-type/numbers.md +++ b/src/basic/base-type/numbers.md @@ -226,6 +226,24 @@ fn main() { } ``` +## 序列(Range) + +Rust提供了一个非常遍历的方式,让我们能生成连续的数值,例如`1..5`,生成从1到4的连续数字,不包含5; `1..5`,生成从1到5的连续数字,包含5,它的用户很简单,常常用于循环中: +```rust + for i in 1..=5 { + println!("{}",i); + } +``` + +最终程序输出: +```console +1 +2 +3 +4 +5 +``` + ## 有理数和复数 Rust的标准库相比其它语言,对于准入的门槛较高,因此有理数和复数并未包含在标准库中: diff --git a/src/basic/compound-type/array.md b/src/basic/compound-type/array.md index 2d531bf7..665e51f5 100644 --- a/src/basic/compound-type/array.md +++ b/src/basic/compound-type/array.md @@ -1 +1,156 @@ # 数组 + +在日常开发中,使用最广的数据结构之一就是数组,在Rust中,最常用的数组有两个,第一个是长度固定且很快速的`array`,第二个是可动态增长的但是有一点性能损耗的`Vector`,在本书中,我们称呼`array`为数组,`Vector`为动态数组。 + +不知道你们发现没,这两个数组的关系跟`&str`与`String`的关系很像,前者是长度固定的字符串切片,后者是可动态增长的字符串。其实,在Rust中无论是`String`还是`Vector`,它们都是Rust的高级类型:集合类型,在后面章节会有详细介绍。 + +对于本章节,我们的重点还是放在数组`array`上。数组的具体定义很简单:将多个类型相同的元素依次组合在一起,就是一个数组。结合上面的内容,可以得出数组的三要素: +- 长度固定 +- 元素必须有相同的类型 +- 依次线性排列 + +这里再啰嗦一句,**我们这里说的数组是Rust的基本类型,因此长度是固定的,这个跟其他编程语言不同,而其它编程语言的数组往往对应的是Rust中的动态数组`Vector`**,希望读者大大牢记此点。 + +### 创建数组 +在Rust中,数组是这样定义的: +```rust +fn main() { + let a = [1, 2, 3, 4, 5]; +} +``` + +数组语法跟`javascript`很像,也跟大多数编程语言很像。由于它的元素类型大小固定,且长度也是固定,因此**数组是存储在栈上**,性能也会非常优秀。与此对应,动态数组`Vector`是存储在堆上,因此长度可以动态改变。当你不确定是使用数组还是动态数组时,那就应该使用后者,具体见[动态数组Vector]一章. + +举个例子,在需要知道一年中各个月份名称的程序中,你很可能希望使用的是数组而不是动态数组。因为月份是固定的,它总是包含 12 个元素: +```rust +let months = ["January", "February", "March", "April", "May", "June", "July", + "August", "September", "October", "November", "December"]; +``` + +在一些时候,还需要为**数组声明类型**,如下所示: +```rust +let a: [i32; 5] = [1, 2, 3, 4, 5]; +``` +这里,数组类型是通过方括号语法声明,`i32`是元素类型,分号后面的数字`5`是数组长度,数组类型也从侧面说明了**数组的元素类型要统一,长度要固定**. + +还可以使用下面的语法初始化一个**某个值重复出现N次的数组**: +```rust +let a: = [3; 5]; +``` +`a`数组包含`5`个元素,这些元素的初始化值为`3`,聪明的读者已经发现,这种语法跟数组类型的声明语法其实是保持一致的:`[3;5]` 和`[类型;长度]`. + +在元素重复的场景,这种写法要简单的多,否则你就得疯狂敲击键盘:`let a = [3, 3, 3, 3, 3];`,不过老板可能很喜欢你的这种疯狂编程的状态。 + +### 访问数组元素 + +因为数组是连续存放元素的,因此可以通过索引的方式来访问存放其中的元素: +```rust +fn main() { + let a = [9, 8, 7, 6, 5]; + + let first = a[0]; // 获取a数组第一个元素 + let second = a[1]; // 获取第二个元素 +} +``` +此处,`first`获取到的值是`9`,`second`是`8`。 + +#### 越界访问 +假如使用超出数组范围的索引访问数组元素,就发生什么?下面是一个接收用户的控制台输入,然后用于访问数组元素的例子: +```rust +use std::io; + +fn main() { + let a = [1, 2, 3, 4, 5]; + + println!("Please enter an array index."); + + let mut index = String::new(); + // 读取控制台的输出 + io::stdin() + .read_line(&mut index) + .expect("Failed to read line"); + + let index: usize = index + .trim() + .parse() + .expect("Index entered was not a number"); + + let element = a[index]; + + println!( + "The value of the element at index {} is: {}", + index, element + ); +} +``` + +使用`cargo run`来运行代码,因为数组只有5个元素,如果我们试图输入`5`去访问第`6`个元素,则会访问到不存在的数组元素,最终程序会崩溃退出: +```console +Please enter an array index. +5 +thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 5', src/main.rs:19:19 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +``` + +这就是数组访问越界,访问了数组中不存在的元素,导致Rust运行时错误。程序因此退出并显示错误消息,未执行最后的`println!`语句。 + +当你尝试使用索引访问元素时,Rust 将检查你指定的索引是否小于数组长度。如果索引大于或等于数组长度,Rust会出现 panic。这种检查必须在运行时进行,尤其是在这种情况下,因为编译器无法在编译期知道用户之后运行代码时将输入什么值。 + +这种就是Rust的安全特性之一。在很多系统编程语言中,并不会检查数组越界问题,你会访问到无效的内存地址获取到一个风马牛不相及的值,最终导致在程序逻辑上出现大问题,而且这种问题会非常难以检查。 + +## 数组切片 + +在之前的[章节](./string-slice.md#切片(slice)),我们有讲到`切片`这个概念,它允许你引用集合中的某个连续片段,而不是整个集合,对于数组也是,数组切片允许我们引用数组的一部分: + +```rust +let a: [i32; 5] = [1, 2, 3, 4, 5]; + +let slice: &[i32] = &a[1..3]; + +assert_eq!(slice, &[2, 3]); +``` +上面的数组切片`slice`的类型是`&[i32]`,与之对比,数组的类型是`[i32;5]`,简单总结下切片的特点: +- 切片的长度与数组不同,并不是固定的,而是取决于你使用时指定的开始和结束位置 +- 创建切片的代价非常小,因为切片只是针对底层数组的一个引用 +- 切片类型[T]拥有不固定的大小,而切片引用类型&[T]则具有固定的大小,因为Rust很多时候都需要固定大小数据类型,因此&[T]更有用,`&str`字符串切片也同理 + +## 总结 +最后,让我们以一个综合性使用数组的例子,来结束本章节的学习: +```rust +fn main() { + // 编译器自动推导出one的类型 + let one = [1, 2, 3]; + // 显式类型标注 + let two: [u8; 3] = [1, 2, 3]; + let blank1 = [0; 3]; + let blank2: [u8; 3] = [0; 3]; + + // arrays是一个二维数组,其中每一个元素都是一个数组,元素类型是[u8; 3] + let arrays: [[u8; 3]; 4] = [one, two, blank1, blank2]; + + // 借用arrays的元素用作循环中 + for a in &arrays { + print!("{:?}: ", a); + // 将a变成一个迭代器,用于循环 + // 你也可以直接用for n in a {}来进行循环 + for n in a.iter() { + print!("\t{} + 10 = {}", n, n+10); + } + + let mut sum = 0; + // 0..a.len,是一个Rust的语法糖,其实就等于一个数组,元素是从0,1,2一直增加到到a.len-1 + for i in 0..a.len() { + sum += a[i]; + } + println!("\t({:?} = {})", a, sum); + } +} +``` + +做个总结,数组虽然很简单,但是其实还是存在几个要注意的点: +- **数组类型容易跟数组切片混淆**,[T;n]描述了一个数组的类型,而[T]描述了切片的类型, 因为切片是运行期的数据结构,因此它不具备编译器的长度,因此不能用[T;n]的形式去描述 +- `[u8; 3]`和`[u8;4]是不同的类型`,数组的长度也是类型的一部分 +- **在实践中,使用最多的是数组切片[T]**,我们往往通过引用的方式去使用`&[T]`,因为后者有固定的类型大小. + + +至此,关于数据类型部分,我们已经全部学完了,对于Rust学习而言,我们也迈出了坚定的第一步,后面将开始对更高级特性的学习。未来如果大家有疑惑需要检索知识,一样可以继续回顾过往的章节,因为本书不仅仅是一门Rust的教程,还是一本厚重的Rust工具书。 \ No newline at end of file diff --git a/src/basic/compound-type/enum.md b/src/basic/compound-type/enum.md index 7b18adf5..a0d9e814 100644 --- a/src/basic/compound-type/enum.md +++ b/src/basic/compound-type/enum.md @@ -1,13 +1,171 @@ # 枚举 +枚举(enum或enumeration)允许你通过列举可能的成员来定义一个**`枚举类型`**,例如扑克牌花色: +```rust +enum PokerSuit { + Clubs, + Spades, + Diamonds, + Hearts, +} +``` + +如果在此之前你没有在其它语言中使用过枚举,那么可能需要花费一些时间来理解这些概念,一旦上手,就会发现枚举的强大,甚至对它爱不释手,枚举虽好,可不要滥用哦。 + +再回到之前创建的`PokerSuit`,扑克总共有四种花色,而这里我们枚举出所有的可能值,这也正是`枚举`名称的由来。 + +任何一张扑克,它的花色肯定会落在四种花色中,而且也只会落在其中一个花色上,这种特性非常适合枚举的使用,因为**`枚举值`**只可能是其中一个成员。抽象来看,四种花色尽管是不同的花色,但是它们都是扑克花色这个概念,因此当某个函数处理扑克花色时,可以把它们当作相同的类型进行传参。 + +细心的读者应该注意到,我们对之前的`枚举类型`和`枚举值`进行了重点标注,这是因为对于新人来说容易混淆相应的概念,总而言之: +**枚举类型是一个类型,它会包含所有可能的枚举成员, 而枚举值是该类型中的具体某个成员的实现。** + +## 枚举值 +现在来创建`PokerSuit`枚举类型的两个成员实例: +```rust +let heart = PokerSuit::Hearts; +let diamond = PokerSuit::Diamonds; +``` + +我们通过`::`操作符来访问`PokerSuit`下的具体成员,从代码可以清晰看出,`heart`和`diamond`都是`PkerSuit`枚举类型的,接着可以定义一个函数来使用它们: +```rust +fn main() { + let heart = PokerSuit::Hearts; + let diamond = PokerSuit::Diamonds; + + print_suit(heart); + print_suit(diamond); +} + +fn print_suit(card: PokerSuit) { + println!("{:?}",card); +} +``` + +`print_suit`函数的参数类型是`PokerSuit`,因此我们可以把`heart`和`diamond`传给它,虽然`heart`是基于`PokerSuit`下的`Hearts`成员实例化的,但是它是货真价实的`PokerSuit`枚举类型。 + +接下来,我们想给扑克牌变得更加实用,那么需要给每张牌赋予一个值:`A`(1)-`K`(13),这样再加上花色,就是一张真实的扑克牌了,例如红心A。 + +目前来说,枚举值还不能带有值,因此先用结构体来实现: +```rust +enum PokerSuit { + Clubs, + Spades, + Diamonds, + Hearts, +} + +struct PokerCard { + suit: PokerSuit, + value: u8 +} + +fn main() { + let c1 = PokerCard { + suit: PokerSuit::Clubs, + value: 1, + }; + let c2 = PokerCard { + suit: PokerSuit::Diamonds, + value: 12, + }; +} +``` +这段代码很好的完成了它的使命,通过结构体`PokerCard`来代表一张牌,结构体的`suit`字段表示牌的花色,类型是`PokerSuit`枚举类型,`value`字段代表扑克牌的值。 + +可以吗?可以!好吗?说实话,不咋地,因为还有简洁的多的方式来实现: +```rust +enum PokerCard { + Clubs(u8), + Spades(u8), + Diamonds(u8), + Hearts(u8), +} + +fn main() { + let c1 = PokerCard::Spades(5); + let c2 = PokerCard::Diamonds(13); +} +``` + +直接将数据信息关联到枚举成员上,直接省去近一半的代码,这种实现漂亮不? + +而且不仅仅如此,同一个枚举类型下的不同成员还能持有不同的类型,例如让部分花色打印1-13的字样,另外花色打印上A-K的字样: +```rust +enum PokerCard { + Clubs(u8), + Spades(u8), + Diamonds(char), + Hearts(char), +} + +fn main() { + let c1 = PokerCard::Spades(5); + let c2 = PokerCard::Diamonds('A'); +} +``` + +回想一下,遇到这种不同类型的情况,再用我们之前的结构体实现方式,可行吗?也许可行,但是会复杂很多。 + + +再来看一个来自标准库中的例子: +```rust +struct Ipv4Addr { + // --snip-- +} + +struct Ipv6Addr { + // --snip-- +} + +enum IpAddr { + V4(Ipv4Addr), + V6(Ipv6Addr), +} +``` +该例子跟我们之前的扑克牌很像,只不过枚举成员包含的类型更复杂了,变成了结构体:分别通过`Ipv4Addr`和`Ipv4Addr`来定义两种不同的IP数据。 -## 枚举的一些妙用 +从这些例子可以看出,**任何类型的数据都可以放入枚举成员中**: 例例如字符串、数值、结构体甚至另一个枚举。 -#### 归一不同类型 -在实际项目中,我们有的时候会遇到用同一个函数去处理不同类型的场景,这些类型具有相似的方法,因此你可以在这个函数中用同一套代码进行处理, -但是问题是如果将这些类型传入此函数?类型该如何统一? +增加一些挑战?先看以下代码: +```rust +enum Message { + Quit, + Move { x: i32, y: i32 }, + Write(String), + ChangeColor(i32, i32, i32), +} + +fn main() { + let m1 = Message::Quit; + let m2 = Message::Move{x:1,y:1}; + let m3 = Message::ChangeColor(255,255,0); +} +``` + +该枚举类型代表一条消息,它包含四个不同的成员: +- `Quit` 没有任何关联数据 +- `Move` 包含一个匿名结构体 +- `Write` 包含一个`String`字符串 +- `ChangeColor`包含三个`i32` + +当然,我们也可以用结构体的方式来定义这些消息: +```rust +struct QuitMessage; // 元结构体 +struct MoveMessage { + x: i32, + y: i32, +} +struct WriteMessage(String); // 元组结构体 +struct ChangeColorMessage(i32, i32, i32); // 元组结构体 +``` + +由于每个结构体都有自己的类型,因此我们无法在需要同一类型的地方进行使用,例如某个函数它的功能是接受消息并进行发送,那么用枚举的方式,就可以接收不同的消息,但是用结构体,该函数无法接受4个不同的结构体作为参数。 + +而且从代码规范角度来看,枚举的实现更简洁,代码内聚性更强,不像结构体的实现,分散在各个地方。 + +最后,再用一个实际项目中的设计考虑,来结束枚举类型的语法学习。 -例如以下代码,需要在同一个函数中处理`tcp`流和`tls`流: +例如我们有一个web服务,需要接受用户的长连接,假设连接有两种:TcpStream和TlsStream,但是我们希望对这两个连接的处理流程相同,也就是用同一个函数来处理这两个连接,代码如下: ```rust func new (stream: TcpStream) { let mut s = stream; @@ -22,10 +180,92 @@ func new (stream: TcpStream) { } ``` -因此,我们需要一个类型既能支持TcpStream,又能支持TlsStream,此时即可借用枚举类型来实现: +此时,枚举类型就能帮上大忙: ```rust enum Websocket { Tcp(Websocket), Tls(Websocket>), } -``` \ No newline at end of file +``` + +## Option枚举用于处理空值 +在其它编程语言中,往往都有一个`null`关键字,该关键字用于表明一个变量当前的值为空(不是零值,例如整形的零值是0),也就是不存在值。当你对这些`null`进行操作时,例如调用一个方法,就会直接抛出异常,导致程序的崩溃,因此我们在编程时需要格外的小心去处理这些`null`空值。 + +> Tony Hoare,null的发明者,曾经说过有非常有名的话 +> +> 我称之为我十亿美元的错误。当时,我在使用一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过在设计过程中,我未能抵抗住诱惑,引入了空引用的概念,因为它非常容易实现。就是因为这个决策,引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数十亿美元的苦痛和伤害。 + +然后空值的表达依然非常有意义,因为空值表示当前时刻变量的值是缺失的。因此,Rust吸取了众多教训,决定抛弃`null`,而改为使用`Option`枚举变量来表述这种结果: +**一个变量要么有值:`Some(T)`, 要么为空: `None`,定义如下: +```rust +enum Option { + Some(T), + None, +} +``` + +其中`T`是泛型参数,`Some(T)`表示该枚举成员的数据类型是`T`, 换句话说,`Some`可以包含任何类型的数据。 + +`Option` 枚举是如此有用以至于它甚至被包含在了`prelude`(Rust会将最常用的类型、函数等提前引入进来,避免我们再手动引入)之中,你不需要将其显式引入作用域。另外,它的成员也是如此,可以不需要`Option::`前缀来直接使用`Some` 和 `None`。即便如此`Option` 也仍是常规的枚举,`Some(T)` 和 `None` 仍是 `Option` 的成员。 + +再来看以下代码: +```rust +let some_number = Some(5); +let some_string = Some("a string"); + +let absent_number: Option = None; +``` + + +如果使用 `None` 而不是 `Some`,需要告诉 Rust `Option` 是什么类型的,因为编译器只通过 `None` 值无法推断出 `Some` 成员保存的值的类型。 + +当有一个 `Some` 值时,我们就知道存在一个值,而这个值保存在 `Some` 中。当有个 `None` 值时,在某种意义上,它跟空值具有相同的意义:并没有一个有效的值。那么,`Option` 为什么就比空值要好呢? + +简而言之,因为 `Option` 和 `T`(这里 `T` 可以是任何类型)是不同的类型,例如,这段代码不能编译,因为它尝试将 `Option`(`Option`) 与 `i8`(`T`) 相加: + +```rust +let x: i8 = 5; +let y: Option = Some(5); + +let sum = x + y; +``` + +如果运行这些代码,将得到类似这样的错误信息: + +```text +error[E0277]: the trait bound `i8: std::ops::Add>` is +not satisfied + --> + | +5 | let sum = x + y; + | ^ no implementation for `i8 + std::option::Option` + | +``` + +很好!事实上,错误信息意味着 Rust 不知道该如何将 `Option` 与 `i8` 相加,因为它们的类型不同。当在 Rust 中拥有一个像 `i8` 这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需做空值检查。只有当使用 `Option`(或者任何用到的类型)的时候需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。 + +换句话说,在对 `Option` 进行 `T` 的运算之前必须将其转换为 `T`。通常这能帮助我们捕获到空值最常见的问题之一:假设某值不为空但实际上为空的情况。 + +不再担心会错误的使用一个空值,会让你对代码更加有信心。为了拥有一个可能为空的值,你必须要显式的将其放入对应类型的 `Option` 中。接着,当使用这个值时,必须明确的处理值为空的情况。只要一个值不是 `Option` 类型,你就 **可以** 安全的认定它的值不为空。这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥以增加 Rust 代码的安全性。 + +那么当有一个 `Option` 的值时,如何从 `Some` 成员中取出 `T` 的值来使用它呢?`Option` 枚举拥有大量用于各种情况的方法:你可以查看[它的文档](https://doc.rust-lang.org/std/option/enum.Option.html)。熟悉 `Option` 的方法将对你的 Rust 之旅非常有用。 + + +总的来说,为了使用 `Option` 值,需要编写处理每个成员的代码。你想要一些代码只当拥有 `Some(T)` 值时运行,允许这些代码使用其中的 `T`。也希望一些代码在值为 `None` 时运行,这些代码并没有一个可用的 `T` 值。`match` 表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。 + +这里先简单看一下`match`的大致模样,在[模式匹配](../match-pattern.md)中,我们会详细讲解: + +```rust +fn plus_one(x: Option) -> Option { + match x { + None => None, + Some(i) => Some(i + 1), + } +} + +let five = Some(5); +let six = plus_one(five); +let none = plus_one(None); +``` + +`plus_one`通过`match`来处理不同`Option`的情况。 \ No newline at end of file diff --git a/src/basic/string-slice.md b/src/basic/compound-type/string-slice.md similarity index 98% rename from src/basic/string-slice.md rename to src/basic/compound-type/string-slice.md index 8fc84be1..e8edd8ab 100644 --- a/src/basic/string-slice.md +++ b/src/basic/compound-type/string-slice.md @@ -135,7 +135,7 @@ let slice = &a[1..3]; assert_eq!(slice, &[2, 3]); ``` -该数组切片的类型是`&[i32]`,数组切片和字符串切片的工作方式是一样的,例如持有一个引用指向原始数组的某个元素和长度。对于集合类型,我们在[这一章](../advance/collection.md)中有详细的介绍。 +该数组切片的类型是`&[i32]`,数组切片和字符串切片的工作方式是一样的,例如持有一个引用指向原始数组的某个元素和长度。对于集合类型,我们在[这一章](../../advance/collection.md)中有详细的介绍。 ## 字符串字面量是切片 @@ -243,7 +243,7 @@ fn say_hello(s: &str) { } ``` -实际上这种灵活用法是因为`deref`强制转换,具体我们会在[Deref特征](../traits/deref.md)进行详细讲解。 +实际上这种灵活用法是因为`deref`强制转换,具体我们会在[Deref特征](../../traits/deref.md)进行详细讲解。 ## 字符串索引 diff --git a/src/basic/flow-control.md b/src/basic/flow-control.md index bc1fb805..340bc7db 100644 --- a/src/basic/flow-control.md +++ b/src/basic/flow-control.md @@ -1 +1,311 @@ -# flow-control.md \ No newline at end of file +# 流程控制 + +80后应该都对学校的小混混记忆犹新,在那个时代,小混混们往往都认为自己是地下王者,管控着地下事务的流程,在我看来,他们就像代码中的流程控制一样,无处不在,很显眼,但是又让人懒得重视。 + +言归正传,Rust程序是从上而下顺序执行的,在此过程中,我们可以引入循环、分支等流程控制方式,帮助我们的代码更好的实现相应的功能。 + +## 使用if来做分支控制 +> if else无处不在 - `鲁迅说` + +但凡你能找到一门编程语言没有`if else`,那么一定更要反馈给鲁迅,反正不是我说的:) 总之,只要你拥有其它语言的编程经验,就一定会有以下认知:`if else`**表达式**允许根据条件执行不同的代码分支: +```rust +if condition == true { + // A... +} else { + // B... +} +该代码读作:若`condition`条件为`true`,则执行`A`代码,否则执行`B`代码. + +先看下面代码: +```rust +fn main() { + let condition = true; + let number = if condition { + 5 + } else { + 6 + }; + + println!("The value of number is: {}", number); +} +``` + +以上代码有以下几点要注意: +- **`if`语句块是表达式**: 因此它可以返回一个值,这里我们使用`if`的返回值来给`number`进行赋值,因此`number`的值是`5`。 +- 用`if`来赋值时,要保证每个分支返回的类型一样,此处返回的`5`和`6`就是同一个类型,如果返回类型不一致就会报错: +```console +error[E0308]: if and else have incompatible types + --> src/main.rs:4:18 + | +4 | let number = if condition { + | __________________^ +5 | | 5 +6 | | } else { +7 | | "six" +8 | | }; + | |_____^ expected integer, found &str // 期望整数类型,但却发现&str字符串切片 + | + = note: expected type `{integer}` + found type `&str` +``` + +#### 使用else if来处理多重条件 +可以将`else if`与`if`、`else`组合在一起实现多种条件分支判断: +```rust +fn main() { + let n = 6; + + if n % 4 == 0 { + println!("number is divisible by 4"); + } else if n % 3 == 0 { + println!("number is divisible by 3"); + } else if n % 2 == 0 { + println!("number is divisible by 2"); + } else { + println!("number is not divisible by 4, 3, or 2"); + } +} +``` +程序执行时,会按照自上至下的顺序执行每一个分支判断,一旦成功,则跳出`if`语句块,最终本程序会匹配执行`else if n % 3 == 0`的分支,输出`"number is divisible by 3"`。 + +有一点要注意,就算有多个分支能匹配,也只有第一个匹配的分支会被执行! + +如果代码中有大量的`else if `会让代码变得极其丑陋,因此在下一章,我们引入一个`match`关键字,用以解决多分支模式匹配的问题。 + +## 循环控制 + +循环无处不在,上到数钱,下到数年,你能想象的很多场景都存在循环,因此它也是流程控制中最重要的组成部分之一. + +在Rust语言中有三种循环方式:`for`、`while`和`loop`,其中`for`循环是Rust循环王冠上的明珠。 + +#### for循环 + +`for`循环是Rust处理迭代的大杀器,当我们迭代数据集合时,往往就用`for`: +```rust +fn main() { + for i in 1..=5 { + println!("{}",i); + } +} +``` +以上代码迭代输出一个从1到5的序列,简单粗暴,核心就在于`for`和`in`的联动,语义表达如下: +```rust +for 元素 in 集合 { + // 使用元素干一些你懂我不懂的事情 +} +``` +这个语法跟`javascript`还是蛮像的,应该挺好理解。 + +注意,使用`for`我们往往使用集合的引用形式,除非你不想在后面的代码中继续使用该集合(不使用引用的话,所有权会被转移到`for`语句块中): +```rust +for item in &container { + // ... +} +``` + +如果想在循环中,**修改该元素**,使用`mut`关键字: +```rust +for item in &mut collection { + // ... +} +``` + +总结如下: + + 使用方法 | 等价使用方式 | 所有权 +---------|--------|-------- +`for item in collection` | `for item in IntoIterator::into_iter(collection)` | 转移所有权 +`for item in &collection` | `for item in collection.iter()` | 不可变借用 +`for item in &mut collection` | `for item in collection.iter_mut()` | 可变借用 + +如果想在循环中**获取元素的索引**: +```rust +fn main() { + let a = [4,3,2,1]; + // `.iter()`方法把`a`数组变成一个迭代器 + for (i,v) in a.iter().enumerate() { + println!("第{}个元素是{}",i+1,v); + } +} +``` + +有同学可能会想到,如果我们要用`for`循环控制某个过程执行10次,但是又不关心那个计数值,该怎么写? +```rust +for _ in 0..10 { + // ... +} +``` +可以用`_`来替代`i`用于`for`循环中,在Rust中`_`的含义是忽略该值或者类型的意思,如果不使用`_`,那么编译器会给你一个`变量未使用的`的警告. + +**两种循环方式优劣对比** + +以下代码,我们实现了两种循环方式: +```rust +// 第一种 +let collection = [1, 2, 3, 4, 5]; +for i in 0..collection.len() { + let item = collection[i]; + // ... +} + +// 第二种 +for item in collection { + +} +``` + +第一种方式是循环索引,然后通过索引下标去访问集合,第二种方式是直接循环迭代集合中的元素,优劣如下: +- **性能**:第一种使用方式中`collection[index]`的索引访问,会因为边界检查(bounds checking)导致运行时的性能损耗 - Rust会检查并确认`index`是落在集合内也就是合法的,但是第二种直接迭代的方式就不会触发这种检查,因为编译器会在编译时就完成分析并证明这种访问是合法的` +- **安全**: 第一种方式里对`collection`的索引访问是非连续的,存在一定可能性在两次访问之间,`collection`发生了变化,导致脏数据产生。而第二种直接迭代的方式是连续访问,因此不存在这种风险 + +由于for循环无需任何条件限制,也不需要通过索引来访问,因此是最安全也是最常用的,在下面的`while`中,我们能看到为什么`for`会更加安全。 + +#### `continue` +使用`continue`可以跳过当前当次的循环,开始下次的循环: +```rust + for i in 1..4 { + if i == 2 { + continue; + } + println!("{}",i); + } +``` +上面代码对1到3的序列进行迭代,且跳过值为2时的循环,输出如下: +```console +1 +3 +``` + +#### while循环 + +如果你需要一个条件来循环,当该条件为`true`时,继续循环,条件为`false`,跳出循环,那么`while`就非常适用: +```rust +fn main() { + let mut n = 0; + + while n <= 5 { + println!("{}!", n); + + n = n + 1; + } + + println!("我出来了!"); +} +``` + +该`while`循环,只有到`n`小于等于`5`时,才执行,否则就立刻跳出循环,因此在上述代码中,它会先从`0`开始,满足条件,进行循环,然后是`1`,满足条件,进行循环,最终到`6`的时候,不满足条件,跳出`while`循环,执行`我出来了`的打印,然后程序结束: +```console +0! +1! +2! +3! +4! +5! +我出来了! +``` + +当然,你也可以用其它方式组合实现,例如`loop`(无条件循环,将在下面介绍) + `if` + `break`: +```rust +fn main() { + let mut n = 0; + + loop { + if n > 5 { + break + } + println!("{}",n); + n+=1; + } + + println!("我出来了!"); +} +``` +可以看出,在这种循环场景下,`while`要简洁的多。 + +**while vs for** + +我们也能用`while`来实现`for`的功能: +```rust +fn main() { + let a = [10, 20, 30, 40, 50]; + let mut index = 0; + + while index < 5 { + println!("the value is: {}", a[index]); + + index = index + 1; + } +} +``` +这里,代码对数组中的元素进行计数。它从索引 `0` 开始,并接着循环直到遇到数组的最后一个索引(这时,`index < 5` 不再为真)。运行这段代码会打印出数组中的每一个元素: +```console +the value is: 10 +the value is: 20 +the value is: 30 +the value is: 40 +the value is: 50 +``` + +数组中的所有五个元素都如期被打印出来。尽管 index 在某一时刻会到达值 5,不过循环在其尝试从数组获取第六个值(会越界)之前就停止了。 + +但这个过程很容易出错;如果索引长度不正确会导致程序 panic。这也使程序更慢,因为编译器增加了运行时代码来对每次循环的每个元素进行条件检查。 + +`for`循环代码如下: +```rust +fn main() { + let a = [10, 20, 30, 40, 50]; + + for element in a.iter() { + println!("the value is: {}", element); + } +} +``` + +可以看出,`for`并不会使用索引去访问数组,因此更安全也更简洁,同时避免`运行时的边界检查`,性能更高。 + + +#### loop循环 +对于循环而言,`loop`循环毋庸置疑,是适用面最高的,它可以适用于所有循环场景(虽然能用,但是在很多场景下,`for`和`while`才是最优选择),因为loop就是一个简单的无限循环,你可以在内部实现逻辑通过`break`关键字来控制循环何时结束。 + +使用`loop`循环一定要打起精神,否则你会写出下面的跑满你一个cpu核心的疯子代码: +```rust,ignore +fn main() { + loop { + println!("again!"); + } +} +``` + +该循环会不停的在终端打印输出,直到你使用`Ctrl-C`结束程序: +```console +again! +again! +again! +again! +^Cagain! +``` + +**注意**,不要轻易尝试上述代码,如果你电脑配置不行,可能会死机!!! + +因此,当使用`loop`时,必不可少的伙伴是`break`关键字,它能让循环在满足某个条件时跳出: +```rust +fn main() { + let mut counter = 0; + + let result = loop { + counter += 1; + + if counter == 10 { + break counter * 2; + } + }; + + println!("The result is {}", result); +} +``` +以上代码当`counter`递增到`10`时,就会通过`break`返回一个`counter*2`的值,最后赋给result并打印出来。 + +这里有几点值得注意: +- **break可以单独使用,也可以带一个返回值**,有些类似`return` +- **loop是一个表达式**,因此可以返回一个值 + diff --git a/src/advance/generitic.md b/src/basic/generitic.md similarity index 100% rename from src/advance/generitic.md rename to src/basic/generitic.md diff --git a/src/basic/match-pattern.md b/src/basic/match-pattern.md deleted file mode 100644 index 49fa3d12..00000000 --- a/src/basic/match-pattern.md +++ /dev/null @@ -1 +0,0 @@ -# match-pattern.md \ No newline at end of file diff --git a/src/basic/match-pattern/all-patterns.md b/src/basic/match-pattern/all-patterns.md new file mode 100644 index 00000000..3f3224fa --- /dev/null +++ b/src/basic/match-pattern/all-patterns.md @@ -0,0 +1,603 @@ +# 全模式列表 + +在本书中我们已领略过许多不同类型模式的例子。在本节中,我们收集了模式中所有有效的语法,并讨论了为什么可能要使用每个语法。 + +### 匹配字面值 + +如第六章所示,可以直接匹配字面值模式。如下代码给出了一些例子: + +```rust +let x = 1; + +match x { + 1 => println!("one"), + 2 => println!("two"), + 3 => println!("three"), + _ => println!("anything"), +} +``` + +这段代码会打印 `one` 因为 `x` 的值是 1。如果希望代码获得特定的具体值,则该语法很有用。 + +### 匹配命名变量 + +命名变量是匹配任何值的不可反驳模式,这在之前已经使用过数次。然而当其用于 `match` 表达式时情况会有些复杂。因为 `match` 会开始一个新作用域,`match` 表达式中作为模式的一部分声明的变量会覆盖 `match` 结构之外的同名变量,与所有变量一样。在示例 18-11 中,声明了一个值为 `Some(5)` 的变量 `x` 和一个值为 `10` 的变量 `y`。接着在值 `x` 上创建了一个 `match` 表达式。观察匹配分支中的模式和结尾的 `println!`,并在运行此代码或进一步阅读之前推断这段代码会打印什么。 + +文件名: src/main.rs + +```rust +fn main() { + let x = Some(5); + let y = 10; + + match x { + Some(50) => println!("Got 50"), + Some(y) => println!("Matched, y = {:?}", y), + _ => println!("Default case, x = {:?}", x), + } + + println!("at the end: x = {:?}, y = {:?}", x, y); +} +``` + +示例 18-11: 一个 `match` 语句其中一个分支引入了覆盖变量 `y` + +让我们看看当 `match` 语句运行的时候发生了什么。第一个匹配分支的模式并不匹配 `x` 中定义的值,所以代码继续执行。 + +第二个匹配分支中的模式引入了一个新变量 `y`,它会匹配任何 `Some` 中的值。因为我们在 `match` 表达式的新作用域中,这是一个新变量,而不是开头声明为值 10 的那个 `y`。这个新的 `y` 绑定会匹配任何 `Some` 中的值,在这里是 `x` 中的值。因此这个 `y` 绑定了 `x` 中 `Some` 内部的值。这个值是 5,所以这个分支的表达式将会执行并打印出 `Matched, y = 5`。 + +如果 `x` 的值是 `None` 而不是 `Some(5)`,头两个分支的模式不会匹配,所以会匹配下划线。这个分支的模式中没有引入变量 `x`,所以此时表达式中的 `x` 会是外部没有被覆盖的 `x`。在这个假想的例子中,`match` 将会打印 `Default case, x = None`。 + +一旦 `match` 表达式执行完毕,其作用域也就结束了,同理内部 `y` 的作用域也结束了。最后的 `println!` 会打印 `at the end: x = Some(5), y = 10`。 + +为了创建能够比较外部 `x` 和 `y` 的值,而不引入覆盖变量的 `match` 表达式,我们需要相应地使用带有条件的匹配守卫(match guard)。我们稍后将在 [“匹配守卫提供的额外条件”](#extra-conditionals-with-match-guards) 这一小节讨论匹配守卫。 + +### 多个模式 + +在 `match` 表达式中,可以使用 `|` 语法匹配多个模式,它代表 **或**(*or*)的意思。例如,如下代码将 `x` 的值与匹配分支相比较,第一个分支有 **或** 选项,意味着如果 `x` 的值匹配此分支的任一个值,它就会运行: + +```rust +let x = 1; + +match x { + 1 | 2 => println!("one or two"), + 3 => println!("three"), + _ => println!("anything"), +} +``` + +上面的代码会打印 `one or two`。 + +### 通过 `..=` 匹配值的范围 + +`..=` 语法允许你匹配一个闭区间范围内的值。在如下代码中,当模式匹配任何在此范围内的值时,该分支会执行: + +```rust +let x = 5; + +match x { + 1..=5 => println!("one through five"), + _ => println!("something else"), +} +``` + +如果 `x` 是 1、2、3、4 或 5,第一个分支就会匹配。这相比使用 `|` 运算符表达相同的意思更为方便;相比 `1..=5`,使用 `|` 则不得不指定 `1 | 2 | 3 | 4 | 5`。相反指定范围就简短的多,特别是在希望匹配比如从 1 到 1000 的数字的时候! + +范围只允许用于数字或 `char` 值,因为编译器会在编译时检查范围不为空。`char` 和 数字值是 Rust 仅有的可以判断范围是否为空的类型。 + +如下是一个使用 `char` 类型值范围的例子: + +```rust +let x = 'c'; + +match x { + 'a'..='j' => println!("early ASCII letter"), + 'k'..='z' => println!("late ASCII letter"), + _ => println!("something else"), +} +``` + +Rust 知道 `c` 位于第一个模式的范围内,并会打印出 `early ASCII letter`。 + +### 解构并分解值 + +也可以使用模式来解构结构体、枚举、元组和引用,以便使用这些值的不同部分。让我们来分别看一看。 + +#### 解构结构体 + +示例 18-12 展示带有两个字段 `x` 和 `y` 的结构体 `Point`,可以通过带有模式的 `let` 语句将其分解: + +文件名: src/main.rs + +```rust +struct Point { + x: i32, + y: i32, +} + +fn main() { + let p = Point { x: 0, y: 7 }; + + let Point { x: a, y: b } = p; + assert_eq!(0, a); + assert_eq!(7, b); +} +``` + +示例 18-12: 解构一个结构体的字段为单独的变量 + +这段代码创建了变量 `a` 和 `b` 来匹配结构体 `p` 中的 `x` 和 `y` 字段。这个例子展示了模式中的变量名不必与结构体中的字段名一致。不过通常希望变量名与字段名一致以便于理解变量来自于哪些字段。 + +因为变量名匹配字段名是常见的,同时因为 `let Point { x: x, y: y } = p;` 包含了很多重复,所以对于匹配结构体字段的模式存在简写:只需列出结构体字段的名称,则模式创建的变量会有相同的名称。示例 18-13 展示了与示例 18-12 有着相同行为的代码,不过 `let` 模式创建的变量为 `x` 和 `y` 而不是 `a` 和 `b`: + +文件名: src/main.rs + +```rust +struct Point { + x: i32, + y: i32, +} + +fn main() { + let p = Point { x: 0, y: 7 }; + + let Point { x, y } = p; + assert_eq!(0, x); + assert_eq!(7, y); +} +``` + +示例 18-13: 使用结构体字段简写来解构结构体字段 + +这段代码创建了变量 `x` 和 `y`,与变量 `p` 中的 `x` 和 `y` 相匹配。其结果是变量 `x` 和 `y` 包含结构体 `p` 中的值。 + +也可以使用字面值作为结构体模式的一部分进行进行解构,而不是为所有的字段创建变量。这允许我们测试一些字段为特定值的同时创建其他字段的变量。 + +示例 18-14 展示了一个 `match` 语句将 `Point` 值分成了三种情况:直接位于 `x` 轴上(此时 `y = 0` 为真)、位于 `y` 轴上(`x = 0`)或不在任何轴上的点。 + +文件名: src/main.rs + +```rust +# struct Point { +# x: i32, +# y: i32, +# } +# +fn main() { + let p = Point { x: 0, y: 7 }; + + match p { + Point { x, y: 0 } => println!("On the x axis at {}", x), + Point { x: 0, y } => println!("On the y axis at {}", y), + Point { x, y } => println!("On neither axis: ({}, {})", x, y), + } +} +``` + +示例 18-14: 解构和匹配模式中的字面值 + +第一个分支通过指定字段 `y` 匹配字面值 `0` 来匹配任何位于 `x` 轴上的点。此模式仍然创建了变量 `x` 以便在分支的代码中使用。 + +类似的,第二个分支通过指定字段 `x` 匹配字面值 `0` 来匹配任何位于 `y` 轴上的点,并为字段 `y` 创建了变量 `y`。第三个分支没有指定任何字面值,所以其会匹配任何其他的 `Point` 并为 `x` 和 `y` 两个字段创建变量。 + +在这个例子中,值 `p` 因为其 `x` 包含 0 而匹配第二个分支,因此会打印出 `On the y axis at 7`。 + +#### 解构枚举 + +本书之前的部分曾经解构过枚举,比如第六章中示例 6-5 中解构了一个 `Option`。一个当时没有明确提到的细节是解构枚举的模式需要对应枚举所定义的储存数据的方式。让我们以示例 6-2 中的 `Message` 枚举为例,编写一个 `match` 使用模式解构每一个内部值,如示例 18-15 所示: + +文件名: src/main.rs + +```rust +enum Message { + Quit, + Move { x: i32, y: i32 }, + Write(String), + ChangeColor(i32, i32, i32), +} + +fn main() { + let msg = Message::ChangeColor(0, 160, 255); + + match msg { + Message::Quit => { + println!("The Quit variant has no data to destructure.") + } + Message::Move { x, y } => { + println!( + "Move in the x direction {} and in the y direction {}", + x, + y + ); + } + Message::Write(text) => println!("Text message: {}", text), + Message::ChangeColor(r, g, b) => { + println!( + "Change the color to red {}, green {}, and blue {}", + r, + g, + b + ) + } + } +} +``` + +示例 18-15: 解构包含不同类型值成员的枚举 + +这段代码会打印出 `Change the color to red 0, green 160, and blue 255`。尝试改变 `msg` 的值来观察其他分支代码的运行。 + +对于像 `Message::Quit` 这样没有任何数据的枚举成员,不能进一步解构其值。只能匹配其字面值 `Message::Quit`,因此模式中没有任何变量。 + +对于像 `Message::Move` 这样的类结构体枚举成员,可以采用类似于匹配结构体的模式。在成员名称后,使用大括号并列出字段变量以便将其分解以供此分支的代码使用。这里使用了示例 18-13 所展示的简写。 + +对于像 `Message::Write` 这样的包含一个元素,以及像 `Message::ChangeColor` 这样包含三个元素的类元组枚举成员,其模式则类似于用于解构元组的模式。模式中变量的数量必须与成员中元素的数量一致。 + +#### 解构嵌套的结构体和枚举 + +目前为止,所有的例子都只匹配了深度为一级的结构体或枚举。当然也可以匹配嵌套的项! + +例如,我们可以重构列表 18-15 的代码来同时支持 RGB 和 HSV 色彩模式: + +```rust +enum Color { + Rgb(i32, i32, i32), + Hsv(i32, i32, i32), +} + +enum Message { + Quit, + Move { x: i32, y: i32 }, + Write(String), + ChangeColor(Color), +} + +fn main() { + let msg = Message::ChangeColor(Color::Hsv(0, 160, 255)); + + match msg { + Message::ChangeColor(Color::Rgb(r, g, b)) => { + println!( + "Change the color to red {}, green {}, and blue {}", + r, + g, + b + ) + } + Message::ChangeColor(Color::Hsv(h, s, v)) => { + println!( + "Change the color to hue {}, saturation {}, and value {}", + h, + s, + v + ) + } + _ => () + } +} +``` + +示例 18-16: 匹配嵌套的枚举 + +`match` 表达式第一个分支的模式匹配一个包含 `Color::Rgb` 枚举成员的 `Message::ChangeColor` 枚举成员,然后模式绑定了 3 个内部的 `i32` 值。第二个分支的模式也匹配一个 `Message::ChangeColor` 枚举成员, 但是其内部的枚举会匹配 `Color::Hsv` 枚举成员。我们可以在一个 `match` 表达式中指定这些复杂条件,即使会涉及到两个枚举。 + +#### 解构结构体和元组 + +甚至可以用复杂的方式来混合、匹配和嵌套解构模式。如下是一个复杂结构体的例子,其中结构体和元组嵌套在元组中,并将所有的原始类型解构出来: + +```rust +# struct Point { +# x: i32, +# y: i32, +# } +# +let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 }); +``` + +这将复杂的类型分解成部分组件以便可以单独使用我们感兴趣的值。 + +通过模式解构是一个方便利用部分值片段的手段,比如结构体中每个单独字段的值。 + +### 忽略模式中的值 + +有时忽略模式中的一些值是有用的,比如 `match` 中最后捕获全部情况的分支实际上没有做任何事,但是它确实对所有剩余情况负责。有一些简单的方法可以忽略模式中全部或部分值:使用 `_` 模式(我们已经见过了),在另一个模式中使用 `_` 模式,使用一个以下划线开始的名称,或者使用 `..` 忽略所剩部分的值。让我们来分别探索如何以及为什么要这么做。 + +#### 使用 `_` 忽略整个值 + +我们已经使用过下划线(`_`)作为匹配但不绑定任何值的通配符模式了。虽然 `_` 模式作为 `match` 表达式最后的分支特别有用,也可以将其用于任意模式,包括函数参数中,如示例 18-17 所示: + +文件名: src/main.rs + +```rust +fn foo(_: i32, y: i32) { + println!("This code only uses the y parameter: {}", y); +} + +fn main() { + foo(3, 4); +} +``` + +示例 18-17: 在函数签名中使用 `_` + +这段代码会完全忽略作为第一个参数传递的值 `3`,并会打印出 `This code only uses the y parameter: 4`。 + +大部分情况当你不再需要特定函数参数时,最好修改签名不再包含无用的参数。在一些情况下忽略函数参数会变得特别有用,比如实现 trait 时,当你需要特定类型签名但是函数实现并不需要某个参数时。此时编译器就不会警告说存在未使用的函数参数,就跟使用命名参数一样。 + +#### 使用嵌套的 `_` 忽略部分值 + +也可以在一个模式内部使用`_` 忽略部分值,例如,当只需要测试部分值但在期望运行的代码中没有用到其他部分时。示例 18-18 展示了负责管理设置值的代码。业务需求是用户不允许覆盖现有的自定义设置,但是可以取消设置,也可以在当前未设置时为其提供设置。 + +```rust +let mut setting_value = Some(5); +let new_setting_value = Some(10); + +match (setting_value, new_setting_value) { + (Some(_), Some(_)) => { + println!("Can't overwrite an existing customized value"); + } + _ => { + setting_value = new_setting_value; + } +} + +println!("setting is {:?}", setting_value); +``` + +示例 18-18: 当不需要 `Some` 中的值时在模式内使用下划线来匹配 `Some` 成员 + +这段代码会打印出 `Can't overwrite an existing customized value` 接着是 `setting is Some(5)`。在第一个匹配分支,我们不需要匹配或使用任一个 `Some` 成员中的值;重要的部分是需要测试 `setting_value` 和 `new_setting_value` 都为 `Some` 成员的情况。在这种情况,我们打印出为何不改变 `setting_value`,并且不会改变它。 + +对于所有其他情况(`setting_value` 或 `new_setting_value` 任一为 `None`),这由第二个分支的 `_` 模式体现,这时确实希望允许 `new_setting_value` 变为 `setting_value`。 + +也可以在一个模式中的多处使用下划线来忽略特定值,如示例 18-19 所示,这里忽略了一个五元元组中的第二和第四个值: + +```rust +let numbers = (2, 4, 8, 16, 32); + +match numbers { + (first, _, third, _, fifth) => { + println!("Some numbers: {}, {}, {}", first, third, fifth) + }, +} +``` + +示例 18-19: 忽略元组的多个部分 + +这会打印出 `Some numbers: 2, 8, 32`, 值 4 和 16 会被忽略。 + +#### 通过在名字前以一个下划线开头来忽略未使用的变量 + +如果你创建了一个变量却不在任何地方使用它, Rust 通常会给你一个警告,因为这可能会是个 bug。但是有时创建一个还未使用的变量是有用的,比如你正在设计原型或刚刚开始一个项目。这时你希望告诉 Rust 不要警告未使用的变量,为此可以用下划线作为变量名的开头。示例 18-20 中创建了两个未使用变量,不过当运行代码时只会得到其中一个的警告: + +文件名: src/main.rs + +```rust +fn main() { + let _x = 5; + let y = 10; +} +``` + +示例 18-20: 以下划线开始变量名以便去掉未使用变量警告 + +这里得到了警告说未使用变量 `y`,不过没有警告说未使用下划线开头的变量。 + +注意, 只使用 `_` 和使用以下划线开头的名称有些微妙的不同:比如 `_x` 仍会将值绑定到变量,而 `_` 则完全不会绑定。为了展示这个区别的意义,示例 18-21 会产生一个错误。 + +```rust,ignore,does_not_compile +let s = Some(String::from("Hello!")); + +if let Some(_s) = s { + println!("found a string"); +} + +println!("{:?}", s); +``` + +示例 18-21: 以下划线开头的未使用变量仍然会绑定值,它可能会获取值的所有权 + +我们会得到一个错误,因为 `s` 的值仍然会移动进 `_s`,并阻止我们再次使用 `s`。然而只使用下划线本身,并不会绑定值。示例 18-22 能够无错编译,因为 `s` 没有被移动进 `_`: + +```rust +let s = Some(String::from("Hello!")); + +if let Some(_) = s { + println!("found a string"); +} + +println!("{:?}", s); +``` + +示例 18-22: 单独使用下划线不会绑定值 + +上面的代码能很好的运行;因为没有把 `s` 绑定到任何变量;它没有被移动。 + +#### 用 `..` 忽略剩余值 + +对于有多个部分的值,可以使用 `..` 语法来只使用部分并忽略其它值,同时避免不得不每一个忽略值列出下划线。`..` 模式会忽略模式中剩余的任何没有显式匹配的值部分。在示例 18-23 中,有一个 `Point` 结构体存放了三维空间中的坐标。在 `match` 表达式中,我们希望只操作 `x` 坐标并忽略 `y` 和 `z` 字段的值: + +```rust +struct Point { + x: i32, + y: i32, + z: i32, +} + +let origin = Point { x: 0, y: 0, z: 0 }; + +match origin { + Point { x, .. } => println!("x is {}", x), +} +``` + +示例 18-23: 通过使用 `..` 来忽略 `Point` 中除 `x` 以外的字段 + +这里列出了 `x` 值,接着仅仅包含了 `..` 模式。这比不得不列出 `y: _` 和 `z: _` 要来得简单,特别是在处理有很多字段的结构体,但只涉及一到两个字段时的情形。 + +`..` 会扩展为所需要的值的数量。示例 18-24 展示了元组中 `..` 的应用: + +文件名: src/main.rs + +```rust +fn main() { + let numbers = (2, 4, 8, 16, 32); + + match numbers { + (first, .., last) => { + println!("Some numbers: {}, {}", first, last); + }, + } +} +``` + +示例 18-24: 只匹配元组中的第一个和最后一个值并忽略掉所有其它值 + +这里用 `first` 和 `last` 来匹配第一个和最后一个值。`..` 将匹配并忽略中间的所有值。 + +然而使用 `..` 必须是无歧义的。如果期望匹配和忽略的值是不明确的,Rust 会报错。示例 18-25 展示了一个带有歧义的 `..` 例子,因此其不能编译: + +文件名: src/main.rs + +```rust,ignore,does_not_compile +fn main() { + let numbers = (2, 4, 8, 16, 32); + + match numbers { + (.., second, ..) => { + println!("Some numbers: {}", second) + }, + } +} +``` + +示例 18-25: 尝试以有歧义的方式运用 `..` + +如果编译上面的例子,会得到下面的错误: + +```text +error: `..` can only be used once per tuple or tuple struct pattern + --> src/main.rs:5:22 + | +5 | (.., second, ..) => { + | ^^ +``` + +Rust 不可能决定在元组中匹配 `second` 值之前应该忽略多少个值,以及在之后忽略多少个值。这段代码可能表明我们意在忽略 `2`,绑定 `second` 为 `4`,接着忽略 `8`、`16` 和 `32`;抑或是意在忽略 `2` 和 `4`,绑定 `second` 为 `8`,接着忽略 `16` 和 `32`,以此类推。变量名 `second` 对于 Rust 来说并没有任何特殊意义,所以会得到编译错误,因为在这两个地方使用 `..` 是有歧义的。 + +### 匹配守卫提供的额外条件 + +**匹配守卫**(*match guard*)是一个指定于 `match` 分支模式之后的额外 `if` 条件,它也必须被满足才能选择此分支。匹配守卫用于表达比单独的模式所能允许的更为复杂的情况。 + +这个条件可以使用模式中创建的变量。示例 18-26 展示了一个 `match`,其中第一个分支有模式 `Some(x)` 还有匹配守卫 `if x < 5`: + +```rust +let num = Some(4); + +match num { + Some(x) if x < 5 => println!("less than five: {}", x), + Some(x) => println!("{}", x), + None => (), +} +``` + +示例 18-26: 在模式中加入匹配守卫 + +上例会打印出 `less than five: 4`。当 `num` 与模式中第一个分支比较时,因为 `Some(4)` 匹配 `Some(x)` 所以可以匹配。接着匹配守卫检查 `x` 值是否小于 `5`,因为 `4` 小于 `5`,所以第一个分支被选择。 + +相反如果 `num` 为 `Some(10)`,因为 10 不小于 5 所以第一个分支的匹配守卫为假。接着 Rust 会前往第二个分支,这会匹配因为它没有匹配守卫所以会匹配任何 `Some` 成员。 + +无法在模式中表达 `if x < 5` 的条件,所以匹配守卫提供了表现此逻辑的能力。 + +在示例 18-11 中,我们提到可以使用匹配守卫来解决模式中变量覆盖的问题,那里 `match` 表达式的模式中新建了一个变量而不是使用 `match` 之外的同名变量。新变量意味着不能够测试外部变量的值。示例 18-27 展示了如何使用匹配守卫修复这个问题。 + +文件名: src/main.rs + +```rust +fn main() { + let x = Some(5); + let y = 10; + + match x { + Some(50) => println!("Got 50"), + Some(n) if n == y => println!("Matched, n = {}", n), + _ => println!("Default case, x = {:?}", x), + } + + println!("at the end: x = {:?}, y = {}", x, y); +} +``` + +示例 18-27: 使用匹配守卫来测试与外部变量的相等性 + +现在这会打印出 `Default case, x = Some(5)`。现在第二个匹配分支中的模式不会引入一个覆盖外部 `y` 的新变量 `y`,这意味着可以在匹配守卫中使用外部的 `y`。相比指定会覆盖外部 `y` 的模式 `Some(y)`,这里指定为 `Some(n)`。此新建的变量 `n` 并没有覆盖任何值,因为 `match` 外部没有变量 `n`。 + +匹配守卫 `if n == y` 并不是一个模式所以没有引入新变量。这个 `y` **正是** 外部的 `y` 而不是新的覆盖变量 `y`,这样就可以通过比较 `n` 和 `y` 来表达寻找一个与外部 `y` 相同的值的概念了。 + +也可以在匹配守卫中使用 **或** 运算符 `|` 来指定多个模式,同时匹配守卫的条件会作用于所有的模式。示例 18-28 展示了结合匹配守卫与使用了 `|` 的模式的优先级。这个例子中重要的部分是匹配守卫 `if y` 作用于 `4`、`5` **和** `6`,即使这看起来好像 `if y` 只作用于 `6`: + +```rust +let x = 4; +let y = false; + +match x { + 4 | 5 | 6 if y => println!("yes"), + _ => println!("no"), +} +``` + +示例 18-28: 结合多个模式与匹配守卫 + +这个匹配条件表明此分支值匹配 `x` 值为 `4`、`5` 或 `6` **同时** `y` 为 `true` 的情况。运行这段代码时会发生的是第一个分支的模式因 `x` 为 `4` 而匹配,不过匹配守卫 `if y` 为假,所以第一个分支不会被选择。代码移动到第二个分支,这会匹配,此程序会打印出 `no`。这是因为 `if` 条件作用于整个 `4 | 5 | 6` 模式,而不仅是最后的值 `6`。换句话说,匹配守卫与模式的优先级关系看起来像这样: + +```text +(4 | 5 | 6) if y => ... +``` + +而不是: + +```text +4 | 5 | (6 if y) => ... +``` + +可以通过运行代码时的情况看出这一点:如果匹配守卫只作用于由 `|` 运算符指定的值列表的最后一个值,这个分支就会匹配且程序会打印出 `yes`。 + +### `@` 绑定 + +*at* 运算符(`@`)允许我们在创建一个存放值的变量的同时测试其值是否匹配模式。示例 18-29 展示了一个例子,这里我们希望测试 `Message::Hello` 的 `id` 字段是否位于 `3..=7` 范围内,同时也希望能将其值绑定到 `id_variable` 变量中以便此分支相关联的代码可以使用它。可以将 `id_variable` 命名为 `id`,与字段同名,不过出于示例的目的这里选择了不同的名称。 + +```rust +enum Message { + Hello { id: i32 }, +} + +let msg = Message::Hello { id: 5 }; + +match msg { + Message::Hello { id: id_variable @ 3..=7 } => { + println!("Found an id in range: {}", id_variable) + }, + Message::Hello { id: 10..=12 } => { + println!("Found an id in another range") + }, + Message::Hello { id } => { + println!("Found some other id: {}", id) + }, +} +``` + +示例 18-29: 使用 `@` 在模式中绑定值的同时测试它 + +上例会打印出 `Found an id in range: 5`。通过在 `3..=7` 之前指定 `id_variable @`,我们捕获了任何匹配此范围的值并同时测试其值匹配这个范围模式。 + +第二个分支只在模式中指定了一个范围,分支相关代码代码没有一个包含 `id` 字段实际值的变量。`id` 字段的值可以是 10、11 或 12,不过这个模式的代码并不知情也不能使用 `id` 字段中的值,因为没有将 `id` 值保存进一个变量。 + +最后一个分支指定了一个没有范围的变量,此时确实拥有可以用于分支代码的变量 `id`,因为这里使用了结构体字段简写语法。不过此分支中没有像头两个分支那样对 `id` 字段的值进行测试:任何值都会匹配此分支。 + +使用 `@` 可以在一个模式中同时测试和保存变量值。 + +## 总结 + +模式是 Rust 中一个很有用的功能,它帮助我们区分不同类型的数据。当用于 `match` 语句时,Rust 确保模式会包含每一个可能的值,否则程序将不能编译。`let` 语句和函数参数的模式使得这些结构更强大,可以在将值解构为更小部分的同时为变量赋值。可以创建简单或复杂的模式来满足我们的要求。 + +接下来,在本书倒数第二章中,我们将介绍一些 Rust 众多功能中较为高级的部分。 diff --git a/src/basic/match-pattern/intro.md b/src/basic/match-pattern/intro.md new file mode 100644 index 00000000..55446016 --- /dev/null +++ b/src/basic/match-pattern/intro.md @@ -0,0 +1,6 @@ +# 模式匹配 + +模式匹配,这个词,对于非函数语言编程来说,真的还蛮少听到,因为它经常出现在函数式编程里,用于为复杂的类型系统提供一个轻松的解构能力. + +曾记否?在枚举和流程控制那章,我们遗留了两个问题,都是关于`match`的,第一个是如何对`Option`枚举进行进一步处理,另外一个就是如何用`match`来替代`else if`这种丑陋的多重分支使用方式,那么让我们先一起来揭开`match`的神秘面纱。 + diff --git a/src/basic/match-pattern/match-if-let.md b/src/basic/match-pattern/match-if-let.md new file mode 100644 index 00000000..070ef2a0 --- /dev/null +++ b/src/basic/match-pattern/match-if-let.md @@ -0,0 +1,287 @@ +# match和if let + +先来看一个关于`match`的简单例子: +```rust +enum Direction { + East, + West, + North, + South, +} + +fn main() { + let dire = Direction::South; + match dire { + Direction::East => println!("East"), + Direction::North | Direction::South => { + println!("South or North"); + }, + _ => println!("West"), + }; +} +``` + +这里我们想去匹配`dire`对应的枚举类型,因此在match中用三个匹配分支来完全覆盖枚举变量`Direction`的所有成员类型,有以下几点值得注意: +- `match`的匹配必须要穷举出所有可能,因此这里用`_`来代表其余的所有可能性 +- `match`的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同 +- **X | Y**, 是逻辑运算符`或`,代表该分支可以匹配`X`也可以匹配`Y`,只要满足一个即可 + + +其实`match`跟其他语言中的`switch`非常像,例如`_`类似`switch`中的`default`。 + +## `match`匹配 + +`match`允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行对应的代码,下面让我们来一一详解,先看一个例子: +```rust +enum Coin { + Penny, + Nickel, + Dime, + Quarter, +} + +fn value_in_cents(coin: Coin) -> u8 { + match coin { + Coin::Penny => { + println!("Lucky penny!"); + 1 + }, + Coin::Nickel => 5, + Coin::Dime => 10, + Coin::Quarter => 25, + } +} +``` + +`value_in_cents`函数根据匹配到的硬币类似,返回对应的美分数值,`match`后紧跟着的是一个表达式,跟`if`很像,但是`if`后的表达式必须是一个布尔值,而`match`后的表达式返回值可以是任意类型,只要能跟后面的分支匹配起来即可,这里的`coin`是枚举`Coin`类型。 + +接下来是`match`的分支。一个分支有两个部分:一个模式和针对该模式的处理代码。第一个分支的模式是`Coin::Penny`而之后的`=>`运算符将模式和将要运行的代码分开。这里的代码就仅仅是表达式`1`, 不同分支之间使用逗号分隔。 + +当`match`表达式执行时,它将目标值`coin`按顺序与每一个分支的模式相比较, 如果模式匹配了这个值,那么模式之后的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支。 + +每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个 match 表达式的返回值。如果分支有多行代码,那么需要用`{}`包裹,同时最后一行代码需要是一个表达式。 + +#### 使用`match`表达式赋值 +还有一点很重要,`match`本身也是一个表达式,因此可以用它来赋值: +```rust +enum IpAddr { + Ipv4, + Ipv6 +} + +fn main() { + // let d_panic = Direction::South; + let ip1 = IpAddr::Ipv6; + let ip_str = match ip1 { + IpAddr::Ipv4 => "127.0.0.1", + _ => "::1", + }; + + println!("{}", ip_str); +} +``` +这里因为匹配到`_`分支,因此将`"::1"`赋值给了`ip_str`. + +#### 模式绑定 + +匹配分支的另外一个重要功能是从模式中取出绑定的值,例如: +```rust +#[derive(Debug)] +enum UsState { + Alabama, + Alaska, + // --snip-- +} + +enum Coin { + Penny, + Nickel, + Dime, + Quarter(UsState), // 25美分硬币 +} +``` +其中Coin::Quarter成员还存放了一个值:美国的某个州,因为在1999年到2008年间,美国在25美分(Quater)硬币的背后为50个州印刷了不同的设计。其它硬币都没有相关的设计。 + +接下来,我们希望在模式匹配中,获取到25美分硬币上刻印的州的名称: +```rust +fn value_in_cents(coin: Coin) -> u8 { + match coin { + Coin::Penny => 1, + Coin::Nickel => 5, + Coin::Dime => 10, + Coin::Quarter(state) => { + println!("State quarter from {:?}!", state); + 25 + }, + } +} +``` +上面代码中,在匹配`Coin::Quarter`模式时,我们把它内部存储的值绑定到了`state`变量上,因此`state`变量就是对应的`UsState`枚举类型。 + +例如有一个印了阿拉斯加州标记的25分硬币:`Coin::Quarter(UsState::Alaska))`, 它在匹配时,`state`变量将被绑定`UsState::Alaska`的枚举值。 + +再来看一个更复杂的例子: +```rust +enum Action { + Say(String), + MoveTo(i32, i32), + ChangeColorRGB(u16, u16, u16), +} + +fn main() { + let actions = [ + Action::Say("Hello Rust".to_string()), + Action::MoveTo(1,2), + Action::ChangeColorRGB(255,255,0), + ]; + for action in actions { + match action { + Action::Say(s) => { + println!("{}", s); + }, + Action::MoveTo(x, y) => { + println!("point from (0, 0) move to ({}, {})", x, y); + }, + Action::ChangeColorRGB(r, g, _) => { + println!("change color into '(r:{}, g:{}, b:0)', 'b' has been ignored", + r, g, + ); + } + } + } +} +``` + +运行后输出: +```console +$ cargo run + Compiling world_hello v0.1.0 (/Users/sunfei/development/rust/world_hello) + Finished dev [unoptimized + debuginfo] target(s) in 0.16s + Running `target/debug/world_hello` +Hello Rust +point from (0, 0) move to (1, 2) +change color into '(r:255, g:255, b:0)', 'b' has been ignored +``` + +#### 穷尽匹配 +在文章的开头,我们简单总结过`match`的匹配必须穷尽所有情况,下面来举例说明,例如: +```rust +enum Direction { + East, + West, + North, + South, +} + +fn main() { + let dire = Direction::South; + match dire { + Direction::East => println!("East"), + Direction::North | Direction::South => { + println!("South or North"); + }, + }; +} +``` + +我们没有处理`Direction::West`的情况,因此会报错: +```rust +error[E0004]: non-exhaustive patterns: `West` not covered // 非穷尽匹配,`West`没有被覆盖 + --> src/main.rs:10:11 + | +1 | / enum Direction { +2 | | East, +3 | | West, + | | ---- not covered +4 | | North, +5 | | South, +6 | | } + | |_- `Direction` defined here +... +10 | match dire { + | ^^^^ pattern `West` not covered // 模式`West`没有被覆盖 + | + = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms + = note: the matched value is of type `Direction` +``` + +首先,不禁想感叹,`Rust`的编译器真**强大,忍不住爆粗口了,sorry,如果你以后进一步深入使用Rust也会像我这样感叹的。 + +其次,Rust知道`match`中没有覆盖的具体分支,甚至知道哪些模式被遗忘了。这种设计初心是为了保证我们处理所有的情况,特别是那种会造成十亿美金的空值问题。 + +#### `_` 通配符 + +Rust 也提供了一个**模式**用于不想列举出所有可能值的场景。例如,`u8` 可以拥有 0 到 255 的有效的值,如果我们只关心 1、3、5 和 7 这几个值,就并不想列出其它的 0、2、4、6、8、9 一直到 255 的值。所幸, 我们不必这么做, 因为可以使用使用特殊的模式 `_` 替代: + +```rust +let some_u8_value = 0u8; +match some_u8_value { + 1 => println!("one"), + 3 => println!("three"), + 5 => println!("five"), + 7 => println!("seven"), + _ => (), +} +``` + +通过将其放置于其他分支后,`_`将会匹配所有遗漏的值。`()`表示啥都不做的意思,所以当匹配到`_`后,什么也不会发生。 + +然后,在某些场景下,我们其实只关心**某一个值是否存在**,此时`match`就显得过于啰嗦,还好,Rust提供了`if let`. + +## `if let`匹配 +很多时候都会遇到只有一个模式的值需要被处理,其它值直接忽略的场景,用`match`处理如下: +```rust + let v = Some(3u8); + match v{ + Some(3) => println!("three"), + _ => (), + } +```` + +我们想要对 `Some(3)` 模式进行匹配, 同时不想处理任何其他 `Some` 值或 `None` 值。但是为了满足`match`表达式(穷尽性)的要求,必须在处理完这唯一的成员后加上 `_ => ()`,这样也要增加很多样板代码。 + +杀鸡焉用牛刀,可以用`if let`的方式来实现: +```rust +if let Some(3) = some_u8_value { + println!("three"); +} +``` + +这两种匹配对于新手来说,可能有些难以抉择,但是只要记住一点就好: **当你只要匹配一个条件,且忽略其他条件时就用`if let`,否则都用match**. + + +## 变量覆盖 +无论是是`match`还是`if let`,他们都可以在模式匹配时覆盖掉老的值,绑定新的直: +```rust +fn main() { + let age = Some(30); + println!("在匹配前,age是{:?}",age); + if let Some(age) = age { + println!("匹配出来的age是{}",age); + } + + println!("在匹配后,age是{:?}",age); +} +``` + +`cargo run`运行后输出如下: +```console +在匹配前,age是Some(30) +匹配出来的age是30 +在匹配后,age是Some(30) +``` + +可以看出在`if let`中,`=`右边`Some(i32)`类型的`age`被左边`i32`类型的新`age`覆盖了,该覆盖一直持续到`if let`语句块的结束。因此第三个`println!`输出的`age`依然是`Some(i32)`类型。 + +对于`match`类型也是如此: +```rust +fn main() { + let age = Some(30); + println!("在匹配前,age是{:?}",age); + match age { + Some(age) => println!("匹配出来的age是{}",age), + _ => () + } + println!("在匹配后,age是{:?}",age); +} +``` \ No newline at end of file diff --git a/src/basic/match-pattern/option.md b/src/basic/match-pattern/option.md new file mode 100644 index 00000000..9cacc4c6 --- /dev/null +++ b/src/basic/match-pattern/option.md @@ -0,0 +1,55 @@ +# 解构Option + +在枚举那一章,提到过`Option`枚举变量是用来解决Rust中一个变量是否有值的问题,定义如下: +```rust +enum Option { + Some(T), + None, +} +``` +总而言之,**一个变量要么有值:`Some(T)`, 要么为空: `None`**. + +那么现在的问题就是该如何去使用这个`Option`枚举类型,根据我们上一节的经验,可以通过`match`来实现。 + +## 匹配 `Option` + +使用`Option`,是为了从 `Some` 中取出其内部的 `T` 值以及处理没有值的情况,为了演示这一点,下面一起来编写一个函数,它获取一个 `Option`,如果其中含有一个值,将其加一;如果其中没有值,则函数返回 `None` 值: + +```rust +fn plus_one(x: Option) -> Option { + match x { + None => None, + Some(i) => Some(i + 1), + } +} + +let five = Some(5); +let six = plus_one(five); +let none = plus_one(None); +``` + +`plus_one`接受一个`Option`类型的参数,同时返回一个`Option`类型的值(这种形式的函数在标准库内随处所见),在该函数的内部处理中,如果传入的是一个`None`,则返回一个`None`切不做任何处理;如果传入的是一个`Some(i32)`,则通过模式绑定,把其中的值绑定到变量`i`上,然后返回`i+1`的值,同时用`Some`进行包裹。 + +进一步说明,假设`plus_one`函数接受的参数值x是`Some(5)`,那么接着我们将`x`与各个分支进行比较。 + +#### 传入参数`Some(5)` +```rust,ignore +None => None, +``` +首先是`None`分支,因为值`Some(5)` 并不匹配模式 `None`,所以继续匹配下一个分支。 + +```rust,ignore +Some(i) => Some(i + 1), +``` + +`Some(5)` 与 `Some(i)` 匹配吗?当然匹配!它们是相同的成员。`i` 绑定了 `Some` 中包含的值,所以 `i` 的值是 `5`。接着匹配分支的代码被执行,所以我们将 `i` 的值加一并返回一个含有值 `6` 的新 `Some`。 + +#### 传入参数None +接着考虑下`plus_one` 的第二个调用,这次传入的`x` 是`None`。我们进入 `match` 并与第一个分支相比较。 + +```rust,ignore +None => None, +``` + +匹配上了!接着程序继续执行该分支后的代码:返回表达式`None`的值,也就是返回一个`None`,因为第一个分支就匹配到了,其他的分支将不再比较。 + diff --git a/src/basic/match-pattern/pattern-match.md b/src/basic/match-pattern/pattern-match.md new file mode 100644 index 00000000..2ff3afbe --- /dev/null +++ b/src/basic/match-pattern/pattern-match.md @@ -0,0 +1,116 @@ +# 模式和匹配 + +## 模式 +模式是Rust中的特殊语法,它用来匹配类型中的结构和数据,它往往和`match`表达式联用,以实现强大的模式匹配能力。模式一般由以下内容组合而成: +- 字面值 +- 解构的数组、枚举、结构体或者元组 +- 变量 +- 通配符 +- 占位符 + + +### 所有可能用到模式的地方 + +#### match分支** + +```rust +match VALUE { + PATTERN => EXPRESSION, + PATTERN => EXPRESSION, + PATTERN => EXPRESSION, +} +``` +如上所示,`match`的每个分支就是一个**模式**,因为`match`匹配是穷尽式的,因此我们往往需要一个特殊的模式`_`,来匹配剩余的所有情况: +```rust +match VALUE { + PATTERN => EXPRESSION, + PATTERN => EXPRESSION, + _ => EXPRESSION, +} +``` + +#### if let分支 +`if let`往往用于匹配一个模式,而忽略剩下的所有模式的场景: +```rust +if let Pattern = SOME_VALUE { + +} +``` + +#### while let条件循环 +一个与 `if let` 结构类似的是 `while let` 条件循环,它允许只要模式匹配就一直进行 `while` 循环。下面展示了一个使用`while let`的例子: +```rust +// Vec是动态数组 +let mut stack = Vec::new(); + +// 向数组尾部插入元素 +stack.push(1); +stack.push(2); +stack.push(3); + +// stack.pop从数组尾部弹出元素 +while let Some(top) = stack.pop() { + println!("{}", top); +} +``` + +这个例子会打印出 `3`、`2` 接着是 `1`。`pop` 方法取出动态数组的最后一个元素并返回 `Some(value)`,如果动态数组是空的,它返回 `None`。对于`while`来说,只要 `pop` 返回 `Some` 就会一直不停的循环。一旦其返回 `None``,while` 循环停止。我们可以使用 `while let` 来弹出栈中的每一个元素。 + +你也可以用`loop` + `if let` 或者`match`来实现,但是会更加啰嗦。 + +#### for循环 +```rust +let v = vec!['a', 'b', 'c']; + +for (index, value) in v.iter().enumerate() { + println!("{} is at index {}", value, index); +} +``` + +这里使用 `enumerate` 方法产生一个迭代器,该迭代器每次迭代会返回一个`(索引,值)`形式的元组,同时用`(index,value)`来匹配。 + +#### let语句 + +```rust +let PATTERN = EXPRESSION; +``` +是的,这个语句我们已经用了无数次了,它也是一种模式匹配: +```rust +let x = 5; +``` +这其中,`x`也是一种模式绑定,代表将**匹配的值绑定到变量x上**.因此,在Rust中,**变量名也是一种模式**,只不过它比较朴素很不起眼罢了。 + +```rust +let (x, y, z) = (1, 2, 3); +``` + +上面将一个元组与模式进行匹配(**模式和值的类型比较相同!**),然后把`1,2,3`分别绑定到`x,y,z`上。 + +因为模式匹配要求两边的类型必须相同,导致了下面的代码会报错: +```rust +let (x, y) = (1, 2, 3); +``` +因为对于元组来说,元素个数也是类型的一部分! + +#### 函数参数 +函数参数也是模式: +```rust +fn foo(x: i32) { + // 代码 +} +``` +其中`x`就是一个模式,你还可以在参数中匹配元组: +```rust +fn print_coordinates(&(x, y): &(i32, i32)) { + println!("Current location: ({}, {})", x, y); +} + +fn main() { + let point = (3, 5); + print_coordinates(&point); +} +``` +`&(3,5)`会匹配模式`&(x,y)`,因此`x`得到了`3`,`y`得到了`5`. + + +#### \ No newline at end of file diff --git a/src/basic/scompound-type/tring-slice.md b/src/basic/scompound-type/tring-slice.md new file mode 100644 index 00000000..8222e249 --- /dev/null +++ b/src/basic/scompound-type/tring-slice.md @@ -0,0 +1 @@ +# 字符串与切片 diff --git a/src/advance/trait.md b/src/basic/trait.md similarity index 100% rename from src/advance/trait.md rename to src/basic/trait.md diff --git a/src/img/.DS_Store b/src/img/.DS_Store index 81b3f334..2239cbb7 100644 Binary files a/src/img/.DS_Store and b/src/img/.DS_Store differ diff --git a/src/performance/runtime-check.md b/src/performance/runtime-check.md index 5b5ced71..dc8de8a6 100644 --- a/src/performance/runtime-check.md +++ b/src/performance/runtime-check.md @@ -1 +1,22 @@ -# 减少runtime check \ No newline at end of file +# 减少runtime check + + +## 减少集合访问的边界检查 + +以下代码,我们实现了两种循环方式: +```rust +// 第一种 +let collection = [1, 2, 3, 4, 5]; +for i in 0..collection.len() { + let item = collection[i]; + // ... +} + +// 第二种 +for item in collection { + +} +``` + +第一种方式是循环索引,然后通过索引下标去访问集合,第二种方式是直接循环迭代集合中的元素,优劣如下: +- **性能**:第一种使用方式中`collection[index]`的索引访问,会因为边界检查(bounds checking)导致运行时的性能损耗 - Rust会检查并确认`index`是落在集合内也就是合法的,但是第二种直接迭代的方式就不会触发这种检查,因为编译器会在编译时就完成分析并证明这种访问是合法的` \ No newline at end of file diff --git a/src/traps/intro.md b/src/traps/intro.md new file mode 100644 index 00000000..3e9811cb --- /dev/null +++ b/src/traps/intro.md @@ -0,0 +1,3 @@ +# 常见陷阱 todo + +本章收集各种Rust使用中常见的陷阱,帮助大家提前规避或者在遇到时能够迅速解决。 \ No newline at end of file