Rust中所有权和作用域及生命周期
在 Rust 中,作用域(Scope) 和所有权(Ownership) 是保证内存安全的核心机制,它们共同决定了变量的生命周期和内存管理方式,无需垃圾回收即可避免悬垂指针、双重释放等内存问题。
概念 | 含义 | 是否自动触发销毁 | 示例 |
---|---|---|---|
作用域 (Scope) | 变量的可访问范围 | ✅ 是 | { let x = 1; } |
所有权 (Ownership) | 资源的唯一持有者 | ✅ 是 | let s = String::from("hi"); |
生命周期 (Lifetime) | 引用的有效区间 | N/A | &'a str |
作用域
作用域是 一个变量(或引用、资源)在程序中 有效、可访问 的代码范围 ,由一对花括号 {}
界定:
- 确定性:作用域由代码结构显式界定,编译期即可确定,不存在运行时动态变化。
- 自动清理:变量离开作用域时,Rust 会自动调用其
drop
方法(若实现drop trait,则会调用drop
函数),释放内存或资源(如文件句柄、网络连接等)。 - 嵌套性:作用域可以嵌套(内部作用域包含在外部作用域中),内部作用域可访问外部作用域的实体,但外部作用域无法访问内部作用域的实体。
事件 | 发生时机 | 结果 |
---|---|---|
变量绑定 | let 语句执行时 | 内存分配(如需要) |
变量可见 | 声明点到作用域结束 | 可在作用域内访问 |
变量销毁 | 离开作用域时 | 调用 drop 释放资源 |
变量销毁顺序:
- 后进先出(LIFO):与声明顺序相反
- 结构体字段:按声明顺序销毁
- 数组元素:从第一个到最后一个
所有权
Rust不使用垃圾回收(GC),而是通过所有权系统在编译期保证内存安全 (在编译时会进行一系列的规则检查,在运行时零开销):
- 每个值(资源)在任意时刻只有一个所有者(owner);
- 当所有者离开作用域时,值会被自动销毁;
- 赋值或传参会移动(move)所有权,除非实现了copy trait。
引用与借用
为避免 频繁的 所有权转移,Rust 提供引用机制:
- 不可变引用(&T):只读访问,允许多个同时存在;
- 可变引用(&mut T):可修改数据,同一作用域内仅允许一个。
借用规则:
- 引用作用域不能超过被引用变量的作用域(防悬垂引用);
- 同一数据不能同时存在可变和不可变引用。
赋值
赋值会引起两种语义:
- 复制(Copy):
a
仍然可以使用;- 隐式浅拷贝:若类型实现
Copy
trait(如整数、浮点、布尔、字符、元组中全是 Copy 类型) ,则赋值时为Copy语义,不会转移所有权; - Clone(显式深拷贝):会同时复制堆数据;
- 隐式浅拷贝:若类型实现
- 所有权转移(Move):
a
不再有效。
Copy类型
类型类别 | 示例 | 特点 |
---|---|---|
标量类型(scalar) | i32 , u64 , f32 , bool , char | 小且固定大小,在栈上 |
全是 Copy 类型的元组 | (i32, i32) , (u8, f64, bool) | 只要里面的每个字段都是 Copy |
指针类(少见) | *const T , *mut T 原生指针) | 不涉及所有权 |
函数指针 | fn(i32) -> i32 | 也是 Copy |
非Copy类型
类型类别 | 示例 | 说明 |
---|---|---|
堆分配类型 | String , Vec<T> , Box<T> | 包含堆内存指针 |
动态大小类型 | &str , [T] , Rc<T> | 内部有引用计数或指针 |
用户定义结构体 | 若字段中含非 Copy 成员 | 整体就不是 Copy |
其他智能指针 | Rc<T> , Arc<T> , RefCell<T> | 有运行时管理逻辑 |
生命周期
生命周期(Lifetime)是编译器用来确保引用始终有效的核心机制。它们解决了"悬垂引用"问题——确保引用不会指向已释放的内存。生命周期是引用有效的作用域范围,由编译器在编译时进行静态分析,不产生运行时开销:** **
- 引用在使用时是合法的;
- 不会悬空(dangling reference);
- 不会同时存在冲突的可变/不可变引用。
生命周期参数用 'a
, 'b
等表示(撇号 '
声明生命周期参数),用法类似于泛型参数 ( 在大多数情况下,Rust 会自动推断生命周期):
'a
并不改变运行逻辑;- 只是告诉编译器:“这些引用之间的关系”。
场景 | 必须明确标注的情况 | 可以省略的情况 |
---|---|---|
结构体 | 包含引用类型的字段(无论多少个) | 不包含引用字段(无需生命周期参数) |
普通函数 | 1. 多个引用参数且返回引用 | 1. 无返回引用 2. 单个引用参数且返回引用 |
方法 | 1. 返回引用来自外部参数(非 self) 2. 多个外部引用参数且返回引用 | 1. 返回引用来自 self 2. 无返回引用 |
Trait / 枚举 | 包含引用类型的关联项或方法参数 / 返回值 | 不包含引用类型的关联项或方法 |
结构体
当结构体包含引用时,必须为这些引用标注生命周期,以确保结构体的生命周期不超过它所包含的引用的生命周期。
若结构体的多个引用来自不同生命周期,可用多个参数区分
// 两个字段的生命周期不同,分别用'a和'b标注
struct Article<'a, 'b> {content: &'a str, // 内容的生命周期为'asource: &'b str, // 来源的生命周期为'b
}fn main() {let content = String::from("Rust lifecycle intro");let source = "Wikipedia"; // 字符串字面量,生命周期为'staticlet art = Article {content: &content,source: &source,};// art的生命周期受限于'a(content的生命周期)和'b(source的生命周期)中较短的一个
}
函数
函数的参数或返回值如果是引用类型,可能需要标注生命周期。
省略规则:编译器有3条生命周期推断规则,允许在常见场景中省略显式注解:
- 每个引用参数获取自己的生命周期参数
fn foo(x: &i32) // 隐式:fn foo<'a>(x: &'a i32)
fn foo(x: &i32, y: &i32) // 隐式:fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
- 若只有一个输入生命周期参数,它被赋予所有输出生命周期参数;
fn bar(x: &str) -> &str // 隐式:fn bar<'a>(x: &'a str) -> &'a str
- 若有多个输入生命周期参数,但其中一个是
&self
或&mut self
,则self的生命周期将赋予所有输出周期参数
impl<'a> ImportantExcerpt<'a> {fn announce(&self, announcement: &str) -> &str {// 隐式:fn announce<'b>(&'a self, announcement: &'b str) -> &'a strprintln!("注意:{}", announcement);self.part}
}
必须明确标注的情况:当函数的返回引用可能来自多个输入引用,或编译器无法推断返回引用的来源时,必须手动标注生命周期:
- 多个输入引用参数,且返回引用
// 正确示例:标注生命周期'a,说明x、y和返回值共享同一生命周期
// 注解'a表示:返回的引用必须来自x或y,且其生命周期不超过x和y中生命周期较短的那个。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {if x.len() > y.len() { x } else { y }
}
静态生命周期
'static
是一个特殊的生命周期,表示引用可以存活到整个程序运行期间。常见:
- 字符串字面量:存储在程序的只读内存中,生命周期为
'static
。 - 显式声明为
'static
的引用(需确保引用的值确实能存活到程序结束,否则会报错)。