Rust 所有权
rust通过所有权系统管理内存,该系统具有一组在编译时检查的规则。程序运行时,所有所有权功能都不会减慢其运行速度。
所有权规则
首先,让我们看一下所有权规则。在通过示例进行说明时,请牢记以下规则:
- Rust中的每个值都有一个变量,称为其所有者。
- 一次只能有一个所有者。
- 当所有者超出范围时,该值将被删除。
可变范围
假设我们有一个看起来像这样的变量:
let s = "hello";
接下来我们来看s的有效范围
fn main() { { // s尚未声明,无效 let s = "hello"; // s声明 // s有效 } //括号后,s将不再有效。 }
换句话说,这里有两个重要的时间点:
- 当s在作用域中,它是有效的。
- 作用域结束,s将无效。
内存的分配与释放
内存分为栈和堆。在rust中栈内存的管理又其他编程语言类似,这里就不说明。 特殊的是堆中的内存。 在具有垃圾收集器(GC)的语言中,GC会跟踪并清理不再使用的内存,因此我们无需考虑它。如果没有GC,我们有责任确定何时不再使用内存,并调用代码以显式返回内存,就像我们请求内存一样。从历史上看,正确执行此操作一直是编程难题。如果我们忘记了,我们将浪费内存。如果我们做得太早,我们将有一个无效的变量。如果我们做两次,那也是一个错误。我们需要将正一allocate与正一配对free。
Rust采取了另一条路径:拥有内存的变量超出范围后,内存将自动回收。
fn main() { { let s = String::from("hello"); // 从现在开始,s是有效的 //s有效 } // 现在这个范围结束了,所以s无效了 }
s超出范围时。当变量超出范围时,Rust为我们调用一个特殊函数。该函数称为drop。Rust会drop在右花括号处自动调用。
对堆:移动
堆中的数据交互,不同与栈中的数据交互。移动仅仅是对堆栈的操作。
我们先来看栈,栈中的数据交互是与其他语言类似的。
fn main() { let x = 5; let y = x; }
我们可能会猜到它在做什么:“将值绑定5到x;然后复制其中的值x并将其绑定到y。” 现在,我们有两个变量x 和y,并且都相等5。确实发生了这种情况,因为整数是具有已知固定大小的简单值(标量),并且这两个5值被压入堆栈。
我们再来看堆,String是一种典型的在堆中分配的内存。
fn main() { let s1 = String::from("hello"); let s2 = s1; }
这里s1被初始化为一个String,内容为hello,后将s1的值移动到s2,此时s2=“hello”,而s1失效。这与其他语言不相同,但对内存安全有极大的意义。
移动原理
现在我们来看其底层原理: 上图中,可见String由三个部分组成
- 指向右图(string内容)的指针。
- 字符串长度。
- 数据结构的容量。
左图这部分数据存在栈中,右图的数据存在堆中。 在将String数据复制时,rust只复制了栈上的指针(还没结束,不是结论)。如下图:
之前说过,当变量超出范围后,rust会自动调用drop函数清楚该变量的堆内存,但是上图明显有两个指向同一个个堆内存的指针,显而易见,该堆内存将会被释放两次,我们将这种错误成为双重释放错误。 为了确保内存安全,rust使移动后的s1失效,而不是尝试复制分配的内存,因此rust不需要在s1超出范围后释放任何内容。
接下来给你展示的使调用失效变量的错误:
fn main() { let s1 = String::from("hello"); let s2 = s1; println!("{}, world!", s1); }
错误:
$ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0382]: borrow of moved value: `s1` --> src/main.rs:5:28 | 2 | let s1 = String::from("hello"); | -- move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait 3 | let s2 = s1; | -- value moved here 4 | 5 | println!("{}, world!", s1); | ^^ value borrowed here after move error: aborting due to previous error For more information about this error, try `rustc --explain E0382`. error: could not compile `ownership`. To learn more, run the command again with --verbose.
克隆
在有些必要的时候,我们确实想深入复制,拥有两个副本,那我们可以使用clone。
fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2); }
底层则会如下图:
对栈:复制
移动是对堆数据的操作,复制是对栈数据的操作。
fn main() { let x = 5; let y = x; }
x与y都将等于5且有效。
以下数据类型都是指向复制操作:
- 所有整数类型,例如u32。
- 布尔类型,bool值true和false。
- 所有浮点类型,例如f64。
- 字符类型char。
- 元组(如果它们仅包含also的类型)Copy。例如, (i32, i32)是Copy,但(i32, String)不是。
关于函数的所有权问题
将值传递给函数作为参数,就像赋值一样,也会触发复制与移动操作。 以下代码将展示变量的作用域。
fn main() { let s = String::from("hello"); //s进入范围 takes_ownership(s); // s的值传入函数 // 应为s是堆中内存,所以s被移动到了函数中的参数中。s无效了。 let x = 5; // x进入范围 makes_copy(x); // 因为x是栈中内存,所以x被复制到函数的参数中 //x依旧有效 } //x超出范围,x从此无效。 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超出范围,从此无效。
返回值和范围
返回值也可以转移所有权。
fn main() { let s1 = gives_ownership(); //函数的返回值移动至s1 let s2 = String::from("hello"); // s2 进入范围 let s3 = takes_and_gives_back(s2); // s2移动至函数参数,s2失效,s3接受函数返回值,进入范围 } // s1与s3超出范围,调用drop,释放内存。 fn gives_ownership() -> String { let some_string = String::from("hello"); // some_string 进入范围 some_string // some_string作为返回值移动到调用的函数。 } fn takes_and_gives_back(a_string: String) -> String { // a_string 进入范围 a_string // a_string 作为返回值移动到调用的函数。 }
下一章:Rust 变量和可变性
在默认情况下rust变量是不可变的。这样可以使代码更加安全。让我们探讨一下Rust如何以及为什么鼓励您支持变量不可变,以及为什么有时您可以选择可变变量。 声明变量 通过let关键字声明变量,可以不声明变量类型,交由编 ...