Rust核心之所有权和生命周期
1 所有权
1.1 简介
所有权对大多数开发者而言是一个新颖的概念,它是 Rust 为高效使用内存而设计的语法机制。所有权概念是为了让 Rust 在编译阶段更有效地分析内存资源的有用性以实现内存管理而诞生的概念。
所有权有以下三条核心规则:
Rust中的每个值都有一个变量,称为其所有者- 一次只能有一个所有者
- 当所有者不在程序运行范围时,该值将被删除
变量范围示例:
{
// 在声明以前,变量 s 无效
let s = "test";
// 这里是变量 s 的可用范围
}
// 变量范围已经结束,变量 s 无效
变量范围是变量的一个属性,其代表变量的可行域,默认从声明变量开始有效直到变量所在域结束。
1.2 变量与数据交互的方式
变量与数据交互方式主要有移动(Move)和克隆(Clone)两种:
1.2.1 移动
多个变量可以在 Rust 中以不同的方式与相同的数据交互:
let x = 5;
let y = x;
这个程序将值 5 绑定到变量 x,然后将 x 的值复制并赋值给变量 y,栈中将有两个值 5,因为 i32 实现了 Copy trait,x 在赋值后仍然可用。实现了 Copy trait 的类型(如整数、浮点、布尔、字符,以及元素均为 Copy 类型的元组和数组),数据本身简单,存储在栈上,复制开销很小。
但如果发生交互的数据在堆中就是另外一种情况:
let s1 = String::from("hello");
let s2 = s1;
第一步产生一个 String 对象,值为 "hello"。其中 "hello" 可以认为是类似于长度不确定的数据,需要在堆中存储。
第二步的情况略有不同(仅用来对比参考)

如图所示:两个 String 对象在栈中,每个 String 对象都有一个指针指向堆中的 hello 字符串。在给 s2 赋值时,只有栈中的数据被复制了,堆中的字符串依然还是原来的字符串。
当变量超出范围时,Rust 自动调用释放资源函数并清理该变量的堆内存。但是 s1 和 s2 都被释放的话堆区中的 hello 被释放两次,这是不被系统允许的。为了确保安全,在给 s2 赋值时 s1 已经无效了。因此,在把 s1 的值赋给 s2 以后 s1 将不可以再被使用。
所以实际情况是,s1 名存实亡

1.2.2 克隆
Rust会尽可能地降低程序的运行成本,所以默认情况下,长度较大的数据存放在堆中,且采用移动的方式进行数据交互。但如果需要将数据单纯的复制一份以供他用,可以使用数据的第二种交互方式——克隆。
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
运行结果:
s1 = hello, s2 = hello
这里是真的将堆中的 hello 复制了一份,所以 s1 和 s2 都分别绑定了一个值,释放的时候也会被当作两个资源。
当然,克隆仅在需要复制的情况下使用,毕竟复制数据会花费更多的时间。
1.3 函数的所有权机制
入参:将变量当作函数的参数传给其他函数,那么它和移动的效果是一样的
出参:被当作函数返回值的变量所有权将会被移动出函数并返回到调用函数的地方,而不会直接被无效释放。
入参示例:
fn main() {
let s = String::from("hello");
// s 被声明有效
takes_ownership(s);
// s 的值被当作参数传入函数
// 所以可以当作 s 已经被移动,从这里开始已经无效
// println!("{}",s); // 不能访问
let x = 5;
// x 被声明有效
makes_copy(x);
// x 的值被当作参数传入函数
// 但 x 是基本类型,依然有效
// 在这里依然可以使用 x 却不能使用 s
println!("{}",x);//可以继续访问
} // 函数结束, x 无效, 然后是 s. 但 s 已被移动, 所以不用被释放
fn takes_ownership(some_string: String) {
// 一个 String 参数 some_string 传入,有效
println!("{}", some_string);
} // 函数结束, 参数 some_string 在这里释放
fn makes_copy(some_integer: i32) {
// 一个 i32 参数 some_integer 传入,有效
println!("{}", some_integer);
} // 函数结束, 参数 some_integer 是基本类型, 无需释
出参示例:
fn main() {
let s1 = gives_ownership();
// gives_ownership 移动它的返回值到 s1
let s2 = String::from("hello");
// s2 被声明有效
let s3 = takes_and_gives_back(s2);
// s2 被当作参数移动, s3 获得返回值所有权
} // s3 无效被释放, s2 被移动, s1 无效被释放.
fn gives_ownership() -> String {
let some_string = String::from("hello");
// some_string 被声明有效
return some_string;
// some_string 被当作返回值移动出函数
}
fn takes_and_gives_back(a_string: String) -> String {
// a_string 被声明有效
a_string // a_string 被当作返回值移出函数
}
1.4 引用与租借
引用(Reference)是 C++ 开发者较为熟悉的概念,如果熟悉指针,可以把它看作一种指针。
实质上引用是变量的间接访问方式,& 运算符可以取变量的"引用"。
当一个变量的值被引用时,变量本身不会被认定无效。因为引用并没有在栈中复制变量的值
借用规则:
- 可以同时有多个不可变借用
- 不可变借用存在时,不能有可变借用
- 同一时刻只能有一个可变借用
- 借用不能比原变量活得更久
fn main() {
let s1 = String::from("hello");
let s2 = &s1;
println!("s1 is {}, s2 is {}", s1, s2);
}
运行结果:
s1 is hello, s2 is hello
引用不会获得值的所有权,引用只能租借(Borrow) 值的所有权,引用本身也是一个类型并具有一个值,这个值记录的是别的值所在的位置,但引用不具有所指值的所有权:
既然引用不具有所有权,即使它租借了所有权,它也只享有使用权(这跟租房子是一个道理),如果尝试利用租借来的权利来修改数据会被阻止:
fn main() {
let s1 = String::from("hello");
let s2 = &s1;
println!("{}", s2);
// s2.push_str("test"); // 错误,禁止修改租借的值
println!("{}", s2);
}
这段程序中 s2 尝试修改 s1 的值被阻止,租借的所有权不能修改所有者的值。
当然,也存在一种可变的租借方式,就像租一个房子,如果物业规定房主可以修改房子结构,房主在租借时也在合同中声明赋予这种权利,那可以重新装修房子的:(使用 &mut 修饰可变的引用类型)
fn main() {
let mut s1 = String::from("hello");
// s1 是可变的
let s2 = &mut s1;
// s2 是可变的引用
s2.push_str("test");
println!("{}", s2);
}
可变引用与不可变引用相比除了权限不同以外,可变引用不允许多重引用,但不可变引用可以:
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
这段程序不正确,因为多重可变引用了 s。
let r;
{
let y = 10;
r = &y; // 编译错误,y 出作用域就被销毁了
}
println!("{}", r);
2 生命周期
2.1 简介
Rust 生命周期机制是与所有权机制同等重要的资源管理机制,之所以引入这个概念主要是应对复杂类型系统中资源管理的问题。引用是对待复杂类型时必不可少的机制,毕竟复杂类型的数据不能被处理器轻易地复制和计算,但引用往往导致极其复杂的资源管理问题
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
这段代码是不会通过 Rust 编译器的,原因是 r 所引用的值已经在使用之前被释放。

