rust所有权、借用、生命周期
1. 所有权
所有权是 Rust 最独特的特性,它让 Rust 无需垃圾回收即可保证内存安全。
所有权规则
- Rust 中的每一个值都有一个被称为其所有者的变量。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
变量与数据交互的方式
移动
fn main() {
let s1 = String::from("hello"); // s1 拥有字符串 "hello"
let s2 = s1; // s1 的所有权被移动(move)到 s2
// println!("{}, world!", s1); // 这行会编译错误!s1 不再有效
println!("{}, world!", s2); // 正确,s2 现在拥有数据
}
关键理解:
- 对于在堆上分配的数据(如
String、Vec),赋值操作不是复制数据,而是移动所有权。 - 移动后,原来的变量不再有效,防止了"双重释放"错误。
- 这确保了在任何时刻,只有一个所有者负责释放内存。
克隆
如果你确实需要深度复制堆数据,可以使用 clone 方法:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 真正的数据复制
println!("s1 = {}, s2 = {}", s1, s2); // 两者都有效
}
栈上数据的复制
对于完全存储在栈上的类型(实现了 Copy trait),赋值操作是复制而不是移动:
fn main() {
let x = 5;
let y = x; // 复制,因为 i32 实现了 Copy trait
println!("x = {}, y = {}", x, y); // 两者都有效
}
实现了 Copy 的类型包括:所有整数类型、布尔类型、浮点类型、字符类型、以及包含这些类型的元组。
所有权与函数
将值传递给函数在语义上类似于赋值:可能会移动或复制。
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // 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 移出作用域。不会有特殊操作
返回值与作用域
返回值也可以转移所有权:
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("yours"); // some_string 进入作用域
some_string // 返回 some_string 并移出给调用的函数
}
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
a_string // 返回 a_string 并移出给调用的函数
}
2. 引用与借用
所有权系统虽然安全,但使用起来可能不方便。引用允许你使用值但不获取其所有权。
引用
引用像一个指针,它是一个地址,我们可以由此访问存储于该地址的数据。
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 传递引用,不获取所有权
println!("The length of '{}' is {}.", s1, len); // s1 仍然有效!
}
fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,所以什么也不会发生
关键点:
- 使用
&符号创建引用 - 引用默认是不可变的
- 获取引用作为函数参数称为借用
可变引用
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");
}
引用规则
- 在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用。
- 引用必须总是有效的。
数据竞争预防
第一条规则防止了数据竞争。数据竞争可由以下行为造成:
- 两个或更多指针同时访问同一数据
- 至少有一个指针被用来写入数据
- 没有同步数据访问的机制
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // 错误!不能在同一作用域中拥有多个可变引用
println!("{}", r1);
}
但可以这样使用:
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s;
// 使用 r1
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
let r2 = &mut s; // 正确
}
也不能同时拥有可变和不可变引用:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
// let r3 = &mut s; // 大问题!不能在有不可变引用的同时拥有可变引用
println!("{} and {}", r1, r2);
}
注意:一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。所以下面的代码是合法的:
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);
}
悬垂引用
Rust 编译器确保引用永远不会变成悬垂状态(指向无效内存):
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle 返回一个字符串的引用
let s = String::from("hello"); // s 是一个新字符串
&s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。危险!
正确的做法是直接返回 String:
fn no_dangle() -> String {
let s = String::from("hello");
s // 所有权被移动出去
}
3. 生命周期
生命周期是 Rust 中一个确保引用有效的范围。生命周期的主要目标是避免悬垂引用。
生命周期注解语法
生命周期注解描述了多个引用生命周期相互的关系。
&i32 // 一个引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用
函数签名中的生命周期注解
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
解读:
<'a>声明了一个生命周期参数x: &'a str表示参数x至少拥有生命周期'ay: &'a str表示参数y也至少拥有生命周期'a-> &'a str表示返回值也至少拥有生命周期'a
这意味着:返回值的生命周期与参数生命周期中较短的那个保持一致。
结构体定义中的生命周期注解
如果结构体包含引用,必须在结构体定义中为每个引用添加生命周期注解:
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
这个注解意味着 ImportantExcerpt 的实例不能比其 part 字段中的引用活得更久。
生命周期省略规则
在早期版本的 Rust 中,所有引用都需要显式生命周期注解。但后来发现某些模式很常见,于是引入了生命周期省略规则:
-
每个引用参数都有自己的生命周期参数
fn foo<'a>(x: &'a i32)fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
-
如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数
fn foo<'a>(x: &'a i32) -> &'a i32
-
如果方法有多个输入生命周期参数并且其中一个参数是
&self或&mut self,那么所有输出生命周期参数被赋予self的生命周期
方法定义中的生命周期注解
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
根据第三条省略规则,announce_and_return_part 方法返回值的生命周期与 &self 的生命周期相同,所以不需要显式注解。
静态生命周期
'static 是一个特殊的生命周期,它表示整个程序的持续时间:
let s: &'static str = "I have a static lifetime.";
字符串字面值都有 'static 生命周期。
结合泛型、trait bounds 和生命周期
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
}
}
贵公网安备52052402000220号