From 7ff4ad4d46ca7c8f03edb3c14aaed645eb442f8b Mon Sep 17 00:00:00 2001 From: Davirain Date: Wed, 17 May 2023 17:34:42 +0800 Subject: [PATCH] update --- src/chapter_3/chapter_3_1.md | 85 +++++++++------- src/chapter_3/chapter_3_2.md | 172 +++++++++++++++++---------------- src/chapter_3/chapter_3_3.md | 9 +- src/chapter_3/chapter_3_4.md | 6 +- src/chapter_3/chapter_3_5.md | 10 +- src/chapter_3/chapter_3_6.md | 16 +-- src/chapter_3/chapter_3_7_1.md | 76 +++++++++------ src/chapter_3/chapter_3_7_2.md | 47 ++++++--- src/chapter_3/chapter_3_7_3.md | 32 +++--- 9 files changed, 263 insertions(+), 190 deletions(-) diff --git a/src/chapter_3/chapter_3_1.md b/src/chapter_3/chapter_3_1.md index 8cae11b..b2a0246 100644 --- a/src/chapter_3/chapter_3_1.md +++ b/src/chapter_3/chapter_3_1.md @@ -1,72 +1,83 @@ -## 3.1 变量和可变性 -### 3.1.1 绑定 -在Rust中,将值和变量关联的过程称为绑定,变量的绑定可以使用let关键字,如下: +# 3.1 变量和可变性 + +## 3.1.1 绑定 + +在Rust中,将值和变量关联的过程称为绑定,变量的绑定可以使用`let`关键字,如下: + ```Rust let a = 1; // 将1绑定到变量a let mut b = 2; // 将2绑定到变量b let some_number = Some(2); // 将Some(2)绑定到some_number ``` -### 3.1.2 变量 +## 3.1.2 变量 + Rust中变量分为不可变变量和可变变量。不可变变量不能对其进行二次绑定,可变变量可以对其进行二次绑定。 - 不可变变量定义方式如下: -```Rust -let a: u32 = 1; //将1绑定到a这个变量 -let b = 0u32; -let c = 1; //定义时不指定类型,可以自动类型推导 -``` -对不可变变量二次绑定一个值会报错: -```Rust -let a: u32 = 1; //将1绑定到变量a,a为不可变变量, -a = 2; //编译错误,a是不可变的变量,不能进行二次绑定 -``` + ```Rust + let a: u32 = 1; //将1绑定到a这个变量 + let b = 0u32; + let c = 1; //定义时不指定类型,可以自动类型推导 + ``` + + 对不可变变量二次绑定一个值会报错: + + ```Rust + let a: u32 = 1; //将1绑定到变量a,a为不可变变量, + a = 2; //编译错误,a是不可变的变量,不能进行二次绑定 + ``` - 可变变量定义方式如下: -```Rust -let mut a: u32 = 1; //通过mut关键字定义可变变量 -a = 2; //将2绑定到变量a,编译正确,因为a是可变变量,可以进行二次绑定 -let mut b = 2; -b = 3; -``` + + ```Rust + let mut a: u32 = 1; //通过mut关键字定义可变变量 + a = 2; //将2绑定到变量a,编译正确,因为a是可变变量,可以进行二次绑定 + let mut b = 2; + b = 3; + ``` 设计思考: 从编译器的角度,如果一个值定义为不可变变量,那它就不会改变,更易于推导。想想一下如果代码非常多,如果变量不会变化,但是允许它可变,其实会更容易滋生bug。 -### 3.1.3常量 - 常量是绑定到一个名称不允许改变的值,定义方式如下: +## 3.1.3常量 + +常量是绑定到一个名称不允许改变的值,定义方式如下: + ```Rust const HOUR_IN_SECONDS: u32 = 60 * 60; ``` 常量和不可变变量的区别: - 不允许对常量使用mut关键字,它总是不可变的,定义时必须显式的标注类型; -```Rust -let a = 1u32; //编译正确 -let a = 1; //编译正确 -const A: u32 = 1; //编译正确 -const B = 2u32; //编译错误 -const C = 2; //编译错误 -``` + ```Rust + let a = 1u32; //编译正确 + let a = 1; //编译正确 + + const A: u32 = 1; //编译正确 + const B = 2u32; //编译错误 + const C = 2; //编译错误 + ``` - 常量可以在任何作用域声明,包括全局作用域; - 常量只能被设置为常量表达式,不能是在运行时计算出来的值。 -```Rust -let a: u32 = 1; -let b: u32 = 2; -const A: u32 = a + b; //编译错误 -``` + ```Rust + let a: u32 = 1; + let b: u32 = 2; + const A: u32 = a + b; //编译错误 + ``` -### 3.1.4 隐藏 +## 3.1.4 隐藏 + +Rust中可以定义一个与之前的变量同名的变量,这称之为第一个变量被第二个变量隐藏。隐藏和`mut`的区别:隐藏是定义了一个新的变量,而使用mut是修改原来的变量。 -Rust中可以定义一个与之前的变量同名的变量,这称之为第一个变量被第二个变量隐藏。隐藏和mut的区别:隐藏是定义了一个新的变量,而使用mut是修改原来的变量。 ```Rust fn main() { let a: u32 = 1; //这个变量a被下面的a隐藏掉了 let a: u32 = 2; //定义了一个新的变量,这个变量也叫作a println!("a: {:?}", a); //输出结果为2 - + let mut b: u32 = 1; //定义可变变量b b = 2; //对b的值进行的修改 println!("b: {:?}", b); //输出结果为2 diff --git a/src/chapter_3/chapter_3_2.md b/src/chapter_3/chapter_3_2.md index 49c0d51..38451d8 100644 --- a/src/chapter_3/chapter_3_2.md +++ b/src/chapter_3/chapter_3_2.md @@ -1,9 +1,10 @@ -## 3.2 基本数据类型 +# 3.2 基本数据类型 Rust是静态类型语言,编译时就必须知道所有变量的类型。根据值以及其使用方式,Rust编译器通常能自动推导出变量的类型。 Rust有两种数据类型子集,分别是:标量(scalar)类型和复合(compound)类型。 -### 3.2.1 标量类型 +## 3.2.1 标量类型 + 标量类型包括:整型、浮点型、布尔类型和字符类型。 - 整型和浮点型 @@ -19,9 +20,9 @@ Rust中的整型和浮点型如下: |128 bit| i128 | u128 | | | arch | isize | usize | | -说明:isize和usize的长度是和平台相关,如果CPU是32位的,则这两个类型是32位的,如果CPU是64位的,则这两个类型是64位的。 +说明:`isize`和`usize`的长度是和平台相关,如果CPU是32位的,则这两个类型是32位的,如果CPU是64位的,则这两个类型是64位的。 -上面的表格中,f32和f64为浮点型,其它为整型。浮点型和整型一起构成数值型。 +上面的表格中,`f32`和`f64`为浮点型,其它为整型。浮点型和整型一起构成数值型。 (1)可以在数值字面量后面加上类型表示该类型的数值,如下: ```Rust @@ -71,103 +72,106 @@ fn main() { - 布尔型 -Rust中的布尔型用bool表示,有两个可能的值,为true和false。布尔类型使用的场景主要是条件表达式(控制流的内容),使用如下: -```Rust -fn main() { - // 定义方式 - let a: bool = true; - let b: bool = false; + Rust中的布尔型用bool表示,有两个可能的值,为true和false。布尔类型使用的场景主要是条件表达式(控制流的内容),使用如下: - // 使用场景 - if a { - println!("a is true"); - } else { - println!("a is false"); - } + ```Rust + fn main() { + // 定义方式 + let a: bool = true; + let b: bool = false; - if b { - println!("b is true"); - } else { - println!("b is false"); + // 使用场景 + if a { + println!("a is true"); + } else { + println!("a is false"); + } + + if b { + println!("b is true"); + } else { + println!("b is false"); + } } -} -``` + ``` - 字符类型 -char类型用于存放单个unicode字符,占用4个字节空间。当存储char类型数据时,Rust会将其转换为utf-8编码的数据存储。char字面量是单引号包含的任意单个字符,字符类型使用示例如下: -```Rust -fn main() { - let c: char = 'z'; - let x: char = 'x'; - let heart_eyed_cat: char = '😻'; -} -``` + char类型用于存放单个unicode字符,占用4个字节空间。当存储char类型数据时,Rust会将其转换为utf-8编码的数据存储。char字面量是单引号包含的任意单个字符,字符类型使用示例如下: -### 3.2.2. 原生复合类型 + ```Rust + fn main() { + let c: char = 'z'; + let x: char = 'x'; + let heart_eyed_cat: char = '😻'; + } + ``` + +## 3.2.2. 原生复合类型 复合类型是将多个值组合成一个类型。Rust有两个原生复合类型:元组和数组。 - 元组 -圆括号以及其中逗号分割的值列表组成元组,定义一个元组方式如下: -```Rust -fn main() { - let tup: (i32, f64, u8) = (500, 6.4, 1); -} -``` -可以将元组重新结构到变量上,如下: -```Rust -fn main() { - let tup = (500, 6.4, 1); - let (x, y, z) = tup; - //接下来你可以使用x、y、z -} -``` -也可以直接使用元组的元素,如下: -```Rust -fn main() { - let x: (i32, f64, u8) = (500, 6.4, 1); - let first = x.0; - let second = x.1; - let third = x.2; -} -``` + 圆括号以及其中逗号分割的值列表组成元组,定义一个元组方式如下: + ```Rust + fn main() { + let tup: (i32, f64, u8) = (500, 6.4, 1); + } + ``` + 可以将元组重新结构到变量上,如下: + ```Rust + fn main() { + let tup = (500, 6.4, 1); + let (x, y, z) = tup; + //接下来你可以使用x、y、z + } + ``` + 也可以直接使用元组的元素,如下: + ```Rust + fn main() { + let x: (i32, f64, u8) = (500, 6.4, 1); + let first = x.0; + let second = x.1; + let third = x.2; + } + ``` -不带任何值的元组,称为unit类型(单元元组),可以代表空值或者空的返回类型,如下: -```Rust -fn main() { - let x: () = (); // 将值()保存到类型为()的变量x中 -} -``` + 不带任何值的元组,称为unit类型(单元元组),可以代表空值或者空的返回类型,如下: + ```Rust + fn main() { + let x: () = (); // 将值()保存到类型为()的变量x中 + } + ``` - 数组 -数组中的每个元素的类型必须相同,数组的长度是固定的,数组的定义方式如下: -```Rust -fn main() { - let a = [1, 2, 3, 4, 5]; //直接写数组的值 - let b: [i32; 5] = [1, 2, 3, 4, 5]; //显示指定数组的类型和长度 - let c: [i32; 5] = [3; 5]; //数组每个元素为同样的值,等价于let a = [5, 5, 5, 5, 5]; -} -``` -数组通过索引来访问元素,索引从0开始计数,如下: -```Rust -fn main() { - let a = [1, 2, 3, 4, 5]; + 数组中的每个元素的类型必须相同,数组的长度是固定的,数组的定义方式如下: - let first = a[0]; // first = 1 - let second = a[1]; // second = 2 -} -``` -Rust中,访问无效的索引元素会报错,如下: -```Rust -fn main() { - let a = [1, 2, 3, 4, 5]; - let b = a[5]; // 错误,只能放为0-4,所以这个代码将无法编译 -} -``` + ```Rust + fn main() { + let a = [1, 2, 3, 4, 5]; //直接写数组的值 + let b: [i32; 5] = [1, 2, 3, 4, 5]; //显示指定数组的类型和长度 + let c: [i32; 5] = [3; 5]; //数组每个元素为同样的值,等价于let a = [5, 5, 5, 5, 5]; + } + ``` + 数组通过索引来访问元素,索引从0开始计数,如下: + ```Rust + fn main() { + let a = [1, 2, 3, 4, 5]; -### 3.2.3 类型转换 (少一个From, Into, TryFrom, TryInto) + let first = a[0]; // first = 1 + let second = a[1]; // second = 2 + } + ``` + Rust中,访问无效的索引元素会报错,如下: + ```Rust + fn main() { + let a = [1, 2, 3, 4, 5]; + let b = a[5]; // 错误,只能放为0-4,所以这个代码将无法编译 + } + ``` + +## 3.2.3 类型转换 (少一个From, Into, TryFrom, TryInto) Rust中可以使用as进行类型转换。 diff --git a/src/chapter_3/chapter_3_3.md b/src/chapter_3/chapter_3_3.md index be35dc2..c07b9d9 100644 --- a/src/chapter_3/chapter_3_3.md +++ b/src/chapter_3/chapter_3_3.md @@ -1,6 +1,7 @@ -## 3.3 函数 +# 3.3 函数 + +## 3.3.1. 函数定义 -### 3.3.1. 函数定义 fn关键字、函数名、函数参数名及其类型(如果有的话)、返回值类型(如果有的话)组成函数签名, 加上由一对花括号包含的函数体组成函数。例子如下: ```Rust // 一个没有参数,也没有返回值的函数 @@ -50,7 +51,7 @@ fn main() { } ``` -### 3.3.2. 语句和表达式 +## 3.3.2. 语句和表达式 Rust中,语句是执行一个写操作但不返回值的指令,表达式则计算并产生一个值。 ```Rust @@ -66,7 +67,7 @@ fn main() { } ``` -### 3.3.3. 函数返回值 +## 3.3.3. 函数返回值 - 使用return指定返回值,如下: ```Rust diff --git a/src/chapter_3/chapter_3_4.md b/src/chapter_3/chapter_3_4.md index 7d30084..c108f50 100644 --- a/src/chapter_3/chapter_3_4.md +++ b/src/chapter_3/chapter_3_4.md @@ -1,4 +1,4 @@ -## 3.4 注释 +# 3.4 注释 在Rust中,注释分为三类: @@ -8,7 +8,7 @@ 本节主要简单介绍代码注释和文档注释,对于注释的其它功能,我们后面再深入。 -### 3.4.1. 代码注释 +## 3.4.1. 代码注释 代码注释有两种: @@ -37,7 +37,7 @@ fn main() { } ``` -### 3.4.2.文档注释 +## 3.4.2.文档注释 Rust提供了cargo doc命令可以把文档注释转换成html网页,最终展示给用户。文档注释也有文档行注释和文档块注释: diff --git a/src/chapter_3/chapter_3_5.md b/src/chapter_3/chapter_3_5.md index 55f5bfe..7b7dae3 100644 --- a/src/chapter_3/chapter_3_5.md +++ b/src/chapter_3/chapter_3_5.md @@ -1,4 +1,4 @@ -## 3.5 控制流 +# 3.5 控制流 Rust中的控制流结构主要包括: @@ -7,7 +7,7 @@ Rust中的控制流结构主要包括: - while循环; - for .. in 循环。 -### 3.5.1. if条件判断 +## 3.5.1. if条件判断 - if执行条件判断,示例如下: ```Rust @@ -64,7 +64,7 @@ fn main() { } ``` -### 3.5.2. loop循环 +## 3.5.2. loop循环 - loop重复执行代码 ```Rust @@ -126,7 +126,7 @@ fn main() { } ``` -### 3.5.3. while条件循环 +## 3.5.3. while条件循环 - while条件循环执行代码,当条件不满足后结束循环,如下: ```Rust fn main() { @@ -156,7 +156,7 @@ fn main() { } ``` -### 3.5.4. for .. in 循环 +## 3.5.4. for .. in 循环 for循环用来对一个集合的每个元素执行一些代码,使用方式如下: ```Rust diff --git a/src/chapter_3/chapter_3_6.md b/src/chapter_3/chapter_3_6.md index b0b6318..3c7cbdd 100644 --- a/src/chapter_3/chapter_3_6.md +++ b/src/chapter_3/chapter_3_6.md @@ -1,5 +1,7 @@ -## 3.6 Rust内存模型 -### 3.6.1 Rust程序内存布局 +# 3.6 Rust内存模型 + +## 3.6.1 Rust程序内存布局 + ![注释](../../assets/2.png) 上图是一张linux系统上Rust程序的内存布局图。在linux操作系统中,会划分固定的区域给内核使用,即上图中的内核空间;应用程序使用的是用户空间。 @@ -11,8 +13,10 @@ Rust程序使用的内存空间分为如下: - 堆区(Heap):程序代码中动态分配的内存,在程序运行时申请,该区域向上增长。 - 栈区(Stack):该区域存放函数调用的参数、局部变量和返回地址等信息,在编译阶段分配,向下增长。 -### 3.6.2 栈和堆 -#### 1.栈和栈帧 +## 3.6.2 栈和堆 + +### 1.栈和栈帧 + “栈和栈帧属于操作系统的概念,由操作系统进行管理,栈空间以后进先出的顺序存储数据。将数据放到栈上就做入栈,将数据移出栈就做出栈。 每次调用函数时,操作系统会在栈顶创建一个栈帧来保存函数的上下文数据(主要是函数内部声明的局部变量),函数返回时返回值也会存储在该栈帧中。当函数调用者取得该函数返回值后,栈帧会被释放。”引用自《Rust入门秘籍》。 @@ -42,12 +46,12 @@ fn main() { 这里需要注意的是两个帧对应同样的内存地址,这是因为在调用完f1函数后,其对应的栈帧释放(释放的实际意义就是这段内存可以被重新分配了),然后调用f2函数为其分配栈帧时从同样的地址进行分配。 -#### 2. 堆 +### 2. 堆 堆空间和栈空间不同,不由操作系统管理,在需要时申请,不需要时释放。申请和释放堆内存是一件困难的事情,尤其当程序代码较多时。只申请堆内存而不释放会造成内存泄露,内存泄露过多会造成内存耗尽而崩溃。错误的释放在使用的内存也会造成程序运行错误(或直接无法运行)。 有些编程语言使用提供垃圾管理回收器(GC)来自动回收不再使用的堆内存,有些语言必须完全由程序员在代码中手动申请和释放内存。 Rust没有GC,但通过其独特的机制管理内存,程序员不用手动申请和释放堆内存。 -#### 3. Rust如何使用堆和栈 +### 3. Rust如何使用堆和栈 栈中存储的所有数据都必须占用(在编译时就)已知且固定的大小。编译时大小未知或可能变化的数据,存储在堆上。 数据存放到栈上时,是直接将数据放到栈内存。 当数据需要存放到堆上时,内存分配器则是根据数据的大小,在堆内存找到合适大小的空区域存放,把它标记为已使用,并返回一个表示该位置地址的指针。该指针存储在栈上,当需要访问具体的数据时,必须先访问指针,然后通过指针找到堆上的位置,从而访问数据。这个过程可以用下图表示: diff --git a/src/chapter_3/chapter_3_7_1.md b/src/chapter_3/chapter_3_7_1.md index f90a74e..78278f2 100644 --- a/src/chapter_3/chapter_3_7_1.md +++ b/src/chapter_3/chapter_3_7_1.md @@ -1,7 +1,7 @@ -### 3.7.1 所有权介绍 +# 3.7.1 所有权介绍 所有权是Rust最为与众不同的特性,它让Rust无需垃圾回收即可保证内存安全。 -#### 1. 所有权规则 +## 1. 所有权规则 Rust所有权的规则如下: @@ -16,13 +16,14 @@ fn main() { } ``` -上面的代码中,a就是8的所有者,b是String::from("hello")的所有者,c则是vec![1, 2, 3]的所有者。 +上面的代码中,a就是`8`的所有者,b是`String::from("hello")`的所有者,c则是`vec![1, 2, 3]`的所有者。 -注意:b是String::from("hello")的所有者,但是b不是字符串"hello"的所有者。同理,c是vec![1, 2, 3]的所有者,但不是[1, 2, 3]的所有者。至于为什么,后续内容(String类型部分)会进行讲解。 +注意:b是`String::from("hello")`的所有者,但是b不是字符串`"hello"`的所有者。同理,c是`vec![1, 2, 3]`的所有者,但不是`[1, 2, 3]`的所有者。至于为什么,后续内容(String类型部分)会进行讲解。 -#### 2. 变量的作用域 +## 2. 变量的作用域 变量作用域是变量在程序中有效的范围。一对花括号表示的范围就是作用域,变量有效的范围就是从创建开始,到离开作用域结束。 示例1: + ```Rust fn f() { let b = 1u32; // --------------------------------| @@ -44,6 +45,7 @@ fn main() { ``` 示例2: + ```Rust fn main() { let a = 8u32; // --------------------------| @@ -58,13 +60,14 @@ fn main() { } ``` -#### 3. String类型 +## 3. String类型 - String类型的创建有下面三种方式: - - String::from - - to_string - - String::new + - `String::from` + - `to_string` + - `String::new` + ```Rust fn main() { let s1 = String::from("Hello"); // 方法一 @@ -87,26 +90,33 @@ fn main() { - String类型的本质 Rust标准库中,String类型的定义如下: + ```Rust pub struct String { vec: Vec, } ``` + Vec类型的定义如下: + ```Rust pub struct Vec { buf: RawVec, len: usize, // 长度 } ``` + RawVec定义则类似于如下(为了更好的说明String类型,下面的定义用简化的代码): + ```Rust struct RawVec { ptr: NonNull, // 指针 cap: usize, // 容量 } ``` + 那对于整个String类型,可以用伪代码表示如下: + ```Rust struct String { v: struct Vec { @@ -115,7 +125,9 @@ struct String { } } ``` + 更进一步的简化,可以得到String类型本质如下: + ```Rust struct String { ptr:NonNull, @@ -123,11 +135,13 @@ struct String { len: usize, } ``` + 所以String类型本质是三个字段:一个指针,一个容量大小,一个长度大小。 - 内存分配 在Rust中,编译时大小确定的数据放在栈上,编译时大小不能确定的数据放在堆上。考虑如下代码: + ```Rust fn main() { let mut s = String::new(); @@ -137,13 +151,15 @@ fn main() { println!("{s}"); // 打印AB } ``` + 在第2行定义String类型时,并不能确定最终字符串的大小,所以字符串内容本身应该存储在堆上。结合什么String类型的本质的内容,可以得到String类型的存储如下: ![注释](../../assets/5.png) String类型本身是三个字段(指针、长度、容量),在编译时是已知的大小,存储在栈上;String类型绑定的字符串(在上面代码中是“AB”)在编译时大小未知,是运行时在堆上分配内存,分配后的内存地址保存在String类型的指针字段中,内存大小保存在cap字段中,内存上存储的字符串长度保存在len字段中。 -#### 4. move语义 +## 4. move语义 + Rust所有权规则第二条,在任意时刻,值有且仅有一个所有者。那么当一个变量赋给另外一个变量时发生了什么? - 完全存储在栈上的类型 @@ -156,11 +172,13 @@ fn main() { println!("x: {:?}, y: {:?}", x, y); } ``` -x和y都是u32类型,在编译时知道大小,都存储在栈上。代码第2行是将5绑定到变量x上,第3行则是通过自动拷贝的方式将5绑定到y上(先拷贝x的值5,然后将拷贝后得到的5绑定到y上)。所以,当let y = x发生后,这段代码里面最后有两个值5,分别绑定到了x和y上。 + +x和y都是u32类型,在编译时知道大小,都存储在栈上。代码第2行是将5绑定到变量`x`上,第3行则是通过自动拷贝的方式将5绑定到`y`上(先拷贝`x`的值5,然后将拷贝后得到的5绑定到y上)。所以,当`let y = x`发生后,这段代码里面最后有两个值5,分别绑定到了`x`和`y`上。 - 涉及到堆存储的类型 再考虑如下代码: + ```Rust fn main() { let s = "Hello world!".to_string(); @@ -169,7 +187,7 @@ fn main() { println!("s1: {:?}", s1); } ``` -s是String类型,字符串“Hello world!”是存储在堆内存上的,其内存布局如下: +s是`String`类型,字符串`"Hello world"`是存储在堆内存上的,其内存布局如下: ![注释](../../assets/6.png) @@ -177,27 +195,28 @@ s是String类型,字符串“Hello world!”是存储在堆内存上的, ![注释](../../assets/7.png) -当let s1 = s执行后,就发生了所有权的转移,String类型值的所有权从s转移到了s1。此时Rust认为原来的s不再有效。因此,上面代码第4行打开编译将会出错。 +当`let s1 = s`执行后,就发生了所有权的转移,String类型值的所有权从`s`转移到了`s1`。此时Rust认为原来的`s`不再有效。因此,上面代码第4行打开编译将会出错。 -#### 5. 浅拷贝与深拷贝 +## 5. 浅拷贝与深拷贝 - 浅拷贝 只拷贝栈上的内容,就叫做浅拷贝。 -对于上面的String类型,执行let s1 = s后,只把s的ptr、len、cap中的值拷贝给s1的ptr、len、cap的值,这种就叫做浅拷贝。浅拷贝发生后,s的ptr和s1的ptr都指向同样的堆内存。内存布局如下: +对于上面的String类型,执行`let s1 = s`后,只把`s`的`ptr`、`len`、`cap`中的值拷贝给`s1`的`ptr`、`len`、`cap`的值,这种就叫做浅拷贝。浅拷贝发生后,`s`的`ptr`和`s1`的`ptr`都指向同样的堆内存。内存布局如下: ![注释](../../assets/8.png) - 深拷贝 除了拷贝栈上的内容外,还拷贝堆内存中的内容,就叫做深拷贝。 -对于上面的String类型,执行let s1 = s后,除了把s的len、cap中的值拷贝给s1的len、cap外,还在堆上重新分配一块内存,将s的ptr指向的堆内存的内容拷贝到这块内存,然后s1的ptr指向这块内存,这种拷贝就叫做深拷贝。深拷贝发生后,s的ptr和s1的ptr指向不同的堆内存,但是堆内存中存储的内容一样。深拷贝发生后的内存布局如下: +对于上面的String类型,执行`let s1 = s`后,除了把`s`的`len`、`cap`中的值拷贝给`s1`的`len`、`cap`外,还在堆上重新分配一块内存,将s的ptr指向的堆内存的内容拷贝到这块内存,然后`s1`的`ptr`指向这块内存,这种拷贝就叫做深拷贝。深拷贝发生后,`s`的`ptr`和`s1`的`ptr`指向不同的堆内存,但是堆内存中存储的内容一样。深拷贝发生后的内存布局如下: ![注释](../../assets/9.png) 显然,**Rust中变量赋值(Rust中叫所有权转移)使用的是浅拷贝**。 -#### 6. Clone -当需要拷贝堆上的数据时,可以使用clone方法,完成深拷贝的操作,如下: +## 6. Clone + +当需要拷贝堆上的数据时,可以使用`clone`方法,完成深拷贝的操作,如下: ```Rust fn main() { let s = "Hello world!".to_string(); @@ -206,21 +225,22 @@ fn main() { println!("s1: {:?}", s1); } ``` -不过不是所有的类型都能使用clone方法进行深拷贝,只有实现了Clone trait的类型才能调用该方法。 +不过不是所有的类型都能使用`clone`方法进行深拷贝,只有实现了`Clone trait`的类型才能调用该方法。 -#### 7. Copy -按照Rust所有权规则第二条,**在任意时刻,值有且仅有一个所有者**。所以当let a = b发生时,就将变量b拥有的值移到了a上,此时a应该回到未初始状态,但实际情况并不一定。不一定的原因是,部分类型实现了Copy trait,在值移动时会对值进行自动拷贝,能让变量a仍拥有原来的值。 +## 7. Copy -Rust中,默认实现了Copy trait的类型有: +按照Rust所有权规则第二条,**在任意时刻,值有且仅有一个所有者**。所以当`let a = b`发生时,就将变量`b`拥有的值移到了`a`上,此时`a`应该回到未初始状态,但实际情况并不一定。不一定的原因是,部分类型实现了`Copy trait`,在值移动时会对值进行自动拷贝,能让变量a仍拥有原来的值。 -- 所有整数类型,比如u32; -- 所有浮点数类型,比如f64; -- 布尔类型,bool,它的值是true和false; -- 字符类型,char; -- 元组,当且仅当其包含的类型也都是Copy的时候。比如(i32, i32)是Copy的,但(i32, String)不是; +Rust中,默认实现了`Copy trait`的类型有: + +- 所有整数类型,比如`u32`; +- 所有浮点数类型,比如`f64`; +- 布尔类型,`bool`,它的值是`true`和`false`; +- 字符类型,`char`; +- 元组,当且仅当其包含的类型也都是`Copy`的时候。比如`(i32, i32)`是Copy的,但`(i32, String)`不是; - 共享指针类型或共享引用类型。 -#### 8. 所有权和函数 +## 8. 所有权和函数 - 将值传给函数 在将值传递给函数时,和变量赋值一样会发生值的移动(或复制),如下: diff --git a/src/chapter_3/chapter_3_7_2.md b/src/chapter_3/chapter_3_7_2.md index c322645..5829dfd 100644 --- a/src/chapter_3/chapter_3_7_2.md +++ b/src/chapter_3/chapter_3_7_2.md @@ -1,5 +1,7 @@ -### 3.7.2 引用与借用 +# 3.7.2 引用与借用 + 考虑如下代码: + ```Rust fn main() { let s2 = String::from("hello"); @@ -12,9 +14,11 @@ fn print(s: String) { println!("s is: {:?}", s); } ``` -第4行打开注释编译将发送错误,因为s2的所有权在第3行已经转移到print函数中了,s2将不再有效,因此第4行不能再使用。 -如果要在调用print函数后仍然能使用s2,根据本书目前学过的Rust知识,则需要将所有权再从函数转移到变量,然后使用,代码如下: +第4行打开注释编译将发送错误,因为s2的所有权在第3行已经转移到`print`函数中了,s2将不再有效,因此第4行不能再使用。 + +如果要在调用`print`函数后仍然能使用s2,根据本书目前学过的Rust知识,则需要将所有权再从函数转移到变量,然后使用,代码如下: + ```Rust fn main() { let s2 = String::from("hello"); @@ -27,10 +31,13 @@ fn print(s: String) -> String { s //将s的所有权返回 } ``` + 除了这种转移所有权的方式外,Rust还提供了引用的方式可以借用数据的所有权。 -#### 1. 引用与借用 +## 1. 引用与借用 + 引用本质上是一个指针,它存储一个地址,通过它可以访问存储在该地址上属于其它变量的数据。与指针不同的是,引用确保指向某个特性类型的有效值。对于一个变量的引用就是在此变量前面加上&符合。 + ```Rust let a = 5u32; let b = &a; // b是对a的引用 @@ -38,7 +45,8 @@ fn print(s: String) -> String { let c = String::from("hello"); let d = &c; // d 是对c的引用 ``` -上面代码中,变量a、b、c、d的内存布局如下: + +上面代码中,变量`a`、`b`、`c`、`d`的内存布局如下: ![注释](../../assets/10.png) @@ -76,6 +84,7 @@ fn print(s: &String) { ``` 引用只能使用变量,并不允许改变变量的值,如果需要改变变量,需要使用可变引用(下节内容),下面的代码会报错: + ```Rust fn main() { let s = String::from("hello"); @@ -86,12 +95,14 @@ fn change(some_string: &String) { some_string.push_str(", world"); //借用不允许改变变量的值,此行报错 } ``` -与引用相对的是解引用,符号为*,本书后续讲解。 -#### 2. 可变引用 +与引用相对的是解引用,符号为`*`,本书后续讲解。 + +## 2. 可变引用 + - 使用可变引用 -可以通过可变引用改变变量的值,对一个变量加上& mut就是对其的可变引用,示例如下: +可以通过可变引用改变变量的值,对一个变量加上`&mut`就是对其的可变引用,示例如下: ```Rust fn main() { let mut s = String::from("hello"); @@ -106,6 +117,7 @@ fn change(some_string: &mut String) { - 引用的作用域 前文讲过变量的作用域是从定义开始到花括号结束的位置,如: + ```Rust { ... @@ -115,6 +127,7 @@ fn change(some_string: &mut String) { ``` 引用的作用域和变量的作用域有些区别,**在老版本编译器(Rust 1.31 之前)中,引用的作用域和变量的作用域一样,也是从定义的位置开始到花括号之前结束**,如下: + ```Rust { ... @@ -123,7 +136,9 @@ fn change(some_string: &mut String) { .... }// 花括号之前为r的作用域结束位置 ``` + **在新版本编译器中,引用作用域的结束位置从花括号变成最后一次使用的位置**,如下: + ```Rust { ... @@ -136,6 +151,7 @@ fn change(some_string: &mut String) { - 使用可变引用的限制 (1)限制一:**同一作用域,特定数据只能有一个可变引用**。如下代码会报错: + ```Rust fn main() { let mut s1 = String::from("hello"); @@ -146,6 +162,7 @@ fn main() { ``` 但是下面的代码可以的(新老编译器都可以): + ```Rust fn main() { let mut s = String::from("hello"); @@ -163,6 +180,7 @@ fn main() { ``` 下面的代码在新编译器中也是可以的: + ```Rust fn main() { let mut s = String::from("hello"); @@ -178,6 +196,7 @@ fn main() { ``` (2)限制二:**同一作用域,可变引用和不可变引用不能同时存在**。如下代码编译错误: + ```Rust fn main() { let mut s = String::from("hello"); @@ -188,7 +207,9 @@ fn main() { println!("{}", r3); } ``` + 下面的代码在新编译器中是可以的: + ```Rust fn main() { let mut s = String::from("hello"); @@ -213,10 +234,11 @@ fn main() { 数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了此情况的发生,因为它甚至不会编译存在数据竞争的代码! -#### 3. 悬垂引用 +## 3. 悬垂引用 - 什么是悬垂引用(悬垂指针)? 在具有指针的语言中(如C/C++),很容易通过释放内存但是保留指向它的指针而错误的生成一个悬垂指针。例如有如下C代码: + ```C #include #include @@ -238,6 +260,7 @@ int main() return 0; } ``` + 在执行第14行前,其内存布局为: ![注释](../../assets/11.png) @@ -246,11 +269,12 @@ int main() ![注释](../../assets/12.png) -第14行执行后,ptr就变成了一个悬垂指针(或者交悬垂引用),然后在第16行继续使用ptr,则会发生错误。 +第14行执行后,`ptr`就变成了一个悬垂指针(或者交悬垂引用),然后在第16行继续使用`ptr`,则会发生错误。 - 在 Rust 中,编译器确保引用永远不会变成悬垂状态。 如下代码因为会产生悬垂引用,编译将不会通过: + ```Rust fn main() { let reference_to_nothing = dangle(); @@ -274,7 +298,8 @@ fn no_dangle() -> String { } // 此处s虽然离开了函数这个作用域范围,但是它的所有权是被转移出去了,值并没有释放 ``` -#### 4. 引用的规则总结 +## 4. 引用的规则总结 + 引用的规则可以总结如下: - 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。 diff --git a/src/chapter_3/chapter_3_7_3.md b/src/chapter_3/chapter_3_7_3.md index b9135a1..43a2709 100644 --- a/src/chapter_3/chapter_3_7_3.md +++ b/src/chapter_3/chapter_3_7_3.md @@ -1,16 +1,18 @@ -### 3.7.3 Slice类型 +# 3.7.3 Slice类型 + Slice(切片)类型,表示引用集合中一段连续的元素序列。Slice是一类引用,没有所有权。Rust常见类型中,有三种支持Slice的操作,分别是String、数组、Vec类型。 -#### 1. Slice类型 +## 1. Slice类型 假定s是可被切片的数据,则对应的操作有: -- s[n1..n2]:获取s中index=n1到index=n2(不包括n2)之间的所有元素; -- s[n1..]:获取s中index=n1到最后一个元素之间的所有元素; -- s[..n2]:获取s中第一个元素到index=n2(不包括n2)之间的所有元素; -- s[..]:获取s中所有元素; -- 其他表示包含范围的方式,如s[n1..=n2]表示取index=n1到index=n2(包括n2)之间的所有元素。 +- `s[n1..n2]`:获取s中index=n1到index=n2(不包括n2)之间的所有元素; +- `s[n1..]`:获取s中index=n1到最后一个元素之间的所有元素; +- `s[..n2]`:获取s中第一个元素到index=n2(不包括n2)之间的所有元素; +- `s[..]`:获取s中所有元素; +- 其他表示包含范围的方式,如`s[n1..=n2]`表示取index=n1到index=n2(包括n2)之间的所有元素。 + +Rust中几乎总是使用切片数据的引用。切片数据的引用对应的数据类型描述为`&[T]`或`&mut [T]`,前者不可通过Slice引用来修改源数据,后者可修改源数据。示例如下: -Rust中几乎总是使用切片数据的引用。切片数据的引用对应的数据类型描述为&[T]或&mut [T],前者不可通过Slice引用来修改源数据,后者可修改源数据。示例如下: ```Rust fn main(){ let mut arr = [11,22,33,44]; @@ -30,8 +32,10 @@ Slice类型是一个胖指针,包含两个字段: - 第一个字段是指向源数据中切片起点元素的指针; - 第二个字段是切片数据中包含的元素数量,即切片的长度。 -#### 2. String的切片类型 -String的切片类型为&str而不是&String,其使用方式如下: +## 2. `String`的切片类型 + +`String`的切片类型为`&str`而不是`&String`,其使用方式如下: + ```Rust fn main() { let s = String::from("hello world!"); @@ -42,12 +46,14 @@ fn main() { } ``` -&str和&String的内存布局如下: +`&str`和`&String`的内存布局如下: ![注释](../../assets/13.png) -#### 3. 其它Slice +## 3. 其它Slice + 数组的Slice,如下: + ```Rust fn main() { let a: [u32; 5] = [1, 2, 3, 4, 5]; @@ -55,7 +61,9 @@ fn main() { println!("b: {:?}", b); } ``` + Vec的Slice,如下: + ```Rust fn main() { let v: Vec = vec![1, 2, 3, 4, 5];