参考:https://kaisery.github.io/trpl-zh-cn/ch04-01-what-is-ownership.html、https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html
栈、堆、所有权
管理计算机内存的方式:
- 垃圾回收机制:在程序运行时不断地寻找不再使用的内存
- 程序员必须亲自分配和释放内存
- 通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查
栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。
栈 (Stack)
- 以放入值的顺序存储值并以相反顺序取出值。这也被称作 后进先出(last in, first out)
- 增加数据叫做 进栈(pushing onto the stack);移出数据叫做 出栈(popping off the stack)
- 栈中的所有数据都必须占用已知且固定的大小,在编译时将数据推入栈中并不被认为是分配
- 指针的大小是已知并且固定的,你可以将指针存储在栈上,不过当需要实际数据时,必须访问指针
- 当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。
类比叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!
堆 (Heap)
大小未知或大小可能变化的数据,要改为存储在堆上。堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。
- 操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针(pointer)。这个过程称作 在堆上分配内存(allocating on the heap),有时简称为 “分配”(allocating)。
- 入栈比在堆上分配内存要快,因为(入栈时)操作系统无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为操作系统必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。
- 访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。
- 处理器在处理的数据彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。在堆上分配大量的空间也可能消耗时间。
类比:假设有一个服务员在餐厅里处理多个桌子的点菜。在一个桌子报完所有菜后再移动到下一个桌子是最有效率的。从桌子 A 听一个菜,接着桌子 B 听一个菜,然后再桌子 A,然后再桌子 B 这样的流程会更加缓慢。
所有权系统 (ownership) 与栈、堆
所有权要处理的事情:跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间
- 一旦理解了所有权,就不需要经常考虑栈和堆了,不过明白了所有权的存在就是为了管理堆数据,能够帮助解释为什么所有权要以这种方式工作。
所有权
所有权的规则:
- Rust 中的每一个值都有一个被称为其 所有者(owner)的变量。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
作用域 (scope )。作用域是一个项(item)在程序中有效的范围。
String
String
类型可以使用 from
函数基于字符串字面值来创建String
,比如let s = String::from("hello");
,s 是 String 类型,而 "hello"
是 “str 类型”,这个语句意味着运行时向操作系统请求内存请求 String 所需的内存,而不是把数据硬编码进二进制文件。
可以修改此类字符串 :s.push_str(", world!"); // push_str() 在字符串后追加字面值
支持一个可变,可增长的文本片段,所以需要在堆上分配一块在编译时未知大小的内存来存放内容:这意味着
- 必须在运行时向操作系统请求内存。
- 需要一个当我们处理完
String
时将内存返回给操作系统的方法 ```![allow(unused)]
fn main() { { let s = String::from(“hello”); // 从此处起,s 是有效的 // 可使用 s } // 此作用域已结束,
}// s 不再有效
当 String 类型的变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 `drop`,在这里 `String` 的作者可以放置释放内存的代码。Rust 在结尾的 `}` 处自动调用 `drop`。<br />析构函数 destructor:与构造函数相反,指当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行的函数,在 Rust 中即实现 Drop trait,但是 drop 一般针对堆中数据,具有 Copy trait 的数据是不会被 drop 的,因为 "Copy trait 不允许存在 Drop trait"。
> 在 C++ 中,这种 item 在生命周期结束时释放资源的模式有时被称作 **资源获取即初始化** (_Resource Acquisition Is Initialization (RAII)_ )。
不定长度的数据类型不支持 Copy trait,比如下面的例子在 Rust 中`s1`被**移动**(move) 到了`s2`中,这充分反映了所有权规则:对象的指针只被一个变量所拥有,不存在“内存共享”。
let s1 = String::from(“hello”); let s2 = s1; println!(“{}, world!”, s1); // error: value used here after move
<br />如果我们 **确实** 需要深度复制 `String` 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 `clone` 的通用函数。
fn main() { let s1 = String::from(“hello”); let s2 = s1.clone(); println!(“s1 = {}, s2 = {}”, s1, s2); }
此外有一个设计选择:Rust 永远也不会**自动**创建数据的 “深拷贝”(手动创建可定义 "Clone trait")。因此,任何**自动**的复制("Copy trait")可以被认为对运行时性能影响较小。<br />这是自动复制的例子:
let x = 5; let y = x; // i32 是 Copy 的,所以在后面可继续使用 x println!(“{} {}”, x, y);
将 `5` 绑定到 `x`;接着生成一个值 `x` 的拷贝并绑定到 `y`”。现在有了两个变量,`x` 和 `y`,都等于 `5`。因为整数是有已知固定大小的简单值,所以这两个 `5` 被放入了栈中。
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 移出作用域。不会有特殊操作
## Clone / Copy trait
> 在一些语言中有深浅拷贝的概念:
> **浅拷贝** 只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。也就是简单的内存拷贝(或者C语言的按位拷贝 memcpy)。
> **深拷贝** 会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
但是 Rust 更倾向于使用 Copy trait 和 Clone trait 来描述 浅拷贝 和 深拷贝:<br />Copy trait 可以看成浅拷贝,但是有一些规则和特殊性:
- Copy 的全名是 `std::marker::Copy`。而 `std::marker` 模块里面的所有的 trait 都是特殊的 trait。目前稳定的有四个: Copy、Send、Sized、Sync。它们的特殊之处在于它们是跟编译器密切绑定的,impl 这些 trait 对编译器的行为有重要影响。在编译器眼里,它们与其它的 trait 不一样。这几个 trait 内部都没有方法,它们的唯一任务是,给类型打一个“标记”,表明它符合某种约定,这些约定会影响编译器的静态检查以及代码生成。
- 如果一个类型 impl 了 Copy trait,意味着任何时候,只拷贝存储在栈上的位来复制值而不需要额外的代码,来实现该类型的复制。这意味着 变量绑定、函数参数传递、函数返回值传递等场景,使用 copy 语义,而不是默认的 move 语义
- 只有 "POD 数据类型" 才有资格实现 Copy trait,但不是所有的 POD 类型都应该实现 Copy trait
- 对于 struct 和 enum 类型,不会自动实现 Copy trait。而且只有当 struct 和 enum 内部每个元素都是Copy类型的时候,编译器才允许我们针对此类型实现(derive) Copy trait
- Copy 操作通常在栈上拷贝,而且编译器可以优化实现 Copy trait 的类型,这意味着无需显式调用 Clone/Copy trait,这让代码更简洁
- 任何使用 Copy trait 的代码都可以通过 Clone trait 实现,但代码可能会稍慢,或者不得不在代码中的许多位置上使用 Clone trait
类比 C 风格 [POD](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Passive_data_structure) (Plain Old Data,或者叫 PDS: Passive Data Structure) 概念,Rust 也可以有 POD 的数据类型,只要一种 struc 或类型同时满足:
1. 只包含特定数据类型的成员:整数、浮点数、布尔 bool、字符类型 char、所有元素都是 Copy trait 的 数组或元组、共享借用指针&
1. 没有指针类型的成员
1. 没有自定义"析构函数",即自身或其任何部分没有 Drop trait
不能实现 Copy trait 的类型:Box、 String、 Vec、(i32, String) 之类存在不可 Copy trait 元素的元组、[String, String] 之类的容纳不可 Copy trait 元素的数组、可写借用指针&mut<br />例子:"整数自动 Copy"
---
Clone trait 不仅仅是深拷贝,它的特殊性:
1.
Clone 的全名是 `std::clone::Clone` 。它的完整声明是这样的:
pub trait Clone : Sized { fn clone(&self) -> Self; fn clone_from(&mut self, source: &Self) { *self = source.clone() } }
它有两个关联方法,其中 clone_from 是有默认实现的,它依赖于 clone 方法的实现。clone 方法没有默认实现,需要我们手动实现,甚至可以根据情况在 clone 函数中编写**任意的逻辑**。
1.
Clone trait 一般用于“基于语义的复制”操作。它做什么事情,跟具体类型的作用息息相关。比如对于 Box 类型,clone 就是执行的“深拷贝”,而对于 Rc 类型,clone 做的事情就是把引用计数值加1。
1.
对于实现了 Copy trait 的类型,它的 Clone trait 应该跟 Copy 语义相容,等同于按位拷贝。
参考:[知乎:Clone VS Copy](https://zhuanlan.zhihu.com/p/21730929)、[Rust Book:变量与数据交互的方式(二):克隆](https://kaisery.github.io/trpl-zh-cn/ch04-01-what-is-ownership.html#%E5%8F%98%E9%87%8F%E4%B8%8E%E6%95%B0%E6%8D%AE%E4%BA%A4%E4%BA%92%E7%9A%84%E6%96%B9%E5%BC%8F%E4%BA%8C%E5%85%8B%E9%9A%86)、[Rust Book:复制值的 Clone 和 Copy](https://kaisery.github.io/trpl-zh-cn/appendix-03-derivable-traits.html#%E5%A4%8D%E5%88%B6%E5%80%BC%E7%9A%84-clone-%E5%92%8C-copy)
# 引用 (Reference)
## 不变引用 / 不变借用 (Borrow)
不变引用 (reference):
- 允许使用值但不获取其所有权,当引用离开作用域时其指向的值不会被丢弃,但是引用(的指针地址)和引用绑定的变量名会被丢弃
- 使用方法: `&变量名` 相应的类型 `&类型`
- 作用域:从声明的地方开始一直持续到最后一次使用为止
- 可以和多个不变引用同时使用
不变借用 (borrowing):将获取引用作为函数参数。当函数使用引用而不是实际值作为参数,无需返回值来交还所有权,因为就不曾拥有所有权。
fn main() { let s = String::from(“hello”); let s1 = &s; // &x: reference 引用 let s2 = f(&s); // &x: borrowing 借用 println!(“{} {} {}”, s, s1, s2); // 结果:hello hello hello }
fn f(s: &String) -> &String { s }

## 可变引用 / 可变借用
- 对引用/借用的变量进行修改
- 使用方法: `&mut 变量名` 相应的类型 `&mut 类型`
- 在特定作用域中的特定数据只能有一个可变引用
fn main() { let mut s = String::from(“hello”); change(&mut s); println!(“{}”, s); // hello, world }
fn change(some_string: &mut String) { some_string.push_str(“, world”); }
引用/借用的规则:
1. 在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用。不能在拥有不可变引用的同时拥有可变引用。<br />
_(注意理解:在任意给定时间、拥有 这些词语的含义)_:不变引用不能和可变引用同时使用<br />
这并不是说不变引用和可变引用在代码里面只能出现一种,而是两种引用都存在的情况下,必须其中一种引用结束才能使用另一种引用。<br />
比如 所有的可变引用必须离开作用域之后才能使用不变引用,这样如果可变引用改变数据就完全不会那些不变引用。
1. 引用必须总是有效的。<br />
Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。<br />
悬垂指针(dangling pointer):释放内存时保留指向它的指针,这是一种错误且无效行为,因为悬垂指针指向的内存可能已经被分配给其它持有者。
来看一段同时使用两种引用的错误的代码:
fn main() {
let mut s = String::from(“hello”);
let s1 = &mut s; // 可变引用在此处被声明
// 我们没有利用可变引用改写值
println!(“{} {}”, s, s1); // 出错:cannot borrow s
as immutable because it is also borrowed as mutable
}
```
error[E0502]: cannot borrow `s` as immutable because it is also borrowed as mutable
--> src/main.rs:4:23
|
3 | let s1 = &mut s;
| ------ mutable borrow occurs here
4 | println!("{} {}", s, s1);
| ^ -- mutable borrow later used here
| |
| immutable borrow occurs here
因为如果可变引用中途改写了数据,而不变引用的值意外被改变了: println!("{} {}", s, s1.push('a'));
这样的语句会造成 “数据竞争”,因此编译器禁止可变引用与不可变引用同时发挥作用。
注意:println!
形式上捕获了变量 s,而实际上捕获了 s 的不变引用 &s
。所以这一行语句同时拥有不变引用和可变引用,而这恰好违反了引用的规则。
再看一段符合引用规则的例子:同时使用多个不变引用(因为它们不影响数据的值),然后使用一个可变引用(无论实际上有没有改变值)。
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);
}
数据竞争(data race)类似于竞态条件,它可由这三个行为造成:
- 两个或更多指针同时访问同一数据。(引用并不是直接让指针访问数据,引用 “首先返回的是指针”)
- 至少有一个指针被用来写入数据。
- 没有同步数据访问的机制。
数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!
参考:https://kaisery.github.io/trpl-zh-cn/ch04-02-references-and-borrowing.html
slice 类型
和引用一样,是一种没有所有权的数据类型。slice (切片) 允许你引用集合中一段连续的元素序列,而不用引用整个集合。
slice 和数组类似,但其大小在编译时是不确定的。
slice 是一个双字 对象(two-word object),第一个字是一个指向数据的指针,第二个字是切片的长度。这 个 “字” 的宽度和 usize 相同,由处理器架构决定,比如在 x86-64 平台上就是 64 位。 slice 可以用来借用数组的一部分。slice 的类型标记为&[T]
。
一些 slice 类型的例子:比如通过返回部分字符串的索引来同步访问部分字符串的值、返回部分数组的索引来同步访问部分数组的值
- “字符串字面值”
str
就是 slice:比如let s = "Hello, world!";
,其中 s 的类型是&str
,即一个指向二进制程序特定位置的 slice,一个不可变引用。所以字符串字面值是不可变的。
- 其他具体类型的 slice 具有
&变量名[x..y]
的形式(x 到 y 是左闭右开,x 和 y 可以省略一个或者都省略):”String 类型” 的 slice 具体类型是&str
,”数组” 的 slice 具体类型是&[数组元素类型]
。
此外,range 还可以用=
来表明需要取到右端点:&变量名[x..=y]
。
数组 slice 例子:
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // slice 类型: &[i32]
println!("{:?}", slice); // [2, 3]
}
字符串相关的 slice 例子:获取第一个单词
#![allow(unused)]
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// first_word 中传入 `String` 的 slice
let word = first_word(&my_string[..]); // word 类型:&str
let my_string_literal = "hello world"; // my_string_literal 类型:&str
// first_word 中传入字符串字面值的 slice
let word = first_word(&my_string_literal[..]);
// 因为字符串字面值 **就是** 字符串 slice,
// 这样写也可以,即不使用 slice 语法!
let word = first_word(my_string_literal);
}