Rust所有权系统-Rust核心特性

所有权(Ownership)是 Rust 最独特且强大的特性,它使 Rust 能够在没有垃圾回收(GC)的情况下保证内存安全。理解所有权是掌握 Rust 的关键。

一、所有权三原则

Rust 的所有权系统基于三个核心规则:

  1. 每个值都有一个所有者(变量)

  2. 同一时间只能有一个所有者

  3. 当所有者离开作用域,值将被丢弃(内存被释放)

二、变量作用域与内存管理

{
    let s = "hello"; // s 进入作用域
    // 可以使用 s
} // s 离开作用域,相关资源被释放

对于堆分配的数据(堆变量):

{
    let s = String::from("hello"); // 在堆上分配内存
    // 使用 s
} // 自动调用 drop 方法释放堆内存

尽量不要对栈变量使用drop(),否则将出现如下警告信息。

warning: calls to `std::mem::drop` with a value that implements `Copy` does nothing

三、变量与数据交互的方式

3.1 复制(Copy)

对于栈数据,也就是基本类型(如整数、布尔值、字符等),赋值操作会复制数据,原变量仍然可用。这些类型都实现了 Copy trait。

fn main() {
    let x = 5;
    let y = x; // 复制 x 的值给 y,x 仍然可用
    
    println!("x = {}, y = {}", x, y); // 正确,输出 "x = 5, y = 5"
}

常见的 Copy 类型(栈变量)

  • 所有整数类型(i32、u64 等)
  • 布尔类型(bool)
  • 字符类型(char)
  • 浮点类型(f32、f64)
  • 元组(仅当所有元素都是 Copy 类型时)

3.2 移动(Move)

对于堆数据,也就是非 Copy 类型采用所有权转移的方式。

对于复杂类型(如 StringVec 等),赋值操作会转移所有权(称为 "移动"),原变量将不再可用。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 的所有权转移给 s2(移动)
    
    // 错误!s1 已失去所有权,不能再用。编译器将抛出 error[E0382]: borrow of moved value: `s1`
    // println!("{}", s1); 
    
    println!("{}", s2); // 正确,s2 是当前所有者
}

为什么这样设计?
防止双重释放(double free)错误。如果 s1 和 s2 都拥有数据所有权,离开作用域时会尝试释放同一块内存,导致内存不安全。

3.3 克隆(Clone)

如果希望对非 Copy 类型进行深拷贝(不仅复制栈上的指针,还复制堆上的数据),可以使用 clone 方法。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // 手动克隆,复制堆上的数据
    
    println!("s1 = {}, s2 = {}", s1, s2); // 正确,两者都可用
}

四、函数与所有权

函数参数传递和返回值也可以发生所有权转移。

4.1 函数参数的所有权转移

fn main() {
    let s = String::from("hello");
    take_ownership(s); // s 的所有权转移到函数内部
    
    // 错误!s 已失去所有权,编译错误:error[E0382]: borrow of moved value: `s`
    // println!("{}", s);
}

fn take_ownership(some_string: String) {
    println!("{}", some_string);
} // some_string 离开作用域,内存被释放,所有权消亡

4.2 函数返回值的所有权转移

fn main() {
    let s1 = gives_ownership(); // 函数返回值的所有权转移给 s1
    let s2 = String::from("hello");
    let s3 = takes_and_gives_back(s2); // s2 所有权转移到函数,再转移给 s3
}

fn gives_ownership() -> String {
    let some_string = String::from("yours");
    some_string // 返回值所有权转移给调用者
}

fn takes_and_gives_back(a_string: String) -> String {
    a_string // 返回值所有权转移给调用者
}

五、借用(Borrowing)

如果我们既想使用值,又不想转移所有权,可以使用引用(reference),这一行为称为 "借用"(borrowing)。引用的符号是 &

5.1 不可变借用

可以创建多个不可变引用,但不能通过不可变引用修改数据。

