Rust 的所有权机制
Rust 的所有权机制
Rust语言以其独特的所有权机制在系统级编程领域崭露头角。本文将深入探讨Rust的所有权机制,包括其基本规则、变量范围、移动和克隆操作、函数中的所有权转移,以及引用与租借等核心概念。通过具体的代码示例,帮助读者全面理解Rust如何高效且安全地管理内存。
1. 引言
在前面的文章中,我们已经介绍了Rust的基本语法,包括环境搭建、Hello World、基础语法(变量、运算、注释、函数和循环)等。虽然Rust的基础语法与其他语言有相似之处,但其真正独特之处在于内存管理方式。本文将详细探讨Rust的核心特性——所有权机制。
2. Rust 所有权
C/C++语言需要开发者手动管理内存,而Java、Python等语言则依赖垃圾回收机制。Rust区别于这些语言的一大特点就是能够高效且安全地使用内存,这背后的机制就是“所有权”机制。
Rust的所有权规则有以下三条:
- 每个值都有一个变量,称为这个值的所有者;
- 一个值一次只能有一个所有者;
- 当所有者不在程序运行范围时,该值将被删除。
初看这三条规则可能不太好理解,接下来我们将通过具体示例来详细说明。
3. 变量的范围
变量的作用域(可行域)概念在C、C++、Java等语言中都存在,指的是一个变量从声明到生命终结的作用范围。例如:
{
// 在声明以前,变量 s 无效
let s = "runoob";
// 这里是变量 s 的可用范围
}
// 变量范围已经结束,变量 s 无效
一旦超出了变量作用的范围,Rust会自动销毁变量和值,这与很多语言在栈空间中分配内存的行为类似。
4. 移动和克隆
4.1 基本类型的移动操作
考虑一个简单的赋值操作:
fn main() {
let x = 5;
let y = x;
println!("x: {}, y: {}", x, y);
}
这个操作在C语言中会为变量y开辟一块新的空间,用来存储变量x的值,在Rust中也是类似的,这样的操作被称为栈中数据的Move操作。
对于基本数据类型,我们都可以执行这样的Move操作:
- 整数类型
- 布尔类型
- 浮点类型
- 字符类型
- 仅包含以上类型的元组
但需要注意的是,只有基本类型可以执行这样的操作,因为他们被分配在栈空间中。而对于字符串、对象、数组等在堆空间中分配的数据来说,这样的操作有着截然不同的行为:
fn main() {
let x = String::from("hello");
let y = x;
println!("x: {}", x);
}
执行上面的代码,会报错:
warning: unused variable: `y`
--> ownership.rs:3:9
|
3 | let y = x;
| ^ help: if this is intentional, prefix it with an underscore: `_y`
|
= note: `#[warn(unused_variables)]` on by default
error[E0382]: borrow of moved value: `x`
--> ownership.rs:4:23
|
2 | let x = String::from("hello");
| - move occurs because `x` has type `String`, which does not implement the `Copy` trait
3 | let y = x;
| - value moved here
4 | println!("x: {}", x);
| ^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to previous error; 1 warning emitted
For more information about this error, try `rustc --explain E0382`.
对于堆空间中分配的数据的所有者来说,在执行let y = x
语句后,x变量已经无效了,这个数据的所有权被移交给了y,此后,x是不能被使用的。
4.2 克隆
那么,即便是对于堆内存中的数据,我也仍然想要让x、y都能开辟独立的内存空间,那要怎么办呢?当然也是可以的,这时候只需要执行克隆操作:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
于是打印出了:
s1 = hello, s2 = hello
需要注意的是,克隆操作因为需要分配和写入数据,所以要花费更多的时间。
5. 变量在函数中的所有权机制
函数往往需要声明接收外部传入参数,在Rust中,此时就必须要关注所有权的转移问题。
例如:
fn main() {
let s = String::from("hello");
takes_ownership(s);
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
}
在main函数中,由于将s所有的字符串数据的所有权转移给了函数的传入参数some_string,在调用函数后,变量s便不能再进行使用,而在函数中,随着函数的结束,some_string也会随着作用域结束而被释放。于是,hello这个字符串数据也就不复存在了。
想要让数据不被销毁,只能将数据的所有权以返回值的方式传递到函数外:
fn main() {
let s = String::from("hello");
let x = takes_ownership(s);
println!("x: {}", x);
}
fn takes_ownership(some_string: String) -> String {
println!("{}", some_string);
return some_string;
}
6. 引用与租借
6.1 引用
综上所述,堆空间中分配的数据一旦经过赋值,就会转移所有权,让原变量失效,有时我们并不希望这样,例如在上一节的第一个例子中,虽然我们将s作为参数传递给了函数,但因为这个函数的功能仅仅是用来打印s的值,我们并不希望数据被销毁,而反复传递所有权又显得过于复杂,有没有更为简单的方法呢?
答案是有的,引用可以解决这一问题,对于C++和Java程序员来说,引用一定不陌生,但在Rust语言中却有所不同:
fn main() {
let s1 = String::from("hello");
let s2 = &s1;
println!("s1 is {}, s2 is {}", s1, s2);
}
可以看到,通过&操作符,让s2成为了s1的引用,s1并不会失效,这是因为s2仅仅租借了s1对数据的所有权,只要s1持有这个数据的所有权,s2也就可以对数据进行操作,但s2并没有数据的实际所有权。
6.2 在函数中传递引用
更常用的是,在函数中通过引用来传递参数:
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()
}
6.3 租借
要记住,引用并没有数据的实际所有权,也就是原变量一旦失去数据的所有权,他的所有引用也同时会失效。例如:
fn main() {
let s1 = String::from("hello");
let s2 = &s1;
let s3 = s1;
println!("{}", s2);
}
这段代码会报错:
warning: unused variable: `s3`
--> reference.rs:4:9
|
4 | let s3 = s1;
| ^^ help: if this is intentional, prefix it with an underscore: `_s3`
|
= note: `#[warn(unused_variables)]` on by default
error[E0505]: cannot move out of `s1` because it is borrowed
--> reference.rs:4:14
|
3 | let s2 = &s1;
| --- borrow of `s1` occurs here
4 | let s3 = s1;
| ^^ move out of `s1` occurs here
5 | println!("{}", s2);
| -- borrow later used here
error: aborting due to previous error; 1 warning emitted
For more information about this error, try `rustc --explain E0505`.
因为s2租借的s1已经将所有权移动到s3,所以s2将无法继续租借使用s1的所有权。如果需要使用s2使用该值,必须重新向s3租借:
fn main() {
let s1 = String::from("hello");
let mut s2 = &s1;
let s3 = s1;
s2 = &s3; // 重新从s3租借所有权
println!("{}", s2);
}
6.4 可变引用
另一个需要注意的点是,上述的引用变量都是不能对数据进行修改的,如果想要让引用的变量能够修改数据,那么就要使用可变引用:
fn main() {
let mut s1 = String::from("run");
// s1 是可变的
let s2 = &mut s1;
// s2 是可变的引用
s2.push_str("oob");
println!("{}", s2);
}
但需要注意的是,一个变量可以有多个不可变引用,但只能有一个可变引用。一旦一个值被可变引用,它就不能再被任何引用。
本文详细介绍了Rust语言的所有权机制,包括其基本规则、变量范围、移动和克隆、函数中的所有权机制以及引用与租借等内容。通过具体的代码示例,帮助读者理解Rust独特的内存管理方式,这对于学习和使用Rust语言的开发者来说具有很高的参考价值。