写作归档
所有权 & 借用 & 生命周期 · 主图
所有权 & 借用 & 生命周期

所有权 & 借用 & 生命周期

类型
文章
日期
2026/09/29
阅读
16 分钟
字数
4,434

所有权

根据位置参数表达式 和值上下文 决定 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 会在堆数据的所有者超出作用域时释放堆数据。
  • 所有权可以通过移动操作转移,移动操作发生在赋值和函数调用中。
  • 堆数据只能通过其当前所有者访问,而不能通过以前的所有者访问。

表达式

  • 当表达式表示一个实际的值,这就是一个值表达式。
  • 当表达式可以表示一个内存位置,这就是一个位置表达式。

位置表达式包括:

  1. 指向局部变量或静态变量的路径
  2. 解引用(expr)
  3. 数组索引(expr[expr])
  4. 字段访问(expr.f)
  5. 括号包裹的位置表达式

其实想要区分值表达式和位置表达式很简单,你看这个表达式能不能放到赋值操作符=的左边。

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);

  1. 一个变量允许存在多个不可变借用,或一个可变借用
  2. 如果存在不可变借用,所有者暂时失去写权限(W),只能读取(R)
  3. 如果存在可变借用,所有者暂时失去读写权限 (RW)
  4. 只要存在任意借用,所有者暂时失去释放与移动权限(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
        }
    }
}

例如上面的这段代码,

0 条

还没有评论。

发表评论