fn main() {
    let s = String::from("hello");
    let r1 = &s; // 不可变引用
    let r2 = &s; // 可以创建多个不可变引用
    
    println!("{} and {}", r1, r2); // 正确
    
    // 错误!不能通过不可变引用修改数据
    // r1.push_str(", world");
}

使用函数时的不可变借用:

fn main() {
    let s = String::from("hello");
    let len = calculate_length(&s); // 传递引用,不转移所有权
    
    println!("The length of '{}' is {}.", s, len); // s 仍然可用
}

// 参数是字符串的引用,不获取所有权
fn calculate_length(s: &String) -> usize {
    s.len()
} // s 离开作用域,但不释放内存(因为它只是引用)

5.2 可变借用

可变引用允许修改数据,但有严格限制:

  1. 同一时间只能有一个可变引用
  2. 可变引用和不可变引用不能同时存在
fn main() {
    //引用的源所有者必须为'可变'
    let mut s = String::from("hello");
    
    let r1 = &mut s; // 可变引用
    // 错误!同一时间只能有一个可变引用,编译器抛出error[E0499]: cannot borrow `s` as mutable more than once at a time
    // let r2 = &mut s;
    
    r1.push_str(", world"); // 正确,通过可变引用修改数据
    println!("{}", r1);
}

可变引用与不可变引用的冲突:

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s; // 不可变引用
    let r2 = &s; // 另一个不可变引用
    
    // 错误!不能同时存在可变引用和不可变引用,编译器抛出error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
    // let r3 = &mut s;
    
    println!("{} and {}", r1, r2); // 正确,使用不可变引用
}

引用作用域:引用的作用域从声明处开始,到最后一次使用处结束。这允许在不同作用域中交替使用可变和不可变引用:

fn main() {
    let mut s = String::from("hello");
    
    {
        let r1 = &mut s;
        r1.push_str(", world");
    } // r1 离开作用域,可变引用失效
    
    let r2 = &s; // 现在可以创建不可变引用
    println!("{}", r2);
}

5.3 悬垂引用(Dangling References)

悬垂引用是指引用指向的内存已被释放的情况。Rust 编译时会阻止这种情况:

// 错误示例:尝试返回悬垂引用
fn dangle() -> &String {
    let s = String::from("hello");
    &s // s 会在函数结束时被释放,返回的引用将指向无效内存
}

解决方法:返回值本身而不是引用(转移所有权):

fn no_dangle() -> String {
    let s = String::from("hello");
    s // 返回所有权,调用者将成为新所有者
}

六、生命周期(Lifetimes)

生命周期用于描述引用的有效范围,确保引用不会比它指向的数据存活更久。大多数情况下,Rust 可以自动推断生命周期,但在复杂场景下需要手动标注。

6.1 生命周期标注语法

生命周期参数以 ' 开头,通常使用小写字母(如 'a'b)表示:

fn main() {
    let s1 = String::from("abc");
    let s2 = String::from("abcde");
    let res = longest(&s1, &s2);
    println!("res={}",res);
}

// 标注参数和返回值的生命周期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

上述代码中,'a 表示 x、y 和返回值必须拥有相同的生命周期,返回值的生命周期不能超过任何一个参数的生命周期。确保在读取函数返回值之时,不要出现函数返回值所引用的参数变量被释放的情况。

6.2 结构体中的生命周期

struct ImportantExcerpt<'a> {
    part: &'a str, // part 的生命周期与结构体实例相同
}

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,
    };
}

6.3 生命周期省略规则

Rust 有一套生命周期省略规则,以下这些情况下无需手动标注。

1. 输入生命周期规则

每个引用参数都获得一个独立的生命周期参数。当函数有多个引用类型的参数时,编译器会为每个参数自动分配不同的生命周期参数。

示例代码(省略标注):

// 函数有两个引用参数,未标注生命周期
fn print_both(a: &str, b: &str) {
    println!("{} and {}", a, b);
}

编译器推断后的完整形式(显式标注):

