diff --git a/src/.DS_Store b/src/.DS_Store index a692ff9c..dfacf0f4 100644 Binary files a/src/.DS_Store and b/src/.DS_Store differ diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 1dac2512..ac92f5f1 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -17,8 +17,11 @@ - [变量绑定与结构](basic/variable.md) - [基本类型](basic/base-type/index.md) - [数值类型](basic/base-type/numbers.md) - - [字符串](basic/base-type/string.md) - [字符、布尔、元类型](basic/base-type/others.md) + - [所有权和借用](basic/ownership/index.md) + - [所有权](basic/ownership/ownership.md) + - [引用与借用](basic/ownership/borrowing.md) + - [字符串](basic/string.md) - [复合类型(todo)](basic/compound-type/intro.md) - [结构体(todo)](basic/compound-type/struct.md) - [枚举](basic/compound-type/enum.md) @@ -33,13 +36,9 @@ - [文档注释(todo)](basic/comment.md) - [包和模块(todo)](basic/crate-module.md) - [语句与表达式(todo)](basic/statement-expression.md) - -- [核心语法 todo](core/intro.md) - - [所有权(todo)](core/ownership.md) - - [借用(todo)](core/borrowing.md) - - [生命周期(todo)](core/lifetime.md) - [进阶语法 todo](advance/intro.md) + - [生命周期(todo)](advance/lifetime.md) - [泛型(todo)](advance/generitic.md) - [特征(todo)](advance/trait.md) - [迭代器(todo)](advance/interator.md) @@ -150,6 +149,7 @@ - [过程宏(todo)](macro/procedure-macro.md) - [性能调优 todo](performance/intro.md) + - [Clone和Copy](performance/clone-copy.md) - [Benchmark性能测试(todo)](performance/benchmark.md) - [减少Runtime check(todo)](performance/runtime-check.md) diff --git a/src/advance/lifetime.md b/src/advance/lifetime.md new file mode 100644 index 00000000..8bbf077a --- /dev/null +++ b/src/advance/lifetime.md @@ -0,0 +1 @@ +# 生命周期(todo) diff --git a/src/basic/base-type/string.md b/src/basic/base-type/string.md deleted file mode 100644 index 97f10b2d..00000000 --- a/src/basic/base-type/string.md +++ /dev/null @@ -1 +0,0 @@ -# 字符与字符串 diff --git a/src/basic/compound-type/tuple.md b/src/basic/compound-type/tuple.md index 6ee1b27c..5e2fda38 100644 --- a/src/basic/compound-type/tuple.md +++ b/src/basic/compound-type/tuple.md @@ -1 +1,18 @@ # 元组 + +可以使用元组返回多个值 +```rust +fn main() { + let s1 = String::from("hello"); + + let (s2, len) = calculate_length(s1); + + println!("The length of '{}' is {}.", s2, len); +} + +fn calculate_length(s: String) -> (String, usize) { + let length = s.len(); // len() 返回字符串的长度 + + (s, length) +} +``` \ No newline at end of file diff --git a/src/basic/ownership/borrowing.md b/src/basic/ownership/borrowing.md new file mode 100644 index 00000000..3e529ef1 --- /dev/null +++ b/src/basic/ownership/borrowing.md @@ -0,0 +1,304 @@ +# 引用与借用 + +上节中提到,如果仅仅是所有权转移,会让程序变得复杂,那能否像其它编程语言一样,使用某个变量的指针或者引用呢?答案是有的。 + +Rust通过`借用(Borrowing)`这个概念来达成上述的目的: **获取变量的引用,称之为借用(borrowing)**。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来,当使用完毕后,也必须要物归原主. + + + +### 引用与解引用 + +常规引用是一个指针类型,指向了对象存储的内存地址。在下面代码中,我们创建一个`i32`值的引用`y`,然后使用解引用运算符来解出`y`所使用的值: +```rust +fn main() { + let x = 5; + let y = &x; + + assert_eq!(5, x); + assert_eq!(5, *y); +} +``` + +变量 `x` 存放了一个 `i32` 值 `5`。`y`是 `x` 的一个引用。可以断言 `x` 等于 `5`。然而,如果希望对 `y` 的值做出断言,必须使用 `*y` 来解出引用所指向的值(也就是 **解引用**)。一旦解引用了 `y`,就可以访问 `y` 所指向的整型值并可以与 `5` 做比较。 + +相反如果尝试编写 `assert_eq!(5, y);`,则会得到如下编译错误: + +```text +error[E0277]: can't compare `{integer}` with `&{integer}` + --> src/main.rs:6:5 + | +6 | assert_eq!(5, y); + | ^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}` // 无法比较整数类型和引用类型 + | + = help: the trait `std::cmp::PartialEq<&{integer}>` is not implemented for + `{integer}` +``` + +不允许比较整数与引用,因为它们是不同的类型。必须使用解引用运算符解出引用所指向的值。 + +### 不可变引用 + +下面的代码,我们用s1的引用作为参数传递给`calculate_length`函数,而不是把s1的所有权转移给该函数: +```rust +fn main() { + let s1 = String::from("hello"); + + let len = calculate_length(&s1); + + println!("The length of '{}' is {}.", s1, len); +} + +fn calculate_length(s: &String) -> usize { + s.len() +} +``` + +能注意到两点: +1. 无需再通过函数参数来传入所有权,通过函数返回来传出所有权,代码更加简洁 +2. `calculate_length`的参数`s`类型从`String`变为`&String` + +这里,`&`符号即是引用,它们允许你使用值,但是不获取所有权,如图所示: +&String s pointing at String s1 +图:`&String s` 指向 `String s1`的示意图 + +`&s1`语法,让我们创建一个**指向s1的引用**,但是并不拥有它。因为并不拥有这个值,当引用离开作用域后,其指向的值也不会被丢弃。 + +同理,函数`calculate_length`使用`&`来表明参数`s`的类型是一个引用: +```rust +fn calculate_length(s: &String) -> usize { // s 是对 String 的引用 + s.len() +} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权, + // 所以什么也不会发生 +``` + +人总是贪心的,可以摸女孩手了,就想着摸摸胳膊(读者中的老司机表示,这个流程完全不对),因此光借用已经满足不了我们了,如果尝试修改借用的变量呢? +```rust +fn main() { + let s = String::from("hello"); + + change(&s); +} + +fn change(some_string: &String) { + some_string.push_str(", world"); +} +``` + +很不幸,胳膊你没摸到, 哦口误,你修改错了: +```console +error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference + --> src/main.rs:8:5 + | +7 | fn change(some_string: &String) { + | ------- help: consider changing this to be a mutable reference: `&mut String` + ------- 帮助:考虑将该参数类型修改为可变的引用: `&mut String` +8 | some_string.push_str(", world"); + | ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable + `some_string`是一个`&`类型的引用,因此它指向的数据无法进行修改 +``` + +正如变量默认不可变一样,引用指向的值默认也是不可变的,没事,来一起看看如果解决这个问题。 + +### 可变引用 + +只需要一个小调整,既可以修复上面代码的错误: +```rust +fn main() { + let mut s = String::from("hello"); + + change(&mut s); +} + +fn change(some_string: &mut String) { + some_string.push_str(", world"); +} +``` + +首先,声明`s`是可变类型,其次创建一个可变的引用`&mut s`和接受可变引用的函数`some_string: &mut String`。 + +##### 可变引用同时只能存在一个 + +不过可变引用并不是随心所欲、想用就用的,它有一个很大的限制:同一作用域,特定数据只能由一个可变引用: +```rust +let mut s = String::from("hello"); + +let r1 = &mut s; +let r2 = &mut s; + +println!("{}, {}", r1, r2); +``` + +以上代码会报错: +```console +error[E0499]: cannot borrow `s` as mutable more than once at a time 同一时间无法对`s`进行两次可变借用 + --> src/main.rs:5:14 + | +4 | let r1 = &mut s; + | ------ first mutable borrow occurs here 首个可变引用在这里借用 +5 | let r2 = &mut s; + | ^^^^^^ second mutable borrow occurs here 第二个可变引用在这里借用 +6 | +7 | println!("{}, {}", r1, r2); + | -- first borrow later used here 第一个借用在这里使用 + ``` + +这段代码出错的原因在于,第一个可变借用`r1`必须要持续到最后一次使用的位置`println!`,在`r1`创建和最后一次使用之间,我们又尝试创建第二个引用`r2`。 +对于新手来说,这个特性绝对是一大拦路虎,也是新人们谈之色变的编译器`borrow checker`特性之一,不过各行各业都一样,限制往往是出于安全的考虑,Rust也一样。 + +这种限制的好处就是使Rust在编译期就避免数据竞争,数据竞争可由以下行为造成: +- 两个或更多的指针同时访问同一数据 +- 至少有一个指针被用来写入数据 +- 没有同步数据访问的机制 + +数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码! + +很多时候,大括号可以帮我们解决一些问题,通过手动限制变量的作用域: +```rust +let mut s = String::from("hello"); + +{ + let r1 = &mut s; + +} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用 + +let r2 = &mut s; +``` + +##### 可变引用与不可变引用不能同时存在 + +下面的代码会导致一个错误: +```rust +let mut s = String::from("hello"); + +let r1 = &s; // 没问题 +let r2 = &s; // 没问题 +let r3 = &mut s; // 大问题 + +println!("{}, {}, and {}", r1, r2, r3); +``` + +错误如下: +```rust +error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable 无法借用可变`s`因为它已经被借用了不可变 + --> src/main.rs:6:14 + | +4 | let r1 = &s; // 没问题 + | -- immutable borrow occurs here 不可变借用发生在这里 +5 | let r2 = &s; // 没问题 +6 | let r3 = &mut s; // 大问题 + | ^^^^^^ mutable borrow occurs here 可变借用发生在这里 +7 | +8 | println!("{}, {}, and {}", r1, r2, r3); + | -- immutable borrow later used here 不可变借用在这里使用 +``` + +其实这个也很好理解,借用了不可变的用户,肯定不希望他借用的东西,被另外一个人莫名其妙改变了。多个不可变借用被允许是因为没有人会去试图修改数据,然后导致别人的数据被污染。 + +> 注意,引用的作用域从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号`}` + +Rust的编译器一直在优化,早期的时候,引用的作用域跟变量作用域是一致的,这对日常使用带来了很大的困扰,你必须非常小心的去安排可变、不可变变量的借用,免得无法通过编译,例如以下代码: +```rust +fn main() { + let mut s = String::from("hello"); + + let r1 = &s; + let r2 = &s; + println!("{} and {}", r1, r2); + // 新编译器中,r1,r2作用域在这里结束 + + let r3 = &mut s; + println!("{}", r3); +} // 老编译器中,r1、r2、r3作用域在这里结束 + // 新编译器中,r3作用域在这里结束 +``` + +在老的编译器中(Rust 1.31前),将会报错,因为`r1`和`r2`的作用域在花括号`}`处结束,那么`r3`的借用就会触发**无法同时借用可变和不可变**的规则。 + +但是在新的编译器中,该代码将顺利通过,因为**引用作用域的结束位置从花括号变成最后一次使用的位置**,因此`r1`借用和`r2`借用在`println!`后,就结束了,此时`r3`可以顺利借用到可变引用。 + +对于这种编译器优化行为,Rust专门起了一个名字 - Non-Lexical Lifetimes(NLL),专门用于找到某个引用在作用域(`}`)结束前就不再被使用的代码位置。 + +虽然这种借用错误有的时候会让我们很郁闷,但是你只要想想这是Rust提前帮你发现了潜在的bug,其实就开心了,虽然减慢了开发速度,但是从长期来看,大幅减少了后续开发和运维成本. + + +总的来说,借用的规则可以总结如下: +1. 同一个作用域,特定数据可以有任意多个不可变借用 +2. 同一个作用域,特定数据最多只有一个可变借用 +3. 同一个作用域,特定数据不能同时拥有可变和不可变引用 +4. 借用在最后一次使用的地方被释放 + +其实也不用死记硬背,你只要从安全性的角度稍微思考下,就能明白了,例如:有几个人同时在阅读一份在线文档,那么只要有一个人修改了,其它人看到的都会发生改变,这会造成错误的行为,对应上述的借用规则也就是: +1. 如果没人修改,那么再多人观看这份文档都没问题 +2. 最多只能有一个人同时修改 +3. 如果有一个人能修改,那么其它人不应该在同时看这份文档 + +### 悬垂引用(Dangling References) + +所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。 + +让我们尝试创建一个悬垂引用,Rust 会通过一个编译时错误来避免: + +文件名: src/main.rs + +```rust,ignore,does_not_compile +fn main() { + let reference_to_nothing = dangle(); +} + +fn dangle() -> &String { + let s = String::from("hello"); + + &s +} +``` + +这里是错误: + +```text +error[E0106]: missing lifetime specifier + --> src/main.rs:5:16 + | +5 | fn dangle() -> &String { + | ^ expected named lifetime parameter + | + = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from +help: consider using the `'static` lifetime + | +5 | fn dangle() -> &'static String { + | ~~~~~~~~ + +``` + +错误信息引用了一个我们还未介绍的功能:生命周期(lifetimes)。[该章](../../advance/lifetime.md)会详细介绍生命周期。不过,如果你不理会生命周期部分,错误信息中确实包含了为什么这段代码有问题的关键信息: + +```text +this function's return type contains a borrowed value, but there is no value for it to be borrowed from. +该函数返回了一个借用的值,但是已经找不到它所借用值的来源 +``` + +让我们仔细看看我们的 `dangle` 代码的每一步到底发生了什么: + + +```rust,ignore,does_not_compile +fn dangle() -> &String { // dangle 返回一个字符串的引用 + + let s = String::from("hello"); // s 是一个新字符串 + + &s // 返回字符串 s 的引用 +} // 这里 s 离开作用域并被丢弃。其内存被释放。 + // 危险! +``` + +因为 `s` 是在 `dangle` 函数内创建的,当 `dangle` 的代码执行完毕后,`s` 将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的 `String`,这可不对!Rust 不会允许我们这么做。 + +这里的解决方法是直接返回 `String`: + +```rust +fn no_dangle() -> String { + let s = String::from("hello"); + + s +} +``` + +这样就没有任何错误了。所有权被移动出去,所以没有值被释放。 \ No newline at end of file diff --git a/src/basic/ownership/index.md b/src/basic/ownership/index.md new file mode 100644 index 00000000..d58c3150 --- /dev/null +++ b/src/basic/ownership/index.md @@ -0,0 +1,7 @@ +# 所有权和借用 + +Rust之所以能成为万众瞩目的语言,就是因为其内存安全性。在以往,内存安全几乎都是通过GC的方式实现,但是GC会引来性能、内存占用以及Stop the world等问题,在高性能场景和系统编程上是不可忍受的,因此Rust采用了与中不同的方式:**所有权系统**。 + +理解所有权和借用,对于Rust学习是至关重要的,因此我们把本章提到了非常靠前的位置。 + +从现在开始,鉴于大家已经掌握了非常基本的语法,有些时候,在示例代码中,将省略`fn main{}`的模版代码,你只要将相应的示例放在`fn main{}`中,即可运行。 \ No newline at end of file diff --git a/src/basic/ownership/ownership.md b/src/basic/ownership/ownership.md new file mode 100644 index 00000000..fcfbd2a2 --- /dev/null +++ b/src/basic/ownership/ownership.md @@ -0,0 +1,324 @@ +# 所有权 + +所有的程序都必须和计算机内存打交道,如何从内存中申请空间来存放程序的运行内容,如何在不需要的时候释放这些空间,成了重中之重,也是所有编程语言设计的难点之一。在计算机语言不断演变过程中,出现了三种流派: +- **垃圾回收机制(GC)**,在程序运行时不断寻找不再使用的内存,典型代表:Java、Go +- **手动管理内存的分配和释放**, 在程序中,通过函数调用的方式来申请和释放内存,典型代表:C++ +- **通过所有权来管理内存**,编译器在编译时会根据一系列规则进行检查 + +其中Rust选择了第三种,最妙的是,这种只发生在编译器,因此对于程序运行期,不会有任何性能上的损失。 + +因为所有权是一个新概念,因此读者需要花费一些时间来掌握它,一旦掌握,海阔天空任你跃,在本章,我们将通过`字符串`来引导讲解所有权的相关知识。 + +## 一段不安全的代码 + +先来看看C语言的一段糟糕代码: +```c +int* foo() { + int a; // 变量a的作用域开始 + a = 100; + char *c = "xyz"; // 变量c的作用域开始 + return &a; +} // 变量a和c的作用域结束 +``` + +这段代码虽然可以编译通过,但是其实非常糟糕,变量`a`和`c`都是局部变量,函数结束后将局部变量`a`的地址返回,但局部变量`a`存在栈中,在离开作用域后,局部变量所申请的栈上内存都会被系统回收,从而造成了`悬空指针(Dangling Pointer)`的问题。这是一个非常典型的内存安全问题。很多编程语言都存在类似这样的内存安全问题。再来看变量`c`,`c`的值是常量字符串,存储于常量区,可能这个函数我们只调用了一次,我们可能不再想使用这个字符串,但`xyz`只有当整个程序结束后系统才能回收这片内存,这点让程序员是不是也很无奈? + +所以内存安全问题,一直都是程序员非常头疼的问题,好在在Rust中,这些问题即将成为历史,那Rust如何做到这一点呢? + +在正式进入主题前,先来一个预热知识。 + +## 栈(Stack)与堆(Heap) + +栈和堆是编程语言最核心的数据结构,但是在很多语言中,你并不需要经常考虑到栈与堆。 不过对于Rust这样的系统编程语言,值是位于栈上还是堆上非常重要,因为这会影响程序的行为和性能。 + +栈和堆的核心目标就是为程序在运行时提供可供使用的内存空间,关于它们的详细解释和实现方式,请参见[Rust代码鉴赏](https://codes.rs/data-structures/heap.html)一书. + +#### 栈 + +栈以放入值的顺序存储值并以相反顺序取出值。这也被称作 **后进先出**。想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做 **进栈**,移出数据则叫做 **出栈**。 + +因为上述的实现方式,栈中的所有数据都必须占用已知且固定的大小的内存空间,如果数据大小是未知的,那么在取出数据时,你将无法取到你想要的数据。 + + +#### 堆 + +与栈不同,对于大小位置或者可能变化的数据,我们需要将它存储在堆上。 + +当向堆上放入数据时,你需要请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 **指针**。这个过程称作 **在堆上分配内存**,有时简称为 “分配”(allocating)。 接着,该指针会被推入`栈`中,因为指针的大小时已知且固定的,在后续使用过程中,你将通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据。 + +因此堆是一种缺乏组织的数据结构。想象一下去餐馆就座吃饭: 进入餐馆,告知服务员有几个人,然后它找到一个够大的空桌子(堆上分配的内存空间)并领你们过去。如果有人来迟了,他们也可以通过桌号(栈上的指针)来找到你们坐在哪。 + +#### 性能区别 + +入栈比在堆上分配内存要快,因为入栈时操作系统无需分配新的空间:新数据的位置放入栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为操作系统必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。 + +访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。得益于CPU高速缓存,现代处理器访问内存的次数越少则越快。继续类比,假设有一个服务员在餐厅里处理多个桌子的点菜,在一个桌子报完所有菜后再移动到下一个桌子是最有效率的。但是如果在桌子A点菜完,再去一个比较远的桌子B点菜,就会比较慢。 + +出于同样原因,处理器在处理栈上数据的时候比处理堆上的数据更加高效,同时,在堆上分配大量的空间也可能消耗时间。 + + +#### 所有权与堆栈 + +当你的代码调用一个函数时,传递给函数的参数(包括可能指向堆上数据的指针和函数的局部变量)依次被压入栈中, 当函数调用结束时,这些值将被从栈中按照相反的顺序依次移除。 + +因为堆上的数据缺乏组织,因此跟踪这些数据何时分配和释放是非常重要的,否则堆上的数据将产生内存泄漏:这些数据将永远无法被回收。这就是Rust所有权系统为我们提供的强大保障。 + +对于其他很多编程语言,你确实无需理解堆栈的原理,但是在Rust中,明白堆栈的原理,对于我们理解所有权的工作原理会有帮助. + + + +## 所有权原则 + +理解了堆栈,接下来看一下*关于所有权的规则*,首先请谨记以下规则: +> 1. Rust中每一个值都`有且只有`一个所有者(变量) +> 2. 当所有者(变量)离开作用域范围时,这个值将被丢弃 + + + +#### 变量作用域 + +作用域是一个变量在程序中有效的范围, 假如有这样一个变量: +```rust +let s = "hello" +``` + +变量`s`绑定到了一个字符串字面值,该字符串字面值是硬编码到程序代码中的。`s`变量从声明的点开始直到当前作用域的结束都是有效的: +```rust +{ // s 在这里无效, 它尚未声明 + let s = "hello"; // 从此处起,s 是有效的 + + // 使用 s +} // 此作用域已结束,s 不再有效 +``` + +简而言之,`s`从创建伊始就开始有效,然后有效期持续到它离开作用域为止,可以看出,就作用域这个概念,Rust语言跟其他编程语言没有区别。 + +#### 简单介绍String类型 + +之前提到过,本章会用String作为例子,因此这里会进行一下简单的介绍,具体的String学习请参见[String类型](../string.md)。 + +我们已经见过字符串字面值`let s ="hello"`,即被硬编码进程序里的字符串值。字符串字面值是很方便的,但是它并不适用于所有场景。原因有二: +- 字符串字面值是不可变的,因为被硬编码到程序代码中 +- 并非所有字符串的值都能在编写代码时就知道 + + +例如,要是想获取用户输入并存储该怎么办呢?这种情况,字符串字面值就完全无用武之地,为此,Rust 有第二个字符串类型,String。这个类型被分配到堆上,所以能够存储在编译时未知大小的文本,可以使用下面的方法基于字符串字面量来创建`String`类型: +```rust +let s = String::from("hello"); +``` + +`::`是一种调用操作符,这里表示调用`String`中的`from`方法,因为String存储在堆上,你也可以修改它: +```rust +let mut s = String::from("hello"); + +s.push_str(", world!"); // push_str() 在字符串后追加字面值 + +println!("{}", s); // 将打印 `hello, world!` +``` + +那么问题来了,为啥`String`可变,而字符串字面值却不可以? + + +## 内存与分配 + +就字符串字面值来说,我们在编译时就知道其内容,所以文本被直接硬编码进最终的可执行文件中。这使得字符串字面值快速且高效。不过这些特性都只得益于字符串字面值的不可变性。不幸的是,我们不能为了每一个在编译时大小未知的文本而将一块内存放入二进制文件中,并且它的大小还可能随着程序运行而改变。 + +对于 `String` 类型,为了支持一个可变、可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容,这些都是在程序运行时完成的: +- 首先向操作系统请求内存来存放`String`对象 +- 在使用完成后,将内存释放,归还给操作系统 + +其中第一个由`String::from`完成,它创建了一个全新的String. + +重点来了,到了第二部分,就是百家齐放的环节,在有**垃圾回收GC**的语言中,GC来负责标记并清除这些不再使用的内存对象,这些都是自动完成,无需开发者关心,非常简单好用;在无GC的语言,是开发者手动去释放这些内存对象,就像创建对象一样,需要通过编写代码来完成,因为未能正确释放对象造成的经济简直不可估量. + +对于Rust而言,安全和性能是写到骨子里的核心特性,使用GC牺牲了性能,使用手动管理内存牺牲了安全,那该怎么办?为此,Rust的开发者想出了一个无比惊艳的办法:变量在离开作用域后,就自动释放其占用的内存: + +```rust +{ + let s = String::from("hello"); // 从此处起,s 是有效的 + + // 使用 s +} // 此作用域已结束, + // s 不再有效,内存被释放 +``` + +与其它系统编程语言的`free`函数相同,Rust也提供了一个释放内存的函数:`drop`,但是不同的是,其它语言要手动调用`free`来释放每一个变量占用的内存,而Rust则在变量离开作用域时,自动调用`drop`函数: 上面代码中,Rust 在结尾的 `}` 处自动调用 `drop`。 + +> 其实,在 C++ 中,也有这种概念: *Resource Acquisition Is Initialization (RAII)*。如果你使用过 RAII 模式的话应该对 Rust 的 `drop` 函数并不陌生 + +这个模式对编写 Rust 代码的方式有着深远的影响,不过上面的例子还是太简单,来看看其它场景。 + +## 变量绑定背后的数据交互 + +#### 转移所有权 + +先来看一段代码: +```rust +let x = 5; +let y = x; +``` + +代码背后的逻辑很简单:“将 5 绑定到变量x;接着拷贝x的值赋给y”,最终`x`和`y`都等于`5`,因为整数是由固定大小的简单值,因此这两个值都被存在栈中,完全无需在堆上分配内存。 + +可能有同学会有疑问:这种拷贝不消耗性能吗?实际上,这种栈上数据的拷贝非常非常快,而且数据本身也足够简单,只要复制一个整数大小的内存即可,因此在这种情况下,拷贝的速度远比在堆上创建内存来得快的多。实际上,上一章我们讲到的Rust基本类型都是通过自动拷贝的方式来赋值的,就像上面代码一样。 + +然后再来看一段代码: +```rust +let s1 = String::from("hello"); +let s2 = s1; +``` +此时,可能某个大聪明(善意昵称)已经想到了:嗯,把s1的内容拷贝一份赋值给s2,实际上,并不是这样。之前也提到了,对于基本类型(存储在栈上),Rust会自动拷贝,但是`String`不是基本类型,而是存储在堆上的,因此并不能自动拷贝。 + +实际上,`String`类型是一个复杂类型,由存储在栈中的堆指针、字符串长度、字符串容量共同组成,其中堆指针是最重要的,它指向了真实存储字符串内容的堆内存,至于长度和容量,如果你有Go语言的经验,这里就很好理解:容量是堆内存分配空间的大小,长度是目前已经使用的大小, 详情见[字符串](../string.md#String底层剖析)一节. + +总之`String`类型指向了一个堆上的空间,这里存储着它的真实数据,这里对上面代码中的`let s2 = s1`分成两种情况讨论: +1. 拷贝所有数据 +如果该语句是拷贝所有数据(深拷贝),那么无论是`String`本身还是底层的堆上数据,都会被全部拷贝,这对于性能而言会造成非常大的影响 + +2. 只拷贝`String`本身 +这样的拷贝非常快,因为在64位机器上就拷贝了`8字节的指针`、`8字节的长度`、`8字节的容量`,总计24字节,但是带来了新的问题,还记得我们之前提到的所有权规则吧?其中有一条就是,一个值只允许有一个所有者,而现在这个值(堆上的真实字符串数据)有了两个所有者:`s1`和`s2`。 + +好吧,就假定一个值可以拥有两个所有者,会发生什么呢? + +之前我们提到过当变量离开作用域后,Rust 自动调用 `drop` 函数并清理变量的堆内存。不过由于两个`String`指向了同一位置。这就有了一个问题:当 s2 和 s1 离开作用域,它们都会尝试释放相同的内存。这是一个叫做 二次释放(double free)的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。 + +因此,Rust这样解决问题:**当`s1`赋予`s2`后,Rust认为`s1`不再有效,因此也无需在`s1`离开作用域后`drop`任何东西,这就是把所有权从`s1`转移给了`s2`**. + +再来看看,在所有权转移后再来使用旧的所有者,会发生什么: +```rust +let s1 = String::from("hello"); +let s2 = s1; + +println!("{}, world!", s1); +``` + +因为Rust禁止你使用无效的引用,你会看到以下的错误 +```console +error[E0382]: use of moved value: `s1` + --> src/main.rs:5:28 + | +3 | let s2 = s1; + | -- value moved here +4 | +5 | println!("{}, world!", s1); + | ^^ value used here after move + | + = note: move occurs because `s1` has type `std::string::String`, which does + not implement the `Copy` trait +``` + +现在再回头看看之前的规则,相信你已经有了更深刻的理解: +> 1. Rust中每一个值都`有且只有`一个所有者(变量) +> 2. 当所有者(变量)离开作用域范围时,这个值将被丢弃 + +如果你在其他语言中听说过术语 浅拷贝(shallow copy)和 深拷贝(deep copy),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量`s1`无效了,因此这个操作被称为 移动(move),而不是浅拷贝。上面的例子可以解读为 s1 被 移动 到了 s2 中。那么具体发生了什么,用一张图简单说明: + +s1 moved to s2 + +这样就解决了我们的问题,s1不再指向任何数据,只有s2是有效的,当`s2`离开作用域,它就会释放内存。 相信此刻,你应该明白了,为什么Rust称呼`let a = b`这种为**变量绑定**了吧? + + +#### 克隆(深拷贝) + +首先,**Rust 永远也不会自动创建数据的 “深拷贝”**。因此,任何**自动**的复制可以被认为对运行时性能影响较小。 + +如果我们**确实**需要深度复制`String`中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做`clone`的方法。 + +```rust +let s1 = String::from("hello"); +let s2 = s1.clone(); + +println!("s1 = {}, s2 = {}", s1, s2); +``` + +这段代码能够正常运行,因此说明s2确实完整的复制了s1的数据。 + +如果代码性能无关紧要,例如初始化程序时,或者在某段时间只会执行一次时,你可以使用`clone`来简化编程。但是对于执行较为频繁的代码,使用`clone`会极大的降低程序性能,需要小心使用! + +#### 拷贝(浅拷贝) + +浅拷贝只发生在栈上,因此性能很高,在日常编程中,浅拷贝无处不在。 + +再回到之前看过的例子: +```rust +let x = 5; +let y = x; + +println!("x = {}, y = {}", x, y); +``` + +但这段代码似乎与我们刚刚学到的内容相矛盾:没有调用 `clone`,不过依然实现了类似深拷贝的效果 - 没有报所有权的错误。 + +原因是像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 `y` 后使 `x` 无效。换句话说,这里没有深浅拷贝的区别,所以这里调用 `clone` 并不会与通常的浅拷贝有什么不同,我们可以不用管它。 + +Rust 有一个叫做 `Copy`的特征,可以用在类似整型这样的存储在栈上的类型上。如果一个类型拥有 `Copy`特征,一个旧的变量在将其赋值给其他变量后仍然可用。 + +那么什么类型是 `Copy` 的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则:**任何基本类型的组合可以是 `Copy` 的,不需要分配内存或某种形式资源的类型是 `Copy` 的**。如下是一些 `Copy` 的类型: + +* 所有整数类型,比如 `u32`。 +* 布尔类型,`bool`,它的值是 `true` 和 `false`。 +* 所有浮点数类型,比如 `f64`。 +* 字符类型,`char`。 +* 元组,当且仅当其包含的类型也都是 `Copy` 的时候。比如,`(i32, i32)` 是 `Copy` 的,但 `(i32, String)` 就不是。 + +## 函数传值与返回 +将值传递给函数,一样会发生`移动`或者`复制`,就跟`let`语句一样,下面的代码展示了所有权、作用域的规则: +```rust +fn main() { + let s = String::from("hello"); // s 进入作用域 + + takes_ownership(s); // s 的值移动到函数里 ... + // ... 所以到这里不再有效 + + let x = 5; // x 进入作用域 + + makes_copy(x); // x 应该移动函数里, + // 但 i32 是 Copy 的,所以在后面可继续使用 x + +} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走, + // 所以不会有特殊操作 + +fn takes_ownership(some_string: String) { // some_string 进入作用域 + println!("{}", some_string); +} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放 + +fn makes_copy(some_integer: i32) { // some_integer 进入作用域 + println!("{}", some_integer); +} // 这里,some_integer 移出作用域。不会有特殊操作 +``` + +你可以尝试在`takes_ownership`之后,再使用`s`,看看如何报错?例如添加一行`println!("在move进函数后继续使用s: {}",s);`。 + + +同样的,函数返回值也有所有权,例如: +```rust +fn main() { + let s1 = gives_ownership(); // gives_ownership 将返回值 + // 移给 s1 + + let s2 = String::from("hello"); // s2 进入作用域 + + let s3 = takes_and_gives_back(s2); // s2 被移动到 + // takes_and_gives_back 中, + // 它也将返回值移给 s3 +} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走, + // 所以什么也不会发生。s1 移出作用域并被丢弃 + +fn gives_ownership() -> String { // gives_ownership 将返回值移动给 + // 调用它的函数 + + let some_string = String::from("hello"); // some_string 进入作用域. + + some_string // 返回 some_string 并移出给调用的函数 +} + +// takes_and_gives_back 将传入字符串并返回该值 +fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域 + + a_string // 返回 a_string 并移出给调用的函数 +} +``` + + +所有权很强大,避免了内存的不安全性,但是也带来了一个新麻烦,你总要把一个值传来传去去使用它,传入一个函数,很可能还要从该函数传出去,结果就是语言表达变得非常啰嗦,幸运的是,Rust提供了新功能解决这个问题。 + diff --git a/src/basic/string.md b/src/basic/string.md new file mode 100644 index 00000000..d0366e68 --- /dev/null +++ b/src/basic/string.md @@ -0,0 +1,49 @@ +# 字符串 + +在其他语言,字符串往往是送分题,因为实在是太简单了,例如`"hello, world"`就是字符串章节的几乎全部内容了,对吧?如果你带着这样的想法来学Rust, +我保证,绝对会栽跟头,**因此这一章大家一定要重视,仔细阅读,这里有很多其它Rust书籍中没有的内容**。 + +首先来看段很简单的代码: +```rust +fn main() { + let my_name = "Pascal"; + greet(my_name); +} + +fn greet(name: String) { + println!("Hello, {}!", name); +} +``` + +`greet`函数接受一个字符串类型的`name`参数,然后打印到终端控制台中,非常好理解,你们猜猜,这段代码能否通过编译? + +```conole +error[E0308]: mismatched types + --> src/main.rs:3:11 + | +3 | greet(my_name); + | ^^^^^^^ + | | + | expected struct `std::string::String`, found `&str` + | help: try using a conversion method: `my_name.to_string()` + +error: aborting due to previous error +``` + +Bingo,果然报错了,编译器提示`greet`函数需要一个`String`类型的字符串,却传入了一个`&str`类型的字符串,相信读者心中现在一定有几头草泥马呼啸而过,怎么字符串也能整出这么多花活? + +接下来,让我们逐点分析讲解。 + +## 什么是字符串? + +顾名思义,字符串是由字符组成的连续集合,但是在上一节中我们提到过,**Rust中的字符是Unicode类型,因此每个字符占据4个字节内存空间,但是在字符串中不一样,字符串是UTF8编码,也就是字符所占的字节数是变长的(2-4)**,这样有助于大幅降低字符串所占用的内存空间. + +Rust在语言级别,只有一中字符串类型:`str`,它通常是以引用类型(更准确的说法是[借用](../../core/borrowing.md),这个概念在后面会讲)出现`&str`, + + +## String底层剖析 + +https://rustwiki.org/zh-CN/book/ch04-01-what-is-ownership.html#变量与数据交互的方式一移动 + +> 为何`String`长度和容量会不一致? +> 之前提到过`String`是一个可修改、可增长的字符串,因此它是可变的,但是不可能在每次改变,我们都重新生成一次堆上的内存空间,这种成本太高了,因此 \ No newline at end of file diff --git a/src/cargo/dependency.md b/src/cargo/dependency.md index 4864c25c..c823279c 100644 --- a/src/cargo/dependency.md +++ b/src/cargo/dependency.md @@ -1 +1,6 @@ # 依赖管理 + + +## 依赖升级 + +Minor note about your second point: You can use cargo update to update versions of transitive dependencies in your Cargo.lock when applicable; the very nice cargo-edit crate provides a cargo upgrade command which does the same for your Cargo.toml. If you use VSCode, I can also recommend the "crates" extension which shows available updates inline in your Cargo.toml. \ No newline at end of file diff --git a/src/core/borrowing.md b/src/core/borrowing.md deleted file mode 100644 index b2f6540e..00000000 --- a/src/core/borrowing.md +++ /dev/null @@ -1 +0,0 @@ -# 借用 diff --git a/src/core/intro.md b/src/core/intro.md deleted file mode 100644 index 3f7ffc1d..00000000 --- a/src/core/intro.md +++ /dev/null @@ -1 +0,0 @@ -# 核心概念 diff --git a/src/core/lifetime.md b/src/core/lifetime.md deleted file mode 100644 index 8b4f0eb7..00000000 --- a/src/core/lifetime.md +++ /dev/null @@ -1 +0,0 @@ -# 生命周期 diff --git a/src/core/ownership.md b/src/core/ownership.md deleted file mode 100644 index 07026cae..00000000 --- a/src/core/ownership.md +++ /dev/null @@ -1,8 +0,0 @@ -# 所有权 - - -## 通过Clone来进行深拷贝 - - - -## 通过Copy复制栈数据 \ No newline at end of file diff --git a/src/img/.DS_Store b/src/img/.DS_Store index e426050b..0a8d907d 100644 Binary files a/src/img/.DS_Store and b/src/img/.DS_Store differ diff --git a/src/img/borrowing-01.svg b/src/img/borrowing-01.svg new file mode 100644 index 00000000..b4bf2ebe --- /dev/null +++ b/src/img/borrowing-01.svg @@ -0,0 +1,87 @@ + + + + + + +%3 + + + +table0 + +s + +name + +value + +ptr + + + + +table1 + +s1 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + + +table0:c->table1:borrowee + + + + + +table2 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + + + +table1:c->table2:pointee + + + + + diff --git a/src/img/ownership01.svg b/src/img/ownership01.svg new file mode 100644 index 00000000..a0513abd --- /dev/null +++ b/src/img/ownership01.svg @@ -0,0 +1,96 @@ + + + + + + +%3 + + + +table0 + + +s1 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + + +table1 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + + + +table0:c->table1:pointee + + + + + +table3 + +s2 + +name + +value + +ptr + + +len + +5 + +capacity + +5 + + + +table3:c->table1:pointee + + + + + diff --git a/src/performance/clone-copy.md b/src/performance/clone-copy.md new file mode 100644 index 00000000..7b5a4db7 --- /dev/null +++ b/src/performance/clone-copy.md @@ -0,0 +1 @@ +# Clone和Copy