一、为什么、是什么、怎么用
1、为什么Rust要提出一个所有权和借用的概念
所有的程序都必须和计算机内存打交道,如何从内存中申请空间来存放程序的运行内容,如何在不需要的时候释放这些空间,成为所有编程语言设计的难点之一。
主要分为三种流派:
-
(1)垃圾回收机制(GC),在程序运行时不断寻找不再使用的内存,典型代表:Java、Go
(2)手动管理内存的分配和释放, 在程序中,通过函数调用的方式来申请和释放内存,典型代表:C++
(3)通过所有权来管理内存,编译器在编译时会根据一系列规则进行检查。避免手动或者运行时垃圾回收带来的额外成本。
讲到内存,在编程语言中,都会讲到栈、堆,栈和堆的结构特性决定了一些值适合放在哪些位置,能够有更好的性能和空间效率。
-
(1)栈,主要是存储局部变量,栈中的所有数据都必须占用已知且固定大小的内存空间。
优点:用完即出,也很好出,性能很好 缺点:无法存储大小未知或者可能变化的数据。
(2)堆,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。
优点:空间大,能够存储大小未知或者可能变化的数据。 缺点:数据组织较为复杂,需要回收内存空间。
2、Rust所有权是什么
先来介绍所有权的几个概念
-
Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者,且有且仅有一个所有者。
当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
特别地,为了便于理解,我认为基本数据类型的值**,比如i32、boolean等基本类型,有别的机制处理这种基本类型的所有权问题:会直接拷贝栈上数据,新生成一个值,把新值的所有权给新变量,不会把旧值的所有权给新变量(新拥有者),没有发生所有权变化的现象。这种叫做Copy行为。
先说一下字符串类型,注意我说的是字符串类型,对于
let s = "hello";
上面这段代码中的“hello”,可以理解成Java中的字符串字面值,不是存储在堆上的,可以想象成存储在一个文件里的。和字符串类型不是一个东西,对于这种值,可以理解成没有所有权的概念,大家都只是持有一个引用它的指针。
那么 什么是字符串类型的数据,比如
let s1 = String::from("hello");
let s2 = s1;
我们来分析上述代码的
第一行代码 String::from(“hello”);
-
会在堆中找到一片地址空间,存储字符串类型数据"hello"。并返回该堆中数据的地址、长度、容量等数据,此时堆中的数据就叫做值。
然后在栈中生成一个结构体变量s1,该结构体就是字符串结构,保存了堆中数据的地址、长度、容量等数据。s1变量是堆中数据(值)的拥有者。
第二行代码 let s2 = s1;
这种行为,就是将s1的值的所有权移交给了s2,即堆中数据"hello"此时的拥有者是变量上s2,s1已经没有"hello"的所有权,我们不能再通过s1去访问或者修改堆中数据"hello"。
如果此时想通过s1再次访问堆中数据"hello",就会报错
fn main() {let s1 = String::from("hello");let s2 = s1;println!("{}, world!", s1);}
error[E0382]: borrow of moved value: `s1`--> src/main.rs:6:26|
3 | let s1 = String::from("hello");| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
4 | let s2 = s1;| -- value moved here
5 |
6 | println!("{}, world!", s1);| ^^ value borrowed here after move|= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
再来说一下这样移交所有权有什么好处。
- 当变量离开作用域后,Rust 会自动调用 drop 函数并清理变量的堆内存。不过由于两个 String
变量指向了同一位置。这就有了一个问题:当 s1 和 s2 离开作用域,它们都会尝试释放相同的内存。这是一个叫做 二次释放(double free) 的错误,也是之前提到过的内存安全性 BUG 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞
可以理解成,所有权移交之后,就可以大胆放心的抛弃先前的拥有者。来个图加深一下印象
s1不再指向堆中数据,s2指向堆中数据
3、我就不想移交所有权,我又想生成一个新变量指向相同数据
这里可以用到深拷贝,即在堆中生成一份相同的数据,赋给新变量。如代码
fn main() {let s1 = String::from("hello");let s2 = s1.clone();println!("{}, world!", s1);}
但是这么做是有性能消耗的,因为你需要复制一份数据,万一你这个数据非常大,复制起来非常耗时耗资源
而对于栈上变量,直接都是深拷贝,其实不是叫深拷贝,是达到深拷贝的效果,但是Rust叫做Copy
这里可以给出一个通用的规则: 任何基本类型的组合可以 Copy ,不需要分配内存或某种形式资源的类型是可以 Copy 的。如下是一些 Copy 的类型:
- 所有整数类型,比如 u32
- 布尔类型,bool,它的值是 true 和 false
- 所有浮点数类型,比如 f64 字符类型,char
- 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是
- 不可变引用 &T ,
但可变引用 &mut T 是不可以 Copy的
3、函数传参和返回值,都是会移交所有权的
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);。
有时我就想用一下,老是移来移去多麻烦,下篇我们讲引用与借用