diff --git a/src/appendix/difficulties.md b/src/appendix/difficulties.md index 1d24284b..cf756867 100644 --- a/src/appendix/difficulties.md +++ b/src/appendix/difficulties.md @@ -3,4 +3,10 @@ 不可否认,Rust难点很多,而且知识点也很多,随着时间的进行,或多或少都会对一些难点逐渐模糊,这些难点无法通过目录章节名进行索引,因此集中放在此附录中,进行索引,方便读者朋友查阅。 ### ? -?主要用于简化错误传播,在[这里](../basic/result-error/result.md#传播界的大明星:)有详细讲解。 \ No newline at end of file +?主要用于简化错误传播,在[这里](../basic/result-error/result.md#传播界的大明星:)有详细讲解。 + +### () +元类型,见[这里](../basic/base-type/char-bool.md#元类型)和[这里](../basic/base-type/function.md#无返回值) + +### ! +[永不返回](../basic/base-type/function.md#永不返回的函数) \ No newline at end of file diff --git a/src/basic/base-type/char-bool.md b/src/basic/base-type/char-bool.md index 5f8a12d1..d08dc29f 100644 --- a/src/basic/base-type/char-bool.md +++ b/src/basic/base-type/char-bool.md @@ -1,10 +1,10 @@ # 字符、布尔、元类型 -这三个类型所处的地位比较尴尬,你说它们重要吧,在需要的时候也是不可或缺,说它们不重要吧,确实出现的身影不是很多,而且这三个类型都有一个共同点:简单,因此我们统一放在一起讲。 +这三个类型所处的地位比较尴尬,你说它们重要吧,有时候也是不可或缺,说它们不重要吧,确实出现的身影不是很多,而且这三个类型都有一个共同点:简单,因此我们统一放在一起讲。 ## 字符类型(char) -字符,对于没有其它编程经验的新手来说可能不太好理解(没有编程经验敢来学Rust的绝对是好汉),如果用字母来表示就很好理解了。下文我们用`char`来指代字符。 +字符,对于没有其它编程经验的新手来说可能不太好理解(没有编程经验敢来学Rust的绝对是好汉),但是你可以把它理解为英文中的字母,中文中的汉字。 下面的代码展示了几个颇具异域风情的字符: ``` @@ -16,9 +16,9 @@ fn main() { } ``` -如果你从部分陈旧的语言来,可能会大喊一声:这XX叫字符?是的,在Rust语言中这些都是字符,Rust的字符不仅仅是`ASCII`,所有的`Unicode`值都可以作为Rust字符,包括中文/日文/韩文,emoji表情等等,都是合法的字符类型。Unicode 值的范围从 U+0000~U+D7FF 和 U+E000~U+10FFFF。不过“字符”并不是 Unicode 中的一个概念,所以人在直觉上对“字符”的理解和 Rust 的字符概念并不一致。 +如果大家从部分陈旧的语言来,可能会大喊一声:这XX叫字符?是的,在Rust语言中这些都是字符,Rust的字符不仅仅是`ASCII`,所有的`Unicode`值都可以作为Rust字符,包括中文/日文/韩文,emoji表情等等,都是合法的字符类型。`Unicode` 值的范围从 `U+0000~U+D7FF`和 `U+E000~U+10FFFF`。不过“字符”并不是 `Unicode` 中的一个概念,所以人在直觉上对“字符”的理解和 Rust 的字符概念并不一致。 -由于Unicode都是4个字节编码,因此字符类型也是占用4个字节: +由于`Unicode`都是4个字节编码,因此字符类型也是占用4个字节: ```rust fn main() { let x = '中'; @@ -39,7 +39,7 @@ $ cargo run ## 布尔(bool) -Rust 中的布尔类型有两个可能的值:`true` 和 `false`。布尔值占用内存的大小为 1 个字节。布尔类型使用 `bool` 指定。例如: +Rust 中的布尔类型有两个可能的值:`true` 和 `false`, 布尔值占用内存的大小为 `1` 个字节: ```rust fn main() { @@ -53,14 +53,15 @@ fn main() { } ``` -使用布尔类型的场景主要在于控制流,例如上述代码的中的`if`就是其中之一。 +使用布尔类型的场景主要在于流程控制,例如上述代码的中的`if`就是其中之一。 ## 元类型 元类型就是`()`,对,你没看错,就是`()`,唯一的值也是`()`,可能读者读到这里就不愿意了,你也太敷衍了吧,管这叫类型? 只能说,再不起眼的东西,都有其用途,在目前为止的学习过程中,大家已经看到过很多次`fn main()`函数的使用吧?那么这个函数返回什么呢? -没错就是这个元类型`()`,你不能说`main`函数无返回值,因为没有返回值的函数在Rust中是有单独的定义的:`发散函数`,顾名思义,无法收敛的函数. -例如常见的`println!()`的返回值也是`()`。 +没错,`main`函数就返回这个元类型`()`,你不能说`main`函数无返回值,因为没有返回值的函数在Rust中是有单独的定义的:`发散函数`,顾名思义,无法收敛的函数. + +例如常见的`println!()`的返回值也是元类型`()`。 diff --git a/src/basic/base-type/function.md b/src/basic/base-type/function.md index dadb8966..0ff723b4 100644 --- a/src/basic/base-type/function.md +++ b/src/basic/base-type/function.md @@ -14,9 +14,9 @@ fn add(i: i32, j: i32) -> i32 { -当你看懂了这张图,其实就等于差不多完成了函数章节的学习。但是这么短的章节显然对不起读者老爷们的厚爱,所以我们来展开下。 +当你看懂了这张图,其实就等于差不多完成了函数章节的学习,但是这么短的章节显然对不起读者老爷们的厚爱,所以我们来展开下。 -函数有以下需要注意的点: +## 函数要点 - 函数名和变量名使用[蛇形命名法(snake case)](../../style-guide/naming.md),例如`fn add_two() -> {}` - 函数的位置可以随便放,Rust不关心我们在哪里定义了函数,只要有定义即可 - 每个函数参数都需要标注类型 @@ -67,7 +67,7 @@ help: if this is a type, explicitly ignore the parameter name // 如果y是一 ``` ## 函数返回 -在上一章节语句和表达式中,我们提到在Rust中,函数就是表达式,因此我们可以把函数的返回值直接赋给调用者。不像有些语言,会给返回值一个名称,在Rust中,只需要声明返回值的类型即可(在`->`之后,`{`之前})。 +在上一章节语句和表达式中,我们有提到在在: Rust中函数就是表达式,因此我们可以把函数的返回值直接赋给调用者。 函数的返回值就是函数体最后一条表达式的返回值,当然我们也可以使用`return`提前返回,下面的函数使用最后一条表达式来返回一个值: ```rust @@ -137,7 +137,7 @@ fn clear(text: &mut String) -> () { } ``` -在实际编程中,你会经常在错误提示中看到该`()`的身影出没,假如你的函数需要返回一个`u32`值,但是如果你不幸的以`表达式+;`的方式作为函数的最后一行代码,就会报错: +在实际编程中,你会经常在错误提示中看到该`()`的身影出没,假如你的函数需要返回一个`u32`值,但是如果你不幸的以`表达式;`的方式作为函数的最后一行代码,就会报错: ```rust fn add(x:u32,y:u32) -> u32 { x + y; diff --git a/src/basic/base-type/index.md b/src/basic/base-type/index.md index e6f1e503..a00687b8 100644 --- a/src/basic/base-type/index.md +++ b/src/basic/base-type/index.md @@ -1,6 +1,7 @@ # 基本类型 +当一门语言不谈类型时,你得小心,这大概率是动态语言(别拍我,我承认是废话)。但是把类型大张旗鼓的用多个章节去讲的,Rust是其中之一。 -基本数据类型在Rust中是最最常见的数据类型,基本类型意味着它们往往是一个最小化原子类型,无法解构为其它类型(一般意义上来说),基本类型由以下组成: +Rust每个值都有其确切的数据类型, 总的来说可以分为两类:基本类型和复合类型, 基本数据类型在Rust中是最最常见的数据类型,基本类型意味着它们往往是一个最小化原子类型,无法解构为其它类型(一般意义上来说),基本类型由以下组成: - 数值类型: 有符号整数 (`i8`, `i16`, `i32`, `i64`, `isize`)、 无符号整数 (`u8`, `u16`, `u32`, `u64`, `usize`) 、浮点数 (`f32`, `f64`)、以及有理数、复数 - 字符串:字符串字面量、字符串切片&str和堆分配字符串String @@ -11,15 +12,14 @@ ## 类型推导与标注 -与python、js等动态语言不同,Rust是一门静态类型语言,也就是编译器必须在编译期知道我们所有变量的数据类型,但这不意味着你需要为你的每个变量指定类型,因为**Rust编译器很聪明,它可以根据变量的值和使用方式来自动推导出变量的类型**,同时编译器也不够聪明,在某些情况下,它无法推导出变量类型,需要我们手动去给予一个类型标注,关于这一点在[Rust语言初印象](../first-try/hello-world.md#Rust语言初印象)中有过展示. - -再比如以下代码(引用自Rust官方编程那本书): +与`python`、`javascript`等动态语言不同,Rust是一门静态类型语言,也就是编译器必须在编译期知道我们所有变量的数据类型,但这不意味着你需要为你的每个变量指定类型,因为**Rust编译器很聪明,它可以根据变量的值和使用方式来自动推导出变量的类型**,同时编译器也不够聪明,在某些情况下,它无法推导出变量类型,需要我们手动去给予一个类型标注,关于这一点在[Rust语言初印象](../../first-try/hello-world.md#Rust语言初印象)中有过展示. +来看段代码: ```rust let guess = "42".parse().expect("Not a number!"); ``` -如果这里不添加类型标注,则编译器会报错: +让我们先忽悠`.parse().expect..`部分,总之,这段代码将字符串`"42"`进行解析,但是我们没有标注想要解析的类型,因此编译器会报错: ```console $ cargo build Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations) @@ -30,5 +30,5 @@ error[E0282]: type annotations needed | ^^^^^ consider giving `guess` a type ``` -意味着,我们需要提供给编译器更多的信息,例如给`guess`变量一个显示的类型标注: `let guess:i32 = ...` 或者`"42".parse::()`. +意味着,我们需要提供给编译器更多的信息,例如给`guess`变量一个**显示的类型标注**: `let guess:i32 = ...` 或者`"42".parse::()`. diff --git a/src/basic/base-type/numbers.md b/src/basic/base-type/numbers.md index 6abc46d0..2882a34b 100644 --- a/src/basic/base-type/numbers.md +++ b/src/basic/base-type/numbers.md @@ -1,6 +1,7 @@ # 数值类型 +我朋友有一个领导(读者:你朋友?黑人问号)说过一句话:所有代码就是0和1,简单的很。咱不评价这句话的正确性,但是计算机底层由01组成,倒是真的。 -计算机和数值关联在一起的时间,远比你想象的要长,因此数值类型可以说是有计算机以来就有的类型,下面内容将深入讨论Rust的数值类型以及相关的运算符。 +计算机和数值关联在一起的时间,远比我们想象的要长,因此数值类型可以说是有计算机以来就有的类型,下面内容将深入讨论Rust的数值类型以及相关的运算符。 ## 整数和浮点数 @@ -10,9 +11,8 @@ Rust使用一个相对传统的语法来创建整数(`1`,`2`,...)和浮点数(`1 #### 整数类型 -**整数**是没有是没有小数部分的数字。在之前,我们使用过`i32`类型, 表示有符号的32位整数(`i` 是英文单词 *integer* 的首字母,与之相反的是 `u`,代表无符号 `unsigned` 类型)。下表显示了 Rust 中的内置的整数类型, 在有符号和和无符号的列中(例如 *i16*)的每个定义形式都可用于声明整数类型。 +**整数**是没有小数部分的数字。之前使用过的`i32`类型, 表示有符号的32位整数(`i` 是英文单词 *integer* 的首字母,与之相反的是 `u`,代表无符号 `unsigned` 类型)。下表显示了 Rust 中的内置的整数类型: -Rust 中的整数类型 | 长度 | 有符号类型 | 无符号类型 | |------------|---------|---------| @@ -26,14 +26,12 @@ Rust使用一个相对传统的语法来创建整数(`1`,`2`,...)和浮点数(`1 类型定义的形式统一为:有无符号 + 类型大小(位数)。**无符号数**表示数字只能取正数,而**有符号**则表示数字即可以取正数又可以取负数。就像在纸上写数字一样:当要强调符号时,数字前面可以带上正号或负号;然而,当很明显确定数字为正数时,就不需要加上正号了。有符号数字以[二进制补码](https://en.wikipedia.org/wiki/Two%27s_complement)形式存储。 每个有符号类型规定的数字范围是 -(2n - 1) ~ 2n - -1 - 1,其中 `n` 是该定义形式的位长度。所以 `i8` 可存储数字范围是 -(27) ~ 27 - 1,即 -128 ~ 127。无符号类型可以存储的数字范围是 0 ~ 2n - 1,所以 `u8` 能够存储的数字为 0 ~ 28 - 1,即 0 ~ 255。 +1 - 1,其中 `n` 是该定义形式的位长度。因此 `i8` 可存储数字范围是 -(27) ~ 27 - 1,即 -128 ~ 127。无符号类型可以存储的数字范围是 0 ~ 2n - 1,所以 `u8` 能够存储的数字为 0 ~ 28 - 1,即 0 ~ 255。 -此外,`isize` 和 `usize` 类型取决于程序运行的计算机cpu类型: 若cpu是32位的,则这两个类型是32位的,同理,若cpu是64位,那么它们则是64位64位. +此外,`isize` 和 `usize` 类型取决于程序运行的计算机cpu类型: 若cpu是32位的,则这两个类型是32位的,同理,若cpu是64位,那么它们则是64位. +整形字面量可以用下表的形式书写: -整型的字面量可以可以写成下表 3-2 中任意一种。注意,除了字节字面量之外的所有的数字字面量都允许使用类型后缀,例如 `57u8`,还有可以使用 `_` 作为可视分隔符,如 `1_000`。 - -表 3-2: Rust 的整型字面量 | 数字字面量 | 示例 | |------------------|---------------| @@ -43,13 +41,14 @@ Rust使用一个相对传统的语法来创建整数(`1`,`2`,...)和浮点数(`1 | 二进制 | `0b1111_0000` | | 字节 (仅限于 `u8`) | `b'A'` | -那么该使用哪种类型的整型呢?如果不确定,Rust 的默认值通常是个不错的选择,整型默认是 `i32`:这通常是最快的,即便在 64 位系统上也是。`isize` 和 `usize` 的主要应用场景是用作某些集合的索引。 + +这么多类型,有没有一个简单的使用准则?答案是肯定的,Rust整形默认使用`i32`,例如`let i = 1`,那`i`就是`i32`类型,因此你可以首选它,同时该类型也往往是性能最好的。 `isize` 和 `usize` 的主要应用场景是用作集合的索引。 > ##### 整型溢出 > -> 比方说有一个 `u8` ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生**整型溢出**。关于这一行为 Rust 有一些有趣的规则。当在 debug 模式编译时,Rust 会检查整型溢出若存在这些问题则使程序在编译时 *panic*。Rust 使用这个术语来表明程序因错误而退出。 [该章节](../../errors/panic.md)会详细介绍 panic。 +> 比方说有一个 `u8` ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生**整型溢出**。关于这一行为 Rust 有一些有趣的规则:当在 debug 模式编译时,Rust 会检查整型溢出,若存在这些问题,则使程序在编译时 *panic*(崩溃,Rust 使用这个术语来表明程序因错误而退出)。 > -> 在当使用 `--release` 参数进行 release 模式构建时,Rust **不**检测溢出。相反当检测到整型溢出时,Rust 会进行一种被称为二进制补码的方式进行(*two’s complement wrapping*)操作。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 `u8` 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖整型溢出包裹的行为不是一种正确的做法。 +> 在当使用 `--release` 参数进行 release 模式构建时,Rust **不**检测溢出。相反当检测到整型溢出时,Rust 会进行一种被称为二进制补码的方式进行(*two’s complement wrapping*)操作。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 `u8` 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖整型溢出的行为不是一种正确的做法。 > > 要显式处理溢出的可能性,可以使用标准库针对原始数字类型提供的以下的一系列方法: > @@ -60,7 +59,7 @@ Rust使用一个相对传统的语法来创建整数(`1`,`2`,...)和浮点数(`1 #### 浮点类型 -**浮点类型数字** 是带有小数点的数字,在 Rust 中浮点类型数字也有两种基本类型: `f32` 和 `f64`,分别为 32 位和 64 位大小。默认浮点类型是 `f64`,因为在现代的 CPU 中它的速度与 `f32` 几乎相同,但精度更高。 +**浮点类型数字** 是带有小数点的数字,在 Rust 中浮点类型数字也有两种基本类型: `f32` 和 `f64`,分别为 32 位和 64 位大小。默认浮点类型是 `f64`,在现代的 CPU 中它的速度与 `f32` 几乎相同,但精度更高。 下面是一个演示浮点数的示例: @@ -72,11 +71,11 @@ fn main() { } ``` -浮点数根据 IEEE-754 标准表示。`f32` 类型是单精度浮点型,`f64` 为双精度。 +浮点数根据 `IEEE-754` 标准表示。`f32` 类型是单精度浮点型,`f64` 为双精度。 #### 数字运算 -Rust 支持所有数字类型的基本数学运算:加法、减法、乘法、除法和取模运算。下面代码演示了各使用一条 `let` 语句来说明相应运算的用法: +Rust 支持所有数字类型的基本数学运算:加法、减法、乘法、除法和取模运算。下面代码各使用一条 `let` 语句来说明相应运算的用法: ```rust @@ -105,43 +104,43 @@ fn main() { ```rust fn main() { - // 编译器会进行自动推导,给予twenty i32的类型 - let twenty = 20; - // 类型标注 - let twenty_one: i32 = 21; - // 通过类型后缀的方式进行类型标注:22是i32类型 - let twenty_two = 22i32; - - // 只有同样类型,才能运算 - let addition = twenty + twenty_one + twenty_two; - println!("{} + {} + {} = {}", twenty, twenty_one, twenty_two, addition); - - // 对于较长的数字,可以用_进行分割,提升可读性 - let one_million: i64 = 1_000_000; - println!("{}", one_million.pow(2)); - - // 定义一个f32数组,其中42.0会自动被推导为f32类型 - let forty_twos = [ - 42.0, - 42f32, - 42.0_f32, - ]; + // 编译器会进行自动推导,给予twenty i32的类型 + let twenty = 20; + // 类型标注 + let twenty_one: i32 = 21; + // 通过类型后缀的方式进行类型标注:22是i32类型 + let twenty_two = 22i32; - // 打印数组中第一个值,其中控制小数位为2位 - println!("{:02}", forty_twos[0]); - } + // 只有同样类型,才能运算 + let addition = twenty + twenty_one + twenty_two; + println!("{} + {} + {} = {}", twenty, twenty_one, twenty_two, addition); + + // 对于较长的数字,可以用_进行分割,提升可读性 + let one_million: i64 = 1_000_000; + println!("{}", one_million.pow(2)); + + // 定义一个f32数组,其中42.0会自动被推导为f32类型 + let forty_twos = [ + 42.0, + 42f32, + 42.0_f32, + ]; + +// 打印数组中第一个值,其中控制小数位为2位 +println!("{:02}", forty_twos[0]); +} ``` #### 浮点数陷阱 浮点数由于底层格式的特殊性,导致了如果在使用浮点数时不够谨慎,就可能造成危险,有两个原因: 1. **浮点数往往是你想要数字的近似表达** -浮点数类型是基于二进制实现的,但是我们想要计算的数字往往是基于十进制,例如0.1在二进制上并不存在精确的表达形式,但是在十进制上就存在。这种不匹配性导致一定的歧义性,更多的,虽然浮点数能代表真实的数值,但是由于底层格式问题,它往往受限于定长的浮点数精度,如果你想要表达完全精准的真实数字,只有使用无限精度的浮点数才行 +浮点数类型是基于二进制实现的,但是我们想要计算的数字往往是基于十进制,例如`0.1`在二进制上并不存在精确的表达形式,但是在十进制上就存在。这种不匹配性导致一定的歧义性,更多的,虽然浮点数能代表真实的数值,但是由于底层格式问题,它往往受限于定长的浮点数精度,如果你想要表达完全精准的真实数字,只有使用无限精度的浮点数才行 2. **浮点数在某些特性上是反直觉的** -例如你觉得浮点数可以进行比较,对吧?是的,它们确实可以使用`>`,`>=`等进行比较,但是在某些场景下,这种直觉上的比较特性反而会害了你。因为`f32`,`f64`上的比较运算实现的是`std::cmp::PartialEq`[特征](../../advance/trait.md), 但是并没有实现`std::cmp::Eq`特征,但是后者在其它数值类型上都有定义,说了这么多,可能大家还是云里雾里,用一个例子来举例: +例如大家都会觉得浮点数可以进行比较,对吧?是的,它们确实可以使用`>`,`>=`等进行比较,但是在某些场景下,这种直觉上的比较特性反而会害了你。因为`f32`,`f64`上的比较运算实现的是`std::cmp::PartialEq`特征(类似其他语言的接口), 但是并没有实现`std::cmp::Eq`特征,但是后者在其它数值类型上都有定义,说了这么多,可能大家还是云里雾里,用一个例子来举例: -Rust的HashMap数据结构,是一个KV类型的hash map实现,它对于K没有特定类型的限制,但是要求能用作K的类型必须实现了`std::cmp::Eq`特征,因为这意味着你无法使用浮点数作为HashMap的Key,来存储键值对,但是作为对比,Rust的整数类型、字符串类型、布尔类型都实现了该特征,因此可以作为HashMap的Key。 +Rust的HashMap数据结构,是一个KV类型的hash map实现,它对于`K`没有特定类型的限制,但是要求能用作`K`的类型必须实现了`std::cmp::Eq`特征,因此这意味着你无法使用浮点数作为`HashMap`的`Key`,来存储键值对,但是作为对比,Rust的整数类型、字符串类型、布尔类型都实现了该特征,因此可以作为`HashMap`的`Key`。 为了避免上面说的两个陷阱,你需要遵守以下准则: - 避免在浮点数上测试相等性 @@ -163,22 +162,22 @@ fn main() { ```rust fn main() { - let abc: (f32, f32, f32) = (0.1, 0.2, 0.3); - let xyz: (f64, f64, f64) = (0.1, 0.2, 0.3); - - println!("abc (f32)"); - println!(" 0.1 + 0.2: {:x}", (abc.0 + abc.1).to_bits()); - println!(" 0.3: {:x}", (abc.2).to_bits()); - println!(); - - println!("xyz (f64)"); - println!(" 0.1 + 0.2: {:x}", (xyz.0 + xyz.1).to_bits()); - println!(" 0.3: {:x}", (xyz.2).to_bits()); - println!(); - - assert!(abc.0 + abc.1 == abc.2); - assert!(xyz.0 + xyz.1 == xyz.2); - } + let abc: (f32, f32, f32) = (0.1, 0.2, 0.3); + let xyz: (f64, f64, f64) = (0.1, 0.2, 0.3); + + println!("abc (f32)"); + println!(" 0.1 + 0.2: {:x}", (abc.0 + abc.1).to_bits()); + println!(" 0.3: {:x}", (abc.2).to_bits()); + println!(); + + println!("xyz (f64)"); + println!(" 0.1 + 0.2: {:x}", (xyz.0 + xyz.1).to_bits()); + println!(" 0.3: {:x}", (xyz.2).to_bits()); + println!(); + + assert!(abc.0 + abc.1 == abc.2); + assert!(xyz.0 + xyz.1 == xyz.2); +} ``` 运行该程序,输出如下: @@ -198,13 +197,13 @@ note: run with `RUST_BACKTRACE=1` environment variable to display ➥a backtrace ``` -仔细看,对`f32`类型做加法时,`0.1+0.2`的结果是`3e99999a`,0.3也是`3e99999a`,因此`f32`下的`0.1+0.2=0.3`通过测试,但是到了`f64`类型时,结果就不一样了,因为f64精度高很多,因此在小数点非常后面发生了一点微小的变化,`0.1+0.2`以`4`结尾,但是0.3以`3`结尾,因此`f64`下的测试失败了,并且抛出了异常。 +仔细看,对`f32`类型做加法时,`0.1+0.2`的结果是`3e99999a`,0.3也是`3e99999a`,因此`f32`下的`0.1+0.2=0.3`通过测试,但是到了`f64`类型时,结果就不一样了,因为f64精度高很多,因此在小数点非常后面发生了一点微小的变化,`0.1+0.2`以`4`结尾,但是0.3以`3`结尾,这个细微区别导致`f64`下的测试失败了,并且抛出了异常。 -是不是**blow your mind away**? 没关系,在本书的后续章节中类似的直击灵魂的代码还很多,这就是我们敢号称`Rust语言圣经`的勇气! +是不是**blow your mind away**? 没关系,在本书的后续章节中类似的直击灵魂的地方还很多,这就是敢号称`Rust语言圣经`的底气! #### NaN -对于数学上未定义的结果,例如对负数取平方根`-42.1.sqrt()`,会产生一个特殊的结果:Rust的浮点数类型使用NaN(not a number)来处理这些情况。 +对于数学上未定义的结果,例如对负数取平方根`-42.1.sqrt()`,会产生一个特殊的结果:Rust的浮点数类型使用`NaN`(not a number)来处理这些情况。 **所有跟`NaN`交互的操作,都会返回一个`NaN`**,而且`NaN`不能用来比较,下面的代码会崩溃: @@ -228,11 +227,11 @@ fn main() { ## 序列(Range) -Rust提供了一个非常遍历的方式,让我们能生成连续的数值,例如`1..5`,生成从1到4的连续数字,不包含5; `1..5`,生成从1到5的连续数字,包含5,它的用户很简单,常常用于循环中: +Rust提供了一个非常简洁的方式,用来生成连续的数值,例如`1..5`,生成从1到4的连续数字,不包含5; `1..=5`,生成从1到5的连续数字,包含5,它的用途很简单,常常用于循环中: ```rust - for i in 1..=5 { - println!("{}",i); - } +for i in 1..=5 { + println!("{}",i); +} ``` 最终程序输出: @@ -244,24 +243,18 @@ Rust提供了一个非常遍历的方式,让我们能生成连续的数值, 5 ``` -序列只允许用于数字或字符类型,因为编译器会在编译时检查序列不为空。字符和数字值是 Rust 仅有的可以判断范围是否为空的类型。 - -如下是一个使用字符类型序列的例子: +序列只允许用于数字或字符类型,因为编译器会在编译时检查序列不为空。如下是一个使用字符类型序列的例子: ```rust -let x = 'c'; - -match x { - 'a'..='j' => println!("early ASCII letter"), - 'k'..='z' => println!("late ASCII letter"), - _ => println!("something else"), +for i in 'a'..='z' { + println!("{}",i); } ``` ## 有理数和复数 Rust的标准库相比其它语言,对于准入的门槛较高,因此有理数和复数并未包含在标准库中: -- 有理数和复数对应的数据库 +- 有理数和复数 - 任意大小的整数和任意精度的浮点数 - 固定精度的十进制小数,常用于货币相关的场景 @@ -292,9 +285,9 @@ use num::complex::Complex; - **Rust拥有相当多的数值类型**. 因此你需要熟悉这些类型所占用的字节数,这样就知道该类型允许的大小范围以及你选择的类型是否能表达负数 - **类型转换必须是显式的**. Rust永远也不会偷偷把你的16bit整数转换成32bit整数 -- **Rust的数值上可以使用方法**. 例如你可以用以下方法来将24.5取整: `13.14_f32.round()`, 在这里我们使用了类型后缀,因为编译器需要知道`13.14`的具体类型 +- **Rust的数值上可以使用方法**. 例如你可以用以下方法来将`24.5`取整: `13.14_f32.round()`, 在这里我们使用了类型后缀,因为编译器需要知道`13.14`的具体类型 -数值类型的讲解已经基本结束,接下来来看看字符串。 +数值类型的讲解已经基本结束,接下来来看看字符和布尔类型。 diff --git a/src/basic/base-type/statement-expression.md b/src/basic/base-type/statement-expression.md index 8661df3c..39f70ebb 100644 --- a/src/basic/base-type/statement-expression.md +++ b/src/basic/base-type/statement-expression.md @@ -1,6 +1,6 @@ # 语句和表达式 -Rust的函数体是由一系列语句组成,然后最后由一个表达式来返回值,例如: +Rust的函数体是由一系列语句组成,最后由一个表达式来返回值,例如: ```rust fn add_with_extra(x: i32, y: i32) -> i32 { let x = x + 1; // 语句 @@ -10,9 +10,9 @@ fn add_with_extra(x: i32, y: i32) -> i32 { ``` 语句会执行一些操作但是不会返回一个值,而表达式会在求值后返回一个值,因此在上述函数体的三行代码中,前两行是语句,最后一行是表达式。 -对于Rust语言而言,**这种基于语句和表达式的方式是非常重要的,你需要能明确的区分这两个概念**, 但是对于其它很多语言而言,这两个往往无需区分。基于表达式是函数式语言的重要特征,表达式总要返回值。 +对于Rust语言而言,**这种基于语句和表达式的方式是非常重要的,你需要能明确的区分这两个概念**, 但是对于很多其它语言而言,这两个往往无需区分。基于表达式是函数式语言的重要特征,**表达式总要返回值**。 -在此之前,我们已经多次使用过语句和表达式, 先从语句讲起: +其实,在此之前,我们已经多次使用过语句和表达式。 ## 语句 @@ -25,6 +25,7 @@ let (a, c) = ("hi", false); 以上都是语句,它们完成了一个具体的操作,但是并没有返回值,因此是语句。 由于`let`是语句,因此不能将let语句赋值给其它值,如下形式是错误的: + ```rust let b = (let a = 8); ``` @@ -50,11 +51,16 @@ error[E0658]: `let` expressions in this position are experimental // 下面的`l ``` -以上的错误告诉我们`let`是语句,不是表达式,因此它不返回值,也就不能给其它变量赋值。但是该错误还透漏了一个重要的信息,`let`作为表达式已经是试验功能了,也许不久的将来,我们在[`stable rust`](../../appendix/rust-dev.md)下可以这样使用。 +以上的错误告诉我们`let`是语句,不是表达式,因此它不返回值,也就不能给其它变量赋值。但是该错误还透漏了一个重要的信息,`let`作为表达式已经是试验功能了,也许不久的将来,我们在[`stable rust`](../../appendix/rust-version.md)下可以这样使用。 ## 表达式 -表达式会进行求值,然后返回一个值。例如`5 + 6`,在求值后,返回值`11`,因此它就是一条表达式。表达式可以成为语句的一部分,例如`let y= 6`中,`6`就是一个表达式,它在求值后返回一个值`6`(有些反直觉,但是确实是表达式),调用一个函数是表达式,因为会返回一个值,调用宏也是表达式,用花括号包裹最终返回一个值的语句块也是表达式,总之,能返回值,它就是表达式,无论简单还是复杂。例如: +表达式会进行求值,然后返回一个值。例如`5 + 6`,在求值后,返回值`11`,因此它就是一条表达式。 + +表达式可以成为语句的一部分,例如`let y= 6`中,`6`就是一个表达式,它在求值后返回一个值`6`(有些反直觉,但是确实是表达式). + +调用一个函数是表达式,因为会返回一个值,调用宏也是表达式,用花括号包裹最终返回一个值的语句块也是表达式,总之,能返回值,它就是表达式: + ```rust fn main() { let y = { @@ -66,7 +72,7 @@ fn main() { } ``` -其中我们把一个语句块表达式的值赋给了y,语句块长这样: +上面使用一个语句块表达式将值赋给`y`变量,语句块长这样: ```rust { let x = 3; diff --git a/src/basic/intro.md b/src/basic/intro.md index f382f360..f5b8e8d5 100644 --- a/src/basic/intro.md +++ b/src/basic/intro.md @@ -1,17 +1,15 @@ # Rust基本概念 -从现在开始,你已经正式踏入了Rust大陆,这篇广袤而神秘的世界,在这个世界中,你将接触到很多之前你都没有听过的概念: +从现在开始,我们正式踏入了Rust大陆,这篇广袤而神秘的世界,在这个世界中,将接触到很多之前都没有听过的概念: - 所有权、借用、生命周期 - 宏编程 - 模式匹配 -类似的还有很多,不过不用怕,一方面,这本书会带你彻底探索这个神秘的大陆. +类似的还有很多,不过不用怕,引用武林外传一句话:咱上面有人。有本书在,一切虚妄终将烟消云散。 本章主要介绍Rust的基础语法、数据类型、项目结构等,学完本章,你将对Rust代码有一个清晰、完整的认识。 -在基本概念方面,Rust和其它语言并没有大的区别,它拥有变量、数据、函数等等。 - -通过下面的代码我们来简单浏览下Rust的语法: +开始之前先通过一段代码来简单浏览下Rust的语法: ```rust // rust程序入口函数,跟其它语言一样,都是main,该函数目前无返回值 @@ -45,8 +43,8 @@ fn main() { ``` > 注意 -在上面的`add`函数中,不要为`i+j`添加`;`,这会改变语法导致函数返回`()`而不是`i32`,具体参见[语句和表达式](./statement-expression.md) +>在上面的`add`函数中,不要为`i+j`添加`;`,这会改变语法导致函数返回`()`而不是`i32`,具体参见[语句和表达式](./base-type/statement-expression.md) -还有几点可以留意下: -- 字符串使用双引号`""`而不是单引号`''`.Rust中单引号是留给单个字符类型(`char`)使用的 -- Rust使用`{}`来作为占位符,其它语言可能使用的是`%s`,`%d`,`%p`等,因为`println!`会自动推导出具体的类型, 无需手动指定 \ No newline at end of file +有几点可以留意下: +- 字符串使用双引号`""`而不是单引号`''`, Rust中单引号是留给单个字符类型(`char`)使用的 +- Rust使用`{}`来作为格式化输出占位符,其它语言可能使用的是`%s`,`%d`,`%p`等,由于`println!`会自动推导出具体的类型, 因此无需手动指定 \ No newline at end of file diff --git a/src/basic/variable.md b/src/basic/variable.md index 6c44fe9d..588b779b 100644 --- a/src/basic/variable.md +++ b/src/basic/variable.md @@ -1,22 +1,20 @@ # 变量绑定与解构 -> 本节在内容上部分参考了[Rust Book](https://doc.rust-lang.org/stable/book/) - -鉴于本书的[目标读者](../intro.md)已经熟练掌握其它任意一门编程语言,因此这里就不再对何为变量进行赘述,让我们开门见山来谈谈,为何Rust选择了手动设定变量可变性。 +鉴于本书的目标读者(别慌,来到这里就说明你就是目标读者)已经熟练掌握其它任意一门编程语言,因此这里就不再对何为变量进行赘述,让我们开门见山来谈谈,为何Rust选择了手动设定变量的可变性。 ## 为何要手动设置变量的可变性? -在其它大多数语言中,变量一旦创建,要么是可变的,要么是不可变的(ClosureScript),前者为编程提供了灵活性,后者为编程提供了安全性,而Rust选择了两者我都要,既要灵活性又要安全性。 +在其它大多数语言中,变量一旦创建,要么是可变的,要么是不可变的(ClosureScript),前者为编程提供了灵活性,后者为编程提供了安全性,而Rust比较野,选择了两者我都要,既要灵活性又要安全性。 -能想要学习Rust,说明我们的读者都是相当有水平的程序员了,你们应该能理解一切选择都是权衡,那么两者都要的权衡是什么呢?这就是Rust开发团队为我们做出的贡献,两者都要意味着底层代码的实现复杂度大幅提升,Respect to The Rust Team! +能想要学习Rust,说明我们的读者都是相当有水平的程序员了,你们应该能理解**一切选择皆是权衡**,那么两者都要的权衡是什么呢?这就是Rust开发团队为我们做出的贡献,两者都要意味着Rust语言底层代码的实现复杂度大幅提升,因此Respect to The Rust Team! -除了以上两个优点,还有一个很大的优点,那就是运行性能上的提升,因为将本身无需改变的变量声明为不可变在运行期会避免一些多余的runtime检查。 +除了以上两个优点,还有一个很大的优点,那就是运行性能上的提升,因为将本身无需改变的变量声明为不可变在运行期会避免一些多余的`runtime`检查。 ## 变量命名 -在命名方面,和其它语言没有区别,不过当你给变量命名时,需要遵循[Rust命名规范](../style-guide/naming.md)。 +在命名方面,和其它语言没有区别,不过当给变量命名时,需要遵循[Rust命名规范](../style-guide/naming.md)。 -> Rust语言有一些**关键字**(*keywords*),和其他语言一样,这些关键字都是被保留给Rust语言使用的,因此,这些关键字不能被用作变量或函数的名称。 在[附录 A](../appendix/keywords) 中可找到关键字列表。 +> Rust语言有一些**关键字**(*keywords*),和其他语言一样,这些关键字都是被保留给Rust语言使用的,因此,它们不能被用作变量或函数的名称。 在[附录 A](../appendix/keywords) 中可找到关键字列表。 ## 变量绑定 @@ -24,7 +22,7 @@ 为何不用赋值而用绑定呢(其实你也可以称之为赋值,但是绑定的含义更清晰准确)?这里就涉及Rust最核心的原则-**所有权**,简单来讲,任何内存对象都是有主人的,而且一般情况下完全属于它的主人,绑定就是把这个对象绑定给一个变量,让这个变量成为它的主人(聪明的读者应该能猜到,在这种情况下,该对象之前的主人就会丧失对该对象的所有权),像极了我们的现实世界,不是吗? -至于为何要采用所有权这种复杂的东东,先别急,等讲[Rust核心概念](../core/intro.md)时,我们会为你详细道来。 +至于为何要采用所有权这种复杂的东东,先别急,等时机合适,我们会为你详细道来。 ## 变量可变性 @@ -34,7 +32,7 @@ Rust的变量在默认情况下是**不可变的**。在上文提到过,这是 然后在新建的 *variables* 目录下,打开 *src/main.rs* 并将代码替换为下面还未能通过编译的代码: -```rust,ignore,does_not_compile +```rust fn main() { let x = 5; println!("The value of x is: {}", x); @@ -65,7 +63,7 @@ error: aborting due to previous error 具体的错误原因是 `cannot assign twice to immutable variable x`(对不可变的变量无法进行二次再赋值),因为我们尝试给不可变的 `x` 变量赋予第二个值。 -这种错误是为了避免无法预期的错误发生在我们的变量上:一部分代码假定该变量的值永远不会改变,而另外一部分代码却无情的改变了这个值,在实际开发过程中,这个错误是很难被发现的,特别是在多线程编程中。 +这种错误是为了避免无法预期的错误发生在我们的变量上:一个变量往往被多处代码所使用,其中一部分代码假定该变量的值永远不会改变,而另外一部分代码却无情的改变了这个值,在实际开发过程中,这个错误是很难被发现的,特别是在多线程编程中。 这种规则让我们的代码变得非常清晰,只有你想让你的变量改变时,它才能改变,这样就不会造成心智上的负担,也给别人阅读代码带来便利。 @@ -73,7 +71,7 @@ error: aborting due to previous error 在Rust中,可变性很简单,只要在变量名前加一个`mut`即可, 而且这种显式的声明方式还会给后来人传达这样的信息:嗯,这个变量在后面代码部分会发生改变。 -为了让变量可变(挺拗口的),将 *src/main.rs* 改为以下内容: +为了让变量声明为可变,将 *src/main.rs* 改为以下内容: ```rust fn main() { @@ -95,7 +93,7 @@ The value of x is: 5 The value of x is: 6 ``` -一切抉择都是权衡,使用可变还是不可变,更多的还是取决于你的选择,例如不可变可以带来安全性,但是丧失了灵活性和性能(如果你要改变,就要重新创建一个新的变量,这里涉及到内存对象的再分配)。而可变变量最大的好处就是使用上的灵活和性能上的提升。 +选择可变还是不可变,更多的还是取决于你的使用场景,例如不可变可以带来安全性,但是丧失了灵活性和性能(如果你要改变,就要重新创建一个新的变量,这里涉及到内存对象的再分配)。而可变变量最大的好处就是使用上的灵活性和性能上的提升。 例如,在使用大型数据结构或者热点代码路径(被大量频繁调用)的情形下,在同一内存位置更新实例可能比复制并返回新分配的实例要更快。使用较小的数据结构时,通常创建新的实例并以更具函数式的风格来编写程序,可能会更容易理解,所以值得以较低的性能开销来确保代码清晰。 @@ -121,13 +119,10 @@ fn main() { 首先,常量不允许使用 `mut`。**常量不仅仅默认不可变,而且自始至终不可变**。 -常量使用 `const` 关键字而不是 `let` 关键字来声明,并且值的类型**必须**标注。我们将在下一节[数据类型](./type.md)中介绍,因此现在暂时无需关心细节。 - - +常量使用 `const` 关键字而不是 `let` 关键字来声明,并且值的类型**必须**标注。我们将在下一节[数据类型](./base-type/index.md)中介绍,因此现在暂时无需关心细节。 -最后一个不同点是常量只能设置为常量表达式,而不能是函数调用的结果或是只能在运行时计算得到的值,例如你可以这样来声明常量: -下面是一个常量声明的例子,其常量名为 `MAX_POINTS`,值设置为 100,000。(Rust 常量的命名约定是全部字母都使用大写,并使用下划线分隔单词,另外对数字字面量可插入下划线以提高可读性): +下面是一个常量声明的例子,其常量名为 `MAX_POINTS`,值设置为 `100,000`。(Rust 常量的命名约定是全部字母都使用大写,并使用下划线分隔单词,另外对数字字面量可插入下划线以提高可读性): ```rust const MAX_POINTS: u32 = 100_000; @@ -141,7 +136,6 @@ const MAX_POINTS: u32 = 100_000; Rust允许声明相同的变量名,在后面声明的变量会遮蔽掉前面声明的, 如下所示: - ```rust fn main() { let x = 5; @@ -168,20 +162,19 @@ The value of x is: 12 ``` 这和`mut`变量的使用是不同的,第二个let生成了完全不同的新变量,两个变量只是恰好拥有同样的名称,涉及一次内存对象的再分配 -,而`mut`声明的变量,可以修改同一个内存地址上的值,并不会发生内存对象的再分配,性能要好很多。 +,而`mut`声明的变量,可以修改同一个内存地址上的值,并不会发生内存对象的再分配,性能要更好。 变量遮蔽的用处在于,如果你在某个作用域内无需再使用之前的变量(在被遮蔽后,无法再访问到之前的同名变量),就可以重复的使用变量名字,而不用绞尽脑汁去想更多的名字。 -例如,假设我们程序要求用户输入空格字符来显示他们想要的空格数目,但我们实际上想要将该输入存储为一个数字: - +例如,假设有一个程序要统计一个空格字符串的空格数量: ```rust - // 字符串类型 - let spaces = " "; - // usize数值类型 - let spaces = spaces.len(); +// 字符串类型 +let spaces = " "; +// usize数值类型 +let spaces = spaces.len(); ``` -这种结构是允许的,因为第一个 `spaces` 变量是一个字符串类型,第二个 `spaces` 变量是一个全新的变量且和第第一个具有相同的变量名,且是一个数值类型。所以变量遮蔽可以让我们就不必给出不同的名称,如 `spaces_str` 和 `spaces_num`;相反我们可以重复使用更简单的 `spaces` 变量名。然而,如果我们对此尝试使用 `mut`,如下所示,我们将得到一个编译期错误: +这种结构是允许的,因为第一个 `spaces` 变量是一个字符串类型,第二个 `spaces` 变量是一个全新的变量且和第第一个具有相同的变量名,且是一个数值类型。所以变量遮蔽可以帮我们节省些脑细胞,不用去想如`spaces_str` 和 `spaces_num`此类的变量名;相反我们可以重复使用更简单的 `spaces` 变量名。 你也可以不用`let`: ```rust, let mut spaces = " "; @@ -202,7 +195,6 @@ error[E0308]: mismatched types error: aborting due to previous error ``` -该错误标明,我们试图把一个`usize`类型的数值赋值为一个字符串变量。 - +显然,Rust对类型的要求很严格,不允许讲字符串类型赋值给整数类型`usize`,`usize`是一种cpu相关的整数类型,在[数值类型](./base-type/numbers#整数类型)有详细介绍. -至此,关于变量的内容你已经彻底掌握了,下面来学习下Rust的数据类型。Rust每个值都有其确切的数据类型, 总的来说可以分为两类:基本类型和复合类型, 先来看看基本类型。 \ No newline at end of file +万事开头难,到目前为止,都进展很顺序,那下面开始,咱们正式进入Rust的类型世界,看看有哪些挑战在前面等着大家。