// 编译器为每个引用参数分配独立的生命周期 'a 和 'b
fn print_both<'a, 'b>(a: &'a str, b: &'b str) {
    println!("{} and {}", a, b);
}

规则应用说明:函数有两个引用参数 a 和 b,编译器自动为它们分配了不同的生命周期 'a 和 'b,彼此独立,互不影响。

2. 输出生命周期规则

如果只有一个输入生命周期参数,它会被赋予所有输出生命周期参数。当函数只有一个引用类型的输入参数时,该参数的生命周期会被自动应用到所有引用类型的输出参数上。

示例代码(省略标注):

// 只有一个引用参数,返回值是引用类型,未标注生命周期
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

编译器推断后的完整形式(显式标注):

// 输入参数的生命周期 'a 被赋予输出参数
fn first_word<'a>(s: &'a str) -> &'a str {
    s.split_whitespace().next().unwrap_or("")
}

规则应用说明:函数只有一个引用参数 s(生命周期 'a),因此返回值的引用类型自动使用 'a 作为生命周期,确保返回的引用不会超过 s 的存活时间。

3. 方法规则

如果是方法,&self 或 &mut self 的生命周期会被赋予所有输出生命周期参数。在结构体 / 枚举的方法中(impl 块内),&self 或 &mut self 的生命周期会被自动应用到所有引用类型的返回值上。

示例代码(省略标注):

struct Text<'a> {
    content: &'a str, // 结构体持有一个字符串切片
}

impl<'a> Text<'a> {
    // 方法接收 &self,返回引用类型,未标注生命周期
    fn get_first_part(&self) -> &str {
        self.content.split('.').next().unwrap_or("")
    }
}

编译器推断后的完整形式(显式标注):

struct Text<'a> {
    content: &'a str,
}

impl<'a> Text<'a> {
    // &self 的生命周期 'a 被赋予输出参数
    fn get_first_part(&'a self) -> &'a str {
        self.content.split('.').next().unwrap_or("")
    }
}

规则应用说明:方法 get_first_part 接收 &self(其生命周期与结构体的 'a 一致),因此返回值的引用类型自动使用 'a 作为生命周期,确保返回的引用不会超过结构体实例的存活时间。

生命周期省略规则的核心是编译器自动推断生命周期,减少手动标注的冗余。上述规则覆盖了绝大多数常见场景,只有在编译器无法确定生命周期关系时(如多个输入参数且输出引用可能来自任意一个),才需要手动标注生命周期。


七、切片(Slices)

切片是一种特殊的引用,用于访问集合中连续的一部分元素,而不获取所有权。切片也是理解所有权和借用的绝佳例子。

7.1 字符串切片

字符串切片的类型是 &str,表示字符串的一部分:
 

fn main() {
    let s = String::from("hello world");
    
    let hello = &s[0..5]; // 从索引 0 到 4(不包含 5)
    let world = &s[6..11]; // 从索引 6 到 10
    
    println!("{} {}", hello, world); // 输出 "hello world"
}

简化写法:

  • &s[..n] 等价于 &s[0..n]
  • &s[n..] 等价于 &s[n..s.len()]
  • &s[..] 等价于 &s[0..s.len()]

7.2 其他切片

除了字符串,其他集合类型(如数组)也可以创建切片:

fn main() {
    let a = [1, 2, 3, 4, 5];
    let slice = &a[1..3]; // 数组切片,类型是 &[i32]
    
    assert_eq!(slice, &[2, 3]); // 正确
}

八、总结

Rust 的所有权系统通过以下机制保证内存安全:

  • 所有权规则确保每个值有唯一的所有者,离开作用域自动释放
  • 借用(引用)允许临时使用值而不转移所有权
  • 可变 / 不可变借用规则防止数据竞争
  • 生命周期确保引用始终有效

虽然所有权系统带来一定的学习和开发成本,但它带来的内存安全和性能优势是 Rust 独特的价值所在。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值