rust所有权、借用、生命周期


1. 所有权


所有权是 Rust 最独特的特性,它让 Rust 无需垃圾回收即可保证内存安全。

所有权规则

  1. Rust 中的每一个值都有一个被称为其所有者的变量
  2. 值在任一时刻有且只有一个所有者
  3. 当所有者(变量)离开作用域,这个值将被丢弃

变量与数据交互的方式

移动

fn main() {
    let s1 = String::from("hello"); // s1 拥有字符串 "hello"
    let s2 = s1; // s1 的所有权被移动(move)到 s2
    
    // println!("{}, world!", s1); // 这行会编译错误!s1 不再有效
    println!("{}, world!", s2); // 正确,s2 现在拥有数据
}

关键理解

  • 对于在堆上分配的数据(如 StringVec),赋值操作不是复制数据,而是移动所有权。
  • 移动后,原来的变量不再有效,防止了"双重释放"错误。
  • 这确保了在任何时刻,只有一个所有者负责释放内存。

克隆

如果你确实需要深度复制堆数据,可以使用 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");
}

引用规则

  1. 在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用
  2. 引用必须总是有效的

数据竞争预防

第一条规则防止了数据竞争。数据竞争可由以下行为造成:

  • 两个或更多指针同时访问同一数据
  • 至少有一个指针被用来写入数据
  • 没有同步数据访问的机制
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 至少拥有生命周期 'a
  • y: &'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 中,所有引用都需要显式生命周期注解。但后来发现某些模式很常见,于是引入了生命周期省略规则

  1. 每个引用参数都有自己的生命周期参数

    • fn foo<'a>(x: &'a i32)
    • fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
  2. 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数

    • fn foo<'a>(x: &'a i32) -> &'a i32
  3. 如果方法有多个输入生命周期参数并且其中一个参数是 &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
    }
}

Read more

Rust 结构体、枚举、模式匹配、Option 和 Result、包和模块

1. 结构体(Structs)深度解析 什么是结构体? 结构体是一种自定义数据类型,允许你将多个相关的值组合在一起,形成一个有意义的组合。 定义和使用结构体 // 1. 基本结构体定义 struct User { username: String, // 字段:用户名 email: String, // 字段:邮箱 sign_in_count: u64, // 字段:登录次数 active: bool, // 字段:是否活跃 } // 2. 创建结构体实例 fn create_user() { // 不可变实例 let user1 = User { email: String::from("alice@example.com"), username: String:

By amm

Rust 生命周期

1. 什么是生命周期? 生命周期是引用有效的作用域范围。在 Rust 中,每个引用都有一个生命周期,这是它保持有效的作用域。 fn main() { // 生命周期开始 let x = 5; // x 的生命周期开始 let r = &x; // r 的生命周期开始,引用 x println!("r = {}", r); // 生命周期结束 } // x 和 r 的生命周期结束 2. 为什么需要生命周期? 问题:悬垂引用(Dangling References) // 这个代码无法编译! fn main() { let reference; { let value = 42; // value 在内部作用域创建

By amm

© 2025 路不易All rights reserved.

黔ICP备2025043243号-1 | 公安备案图标 贵公网安备52052402000220号