所有权
根据位置参数表达式 和值上下文 决定 Move 还是 Copy 判断 R W O 权限。
默认情况下 值(栈区)发生浅拷贝,引用类型会发生 Move 移动(内存地址不一定固定)
Rust 是不允许手动管理内存的,业务手动管理内存可能会造成许多存在未定义的行为。例如一个对数据,可能会被多处使用;如果在另外一处地方将其给销毁了,那么就会影响整个行为链,造成未定义行为。
所有权的基本原则:
当一个变量持有资源(RWO)的所有权,在进行值操作时,如果其实现了Copy则直接拷贝,如果没有则触发移动,此时所有权也会被转移到新的变量。因此引用类型(放堆区的)都是没有Copy,只有 Clone 这两者不能同时存在
1、 Box堆内存的释放原则:如果一个变量拥有一个box,当Rust释放该变量的 frame 的时候,Rust也会释放该box的堆内存。(一块内存必须被唯一的变量所拥有 → 排除 Rc智能指针引用计数器)
fn main() {
let a_num = 4;// L1 栈内存 a_num:4
make_and_drop(); // 执行完这里的时候,a_box 是没了,a_num 还在
}
fn make_and_drop() {
let a_box = Box::new(5); // 到这里的时候 栈内存 a_num 和 a_box 指针指向堆内存 5
}
但是如果我们赋值了这个指针
let a = Box::new([0; 1_000_000]);
let b = a;
当 a 被销毁的时候是没问题,但是到了销毁b的时候,业务a的堆内存没了,所以还会再次去销毁,这时候就产生未定义行为了。
因此,就产生了规则:当 let b = a 的时候,堆内存的所有权从a移动到了b,因此只会对b做一次销毁。
2、如果变量 x 将堆数据所有权转移到另一个变量 y,则移动后的 x 不能使用
fn main() {
let first = String::from("Ferris");
let full = add_suffix(first);
// 这里 frist 其实已经失效掉了,因为其实 函数内的 push_str 会扩容导致可能重新分配堆内存
println!("{full}, originally {first}"); // first is now used here
}
fn add_suffix(mut name: String) -> String {
name.push_str(" Jr.");
name
}
可以用 clone 解决这个问题,因为“引用语义”使用指针间接访问数据,如果直接拷贝会导致多个指针指向同一个数据,因此“引用语义”都是没有实现copy的,只有 i32 这种值语义才会被copy 实现拷贝。而 clone 可以实现深拷贝,分配到新的堆内存中
fn main() {
let first = String::from("Ferris");
// 这里frist_cloen 是新的堆内存,不会影响到first 的堆内存
let first_clone = first.clone();
let full = add_suffix(first_clone);
println!("{full}, originally {first}");
}
fn add_suffix(mut name: String) -> String {
name.push_str(" Jr.");
name
}
总结:
- 堆中的所有数据必须仅由一个变量拥有。
- Rust 会在堆数据的所有者超出作用域时释放堆数据。
- 所有权可以通过移动操作转移,移动操作发生在赋值和函数调用中。
- 堆数据只能通过其当前所有者访问,而不能通过以前的所有者访问。
表达式
- 当表达式表示一个实际的值,这就是一个值表达式。
- 当表达式可以表示一个内存位置,这就是一个位置表达式。
位置表达式包括:
- 指向局部变量或静态变量的路径
- 解引用(
expr) - 数组索引(
expr[expr]) - 字段访问(
expr.f) - 括号包裹的位置表达式
其实想要区分值表达式和位置表达式很简单,你看这个表达式能不能放到赋值操作符=的左边。
let mut arr = [10, 20, 30];
let x = arr; // 变量本身是位置表达式
let y = arr[1]; // 数组索引是位置表达式
// 字段访问是位置表达式
struct Point { x: i32, y: i32 }
let mut p = Point { x: 1, y: 2 };
let z = p.x;
// 解引用是位置表达式
let mut v = 42;
let r = &mut v;
*r = 100;
函数返回值的表达式类型取决于函数返回的类型
fn get_num() -> i32 {
42
}
fn get_ref(r: &mut i32) -> &mut i32 {
r
}
fn main() {
get_num() = 100; // error 42 = 100 错误
let mut x = 42;
let r = &mut x;
*get_ref(r) = 100; // success 返回的是一个借用,解引用后是一个地址 *get_ref(r)
}
位置表达式可以提供一个值,也可以提供一个地址
let mut arr = [1, 2, 3];
// 作为值使用
let x = arr[0]; // arr[0] 在这里表示“第0个元素的值”
// 作为位置使用
arr[0] = 42; // arr[0] 在这里表示“第0个元素的存储位置”
上下文
何时作为值使用,何时作为位置使用?
上下文分为 值上下文 和 位置上下文 。
- 值上下文
Value Context:语法环境要求提供一个值,包括:- 赋值语句右边:
let x = arr[0]; - 表达式求值:
foor(arr[0] + 1); - 模式按值绑定:
let y = p.x;
- 赋值语句右边:
- 位置上下文,语法还价要求提供一个位置,包括:
- 赋值语句左边:
arr[0] = 42; - 借用:
let r = &arr[0]; - 可变借用:
let r = &mut arr[0]; - 模式绑定中的
ref或者ref mut:let ref mut x = arr[0];
- 赋值语句左边:
总结:
- 值表达式放在一般放在值上下文中
- 位置表达式放在值上下文中就是提供一个值,位置表达式放到位置上下文中就是提供一个地址
因此,通过这个规则我们就可以判定何时会触发 Copy 还是 Move
当位置表达式出现在值上下文,就会触发 拷贝还是移动 的判断
例如:
let s1 = String::from("hello");
let s2 = s1;
// 此处 s2 = s1 是一个值上下文,而s1作为一个变量,它可以做位置表达式,也就是取地址。位置表达式出现在值上下文中,因此会触发机制,发现 String 是在堆区,没有实现 Copy,因此触发 Move 移动。这就是所有权规则发生变化的完整逻辑。
不合法例子:
let s1 = String::from("hello");
s1; // 一个表达式语句
println!("{}", s1); // error
// s1 这一行语句,是一个求值表达式,因此这是一个值上下文,而 s1 是一个位置表达式,也就是相当于把一个位置表达式放到了一个值上下文,触发判断 String 没有实现 Copy,所以 s1 被移动了,但是没有人接收他,就变成了,求出一个值之后,直接就触发了析构 drop(s1)
聚合所有权
对于数组这种类型,它的所有元素共享所有权,对单一元素的所有权操作会影响到整体。
let mut arr = [String::from("a"), String::from("b")];
// 借用arr[0] 此时 arr 失去 W 和 O 权限
let borrow_one = &arr[0];
arr[1] = String::from("c"); // error
println!("print by borrow: {}", borrow_one);
以上代码中,先对arr[0]进行了一次借用,随后修改arr[1],但是第三行代码会进行报错。
因为Rust的编译器起始无法确定每次操作是不同位置的元素。因此当你借用一个元素,就相当于整个数组都被借用出去了,各个元素之间是相互影响的,它们共享所有权机制。
字段所有权
对于结构体、枚举、元组这些类型,它们的各个字段所有权独立。
struct Person {
name: String,
age: u8,
email: String,
}
let mut p = Person {
name: String::from("zhangsan"),
age: 18,
email: String::from("123456@123.com"),
};
let s = p.name; // 移动
let b = &mut p.email; // 借用
println!("age: {}", p.age);
println!("email: {}", b);
以上代码中,Person有三个字段,并且三个字段的所有权分别独立。对name进行移动后,可以在对email借用,与此同时还能对age进行输出。
RAII
当一个变量生命周期结束时,它会把拥有的所有数据销毁,这种把资源生命周期和变量生命周期绑定在一起的机制叫做 RAII
如果某个变量本身就是值语义,那么在函数调用结束后,数据会随着栈区自动销毁。但如果它是引用语义,就需要考虑将堆区的数据一起销毁,这需要通过Drop这个Trait实现。
Drop 是一个特殊的 Trait,用于在值生命周期结束时自动执行清理逻辑。它是 Rust 实现 RAII 模式的关键。
pub trait Drop {
fn drop(&mut self);
}
例如我们自己实现新类型,并实现析构
struct NewString(String);
impl Drop for NewString {
fn drop(&mut self) {
println!("Dropping string: {}", self.0);
}
}
struct NewBox<T: std::fmt::Display>(Box<T>);
impl<T: std::fmt::Display> Drop for NewBox<T> {
fn drop(&mut self) {
println!("Dropping box: {}", self.0);
}
}
struct Heap {
str: NewString,
ptr: NewBox<i32>,
}
impl Drop for Heap {
fn drop(&mut self) {
println!("Dropping heap:");
}
}
fn main() {
println!("=== main start ===");
{
let h = Heap{
str: NewString(String::from("hello")),
ptr: NewBox(Box::new(5)),
};
} // Drop
println!("=== main end ===");
}
输出:
=== main start ===
Dropping heap:
Dropping string: hello
Dropping box: 5
=== main end ===
在main函数中,当h离开自己的块作用域就会自动调用drop方法,输出Dropping heap: ,然后当调用完自己的 drop 之后,会依次调用所有成员的 drop 方法。
只要变量离开生命周期,就会递归式的触发变量自己以及所有子类型析构函数,保证所有资源正常释放
不允许给同一个类型实现Copy和Drop两个特征,否则编译失败:
如果某个类型实现了Copy,说明它是值类型,所有数据都放在了栈区。那么当栈帧销毁,会自动把这个类型的数据进行销毁,根本不需要Drop。反过来,如果某个类型实现了Drop,往往说明它有一些其它的资源需要管理,如果直接通过Copy按位拷贝,那么就可能导致多个变量指向一个资源的问题,这违背了所有权的基本要求
由于一个数据只能被一个变量拥有,因此谁拥有这个数据,谁就负责在drop中回收这个数据。
fn main() {
let s1 = NewString(String::from("hello"));
println!("block start");
{
let s2 = s1;
}
println!("block end");
}
// block start
// Dropping string: hello
// block end
起初s1拥有字符串的所有权,进入块作用域后,s2转移走了s1的所有权。当s2离开块作用域,生命周期结束,就会调用drop直接把字符串回收。因此先输出Dropping string: hello,后输出block end。
而对于s1,由于它失去了所有权,最后不会调用它的drop方法。
持有所有权的变量,才有资格释放它指向的资源
借用
如果我们的所有权这样来回移动,代码会冗余并且不方便,例如:
fn main() {
let m1 = String::from("Hello");
let m2 = String::from("world");
// 将 m1 和 m2 的所有权移交到内部 g1 和 g2 了
let (m1_again, m2_again) = greet(m1, m2);
let s = format!("{} {}", m1_again, m2_again);
}
fn greet(g1: String, g2: String) -> (String, String) {
println!("{} {}!", g1, g2);
(g1, g2)
}
如果 greet 不返回的话,那么就会跟随这个声明周期导致被销毁掉,外面的继续引用就会造成未定义行为,但是这样写又多余了,来回移动。
因此,如果只是 只读引用,可以使用 & 表示,此时就可以对这个数据做只读引用
fn main() {
let m1 = String::from("Hello");
let m2 = String::from("world");
greet(&m1, &m2); // note the ampersands
let s = format!("{} {}", m1, m2);
}
fn greet(g1: &String, g2: &String) { // note the ampersands
println!("{} {}!", g1, g2);
}
在处于 greet 之后, m1和m2并没有被销毁,只有在最后一次使用后才会被销毁掉。在函数内部,g1 和g2实际上指向的是 m1 和 m2,然后由 m1 和 m2 指向堆内存数据。
引用是没有所有权的指针,所以他们不能直接指向堆内存里的数据。
如果想要直接访问到堆内存中的数据,那么就需要进行解引用指针进行数据访问,很多操作都会造成直接的指针解引用。
- 1、指向局部变量或静态变量的路径
- 2、解引用
*expr - 数组索引
expr[expr] - 字段访问
expr.r - 括号包裹的位置表达式
let mut arr = [10, 20, 30];
let x = arr; // 变量本身是位置表达式
let y = arr[1]; // 数组索引是位置表达式
// 字段访问是位置表达式
struct Point { x: i32, y: i32 }
let mut p = Point { x: 1, y: 2 };
let z = p.x;
// 解引用是位置表达式
let mut v = 42;
let r = &mut v;
*r = 100;
区分位置表达式和值表达式,只需要区分表达式能不能放到赋值操作符 = 的左边即可,例如
arr[1] = 100; // 数组元素
p.x = 100; // 结构体字段
*e = 100; // 解引用
x = 100; // 变量赋值
隐式和显示的解引用
let x: Box<i32> = Box::new(-1);
let x_abs1 = i32::abs(*x); // 显式
let x_abs2 = x.abs(); // 隐式
assert_eq!(x_abs1, x_abs2);
let r: &Box<i32> = &x;
let r_abs1 = i32::abs(**r); // 显式
let r_abs2 = r.abs(); // 隐式
assert_eq!(r_abs1, r_abs2);
let s = String::from("Hello");
let s_len1 = str::len(&s); // 显式
let s_len2 = s.len(); // 隐式
assert_eq!(s_len1, s_len2);
- 一个变量允许存在多个不可变借用,或一个可变借用
- 如果存在不可变借用,所有者暂时失去写权限(W),只能读取(R)
- 如果存在可变借用,所有者暂时失去读写权限 (RW)
- 只要存在任意借用,所有者暂时失去释放与移动权限(O)
NLL
Rust 2018引入了智能的借用检查Non-Lexical Lifetimes (NLL) 优化,能够分析借用的实际使用范围:
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2); // r1 和 r2 在这里不再被使用,生命周期结束
let r3 = &mut s; // 现在可以创建可变引用
println!("{}", r3); // r3 生命周期结束
编译器能够分析出r1和r2在println!之后不再使用,所以允许后续创建可变引用。这比简单的词法作用域更智能,提高了代码的灵活性。
重新借用
基于NLL,Rust可以更灵活地判断借用的生命周期,于是衍生出了重新借用的语法。
对一个已经存在的借用,可以再次进行借用
- 原借用为可变借用,此时原借用暂时失效,直到新借用生命周期结束
- 原借用为不可变借用,原借用仍然生效
fn main() {
let mut s = String::from("hello");
// 首先b1对s进行了一次可变借用
let b1 = &mut s; // b1 start
// 随后let b2 = &*b1;就是重新借用
let b2 = &*b1; // b2 start
// b2基于b1进行重新借用,在b2生命周期期间,b1无法使用,当b2结束才把所有权归还b1
print!("{}", b2); // b2 end
b1.push_str(", world!"); // b1 end
}
分析一下这个语法&*b1,b1本身是一个借用,*b1拿到的是b1指向的值,这是一个位置表达式。随后对其进行借用&*b1,相当于对b1指向的值进行借用
fn main() {
let mut s = String::from("hello");
let b1 = &mut s; // 可变借用: b1
let b2 = &*b1; // 不可变借用: b2
let b3 = &*b1; // 不可变借用: b2 b3
print!("{}", b3); // 不可变借用: b2 b3
print!("{}", b2); // 不可变借用: b2
b1.push_str(", world!"); // 可变借用: b1
}
一开始,只有b1这个可变借用,并且没有其他借用,合法。
创建b2后,此时b1暂时不能使用了,当做b1不存在,只有b2不可变借用独占,合法。
创建b3后,此时相当于有两个不可变借用b2和b3,允许存在多个不可变借用,合法。
当b2和b3依次离开生命周期,此时b1所有权恢复,只有b1可变借用独占,合法。
可以看出来,将代码拆成一行一行看,把已经被重新借用的原借用暂时忽略,任何时刻都是符合借用规则的。
fn main() {
let mut s = String::from("hello");
let b1 = &mut s; // 可变借用: b1
let b2 = &mut *b1; // 可变借用: b2
print!("{}", b2); // 可变借用: b2
b1.push_str(", world!"); // 可变借用: b1
}
一开始,只有b1这个可变借用,并且没有其他借用,合法。
创建b2后,此时b1暂时不能使用了,当做b1不存在,只有b2可变借用独占,合法。
当b2离开生命周期,此时b1所有权恢复,只有b1可变借用独占,合法。
可以看出,重新借用是对借用规则的锦上添花,而不是对借用规则的破坏性改写。基于重新借用,你可以将一个可变借用的权限临时缩小为不可变借用,在传参时也可以创建一个生命周期更短的临时引用。
生命周期
让 Rust 编译器 确定引用的值的存活时间
use std::collections::HashMap;
fn main() {
let query = String::from("name=Venus&age=24&hobby=coding");
let qp = QueryParser::from_string(query.clone());
println!("params: {:#?}", qp);
let qp2 = EnhancedQueryParser::from_string(&query);
println!("params: {:#?}", qp2);
let name = qp2.params.get("name").unwrap();
println!("name: {}", name);
}
#[derive(Debug)]
struct QueryParser {
query: String,
params: HashMap<String, String>
}
impl QueryParser {
fn from_string(query: String) -> Self {
// name=value&name=value
let params: HashMap<String, String> = query.split("&")
.map(|item| {
let mut parts = item.split("=");
// 使用clone 每次循环发生两次内存创建,性能不好!
let key = String::from(parts.next().expect("no key"));
let value = String::from(parts.next().unwrap());
(key, value)
})
.collect();
Self {
query,
params,
}
}
}
#[derive(Debug)]
struct EnhancedQueryParser<'a> {
query: &'a str,
params: HashMap<&'a str, &'a str>
}
// 'a 就是代表,析构的时间肯定不晚于 &query的 生命周期
impl<'a> EnhancedQueryParser<'a> {
fn from_string(query: &'a str) -> Self {
let params: HashMap<&'a str, &'a str> = query.split("&")
.map(|kv| {
let mut parts = kv.split("=");
(
parts.next().unwrap(),
parts.next().unwrap()
)
})
.collect();
Self {
query,
params
}
}
}
例如上面的这段代码,