上图中的绿色范围 'a 表示 r 的生命周期,蓝色范围 'b 表示 x 的生命周期。很显然,'b 比 'a 小得多,引用必须在值的生命周期以内才有效
在结构体中使用 String 而不用 &str,用一个案例解释原因:
fn longer(s1: &str, s2: &str) -> &str { // 此处的 返回 -> &str 报错 不知道这个引用的生命周期跟哪个入参绑定
if s2.len() > s1.len() {
s2
} else {
s1
}
}
longer 函数取 s1 和 s2 两个字符串切片中较长的一个返回其引用值。但是这段代码不会通过编译,原因是返回值引用可能会返回过期的引用:
fn main() {
let r;
{
let s1 = "rust";
let s2 = "ecmascript";
r = longer(s1, s2);
}
println!("{} is longer", r);
}
这段程序中虽然经过了比较,但 r 被使用的时候源值 s1 和 s2 都已经失效了。当然我们可以把 r 的使用移到 s1 和 s2 的生命周期范围以内防止这种错误的发生,但对于函数来说,它并不能知道自己以外的地方是什么情况,它为了保障自己传递出去的值是正常的,必选所有权原则消除一切危险,所以 longer 函数并不能通过编译。
2.2 生命周期注释
2.2.1 简介
生命周期标注:不改变任何变量的实际存活时间,它只是给编译器提供信息,让编译器能在编译期验证引用是否安全
let r;
{
let x = 5;
r = &x; // r 指向 x
} // x 在这里被销毁
println!("{}", r); // x 已经不存在了,r 是悬垂引用
生命周期注释用单引号开头,跟着一个小写字母单词:
&i32 // 常规引用
&'a i32 // 含有生命周期注释的引用
&'a mut i32 // 可变型含有生命周期注释的引用
单个变量的情况编译器自己能推断,但函数就不行了:
fn longer(s1: &str, s2: &str) -> &str {
if s2.len() > s1.len() { s2 } else { s1 }
}
编译器看到返回值是 &str,但它不知道这个引用跟 s1 还是 s2 的生命周期绑定,无法判断调用方拿到返回值后用多久是安全的,所以直接拒绝。
加了标注之后编译器能做什么呢,可以直接告诉编译器:"返回值的生命周期和 s1、s2 中较短的那个一样长"。
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str { ... }
2.2.2 静态生命周期
生命周期注释有一个特别的:'static 。所有用双引号包括的字符串常量所代表的精确数据类型都是 &'static str
'static 所表示引用活的时间足够长,可以满足任何生命周期要求,不一定真的活到程序结束,只是比调用者活得更久
// 1. 字符串字面量(编译期嵌入二进制)
let s: &'static str = "hello";
// 2. 全局变量
static CONFIG: &str = "config";
// 3. 通过 Box::leak 创建的引用
let boxed = Box::new(String::from("leaked"));
let leaked: &'static str = Box::leak(boxed); // 内存泄漏,但引用是 'static
// 4. 任何拥有所有权的类型都可以是 'static
let owned: String = String::from("owned"); // String: 'static
其他示例:
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where T: Display
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}

浙公网安备 33010602011771号