跳转至

所有权与借用

所有权与借用

📚 章节概述

所有权(Ownership)是 Rust 最独特的特性,它让 Rust 无需垃圾回收器就能保证内存安全。本章将深入讲解所有权系统、借用规则、引用和生命周期。

🎯 学习目标

  • 理解所有权的三大规则
  • 掌握借用和引用的概念
  • 学会使用生命周期标注
  • 理解内存管理机制
  • 避免常见的所有权错误

📖 所有权系统

1.1 什么是所有权

所有权是 Rust 管理内存的核心机制。它通过一套编译时规则来管理内存,无需运行时垃圾回收器。

所有权规则: 1. Rust 中的每个值都有一个所有者(owner) 2. 值在同一时间只能有一个所有者 3. 当所有者离开作用域时,值将被丢弃

1.2 栈和堆

理解栈和堆的区别对于掌握所有权至关重要:

Rust
fn main() {
    // 栈上的数据(固定大小,快速)
    let x = 5;
    let y = x; // 复制值

    // 堆上的数据(动态大小,灵活)
    let s1 = String::from("hello");
    let s2 = s1; // 移动所有权

    // println!("{}", s1); // 编译错误!s1 已被移动
    println!("{}", s2);
}

栈 vs 堆:

特性
分配速度
访问速度
数据大小 固定 动态
用途 基本类型、局部变量 复杂数据结构

1.3 所有权转移

移动(Move)

Rust
fn main() {
    let s1 = String::from("hello");  // String::from 创建堆上的字符串
    let s2 = s1; // s1 的所有权移动到 s2

    // println!("{}", s1); // 编译错误!s1 不再有效
    println!("{}", s2);
}

克隆(Clone)

如果需要深度复制,使用 clone 方法:

Rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // 深度复制

    println!("s1 = {}, s2 = {}", s1, s2);
}

拷贝(Copy)

实现了 Copy trait 的类型在赋值时会自动拷贝:

Rust
fn main() {
    let x = 5;
    let y = x; // 拷贝,x 仍然有效

    println!("x = {}, y = {}", x, y);

    let s1 = String::from("hello");
    let s2 = s1; // 移动,s1 不再有效
    // println!("{}", s1); // 编译错误
}

实现 Copy 的类型: - 所有整数类型 - 布尔类型 - 浮点类型 - 字符类型 - 元组(如果所有元素都实现了 Copy)

🔗 函数与所有权

2.1 传递值给函数

Rust
fn main() {
    let s = String::from("hello");  // s 进入作用域
    takes_ownership(s);             // s 的所有权移动到函数
    // println!("{}", s);           // 编译错误!s 不再有效

    let x = 5;                      // x 进入作用域
    makes_copy(x);                  // x 拷贝到函数
    println!("{}", x);              // x 仍然有效
}

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
} // some_string 离开作用域,`drop` 被调用,内存被释放

fn makes_copy(some_integer: i32) {
    println!("{}", some_integer);
} // some_integer 离开作用域,但因为是 Copy 类型,不需要特殊处理

2.2 返回值与作用域

Rust
fn main() {
    let s1 = gives_ownership();         // gives_ownership 将返回值的所有权移动给 s1
    let s2 = String::from("hello");    // s2 进入作用域
    let s3 = takes_and_gives_back(s2);  // s2 移动到函数,函数返回值移动给 s3
    // println!("{}", s2);              // 编译错误!s2 不再有效
} // s1、s3 离开作用域,被 drop

fn gives_ownership() -> String {
    let some_string = String::from("yours");
    some_string  // some_string 的所有权被返回
}

fn takes_and_gives_back(a_string: String) -> String {
    a_string  // a_string 的所有权被返回
}

🔍 引用与借用

3.1 不可变引用

Rust
fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // 借用 s1

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
} // s 离开作用域,但因为它不拥有引用的值,所以什么也不会发生

3.2 可变引用

Rust
fn main() {
    let mut s = String::from("hello");
    change(&mut s);
    println!("{}", s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

可变引用的限制: 1. 在特定作用域中,一个数据只能有一个可变引用 2. 不能同时拥有可变和不可变引用

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

3.3 悬垂引用

Rust 编译器防止悬垂引用:

Rust
fn main() {
    let reference_to_nothing = dangle();
}

// 编译错误!
// fn dangle() -> &String {
//     let s = String::from("hello");
//     &s // 返回 s 的引用
// } // s 离开作用域并被丢弃,引用指向无效内存

// 正确的做法
fn dangle() -> String {
    let s = String::from("hello");
    s // 返回 s 的所有权
}

📏 切片

4.1 字符串切片

Rust
fn main() {
    let s = String::from("hello world");
    let len = s.len();

    let hello = &s[0..5];   // "hello"
    let world = &s[6..11];  // "world"

    println!("hello: {}, world: {}", hello, world);

    // 简写
    let slice = &s[0..2];   // "he"
    let slice = &s[..2];    // "he"

    let slice = &s[3..len]; // "lo world"
    let slice = &s[3..];    // "lo world"

    let slice = &s[0..len]; // "hello world"
    let slice = &s[..];     // "hello world"
}

4.2 字符串字面量是切片

Rust
fn main() {
    let s = "Hello, world!"; // s 的类型是 &str
    println!("{}", s);
}

4.3 其他切片

Rust
fn main() {
    let a = [1, 2, 3, 4, 5];
    let slice = &a[1..3]; // 类型是 &[i32]

    println!("slice: {:?}", slice);
}

⏰ 生命周期

5.1 生命周期标注

生命周期标注确保引用始终有效:

Rust
fn main() {
    let string1 = String::from("long string is long");
    let string2 = String::from("xyz");
    let result = longest(string1.as_str(), string2.as_str());
    println!("The longest string is {}", result);
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {  // 'a 生命周期标注,确保引用有效
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

注意: 返回值的生命周期必须与某个输入参数的生命周期相关联。以下示例会编译失败,因为result可能引用了已失效的string2

Rust
// 这段代码无法编译!
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    } // string2 在这里被丢弃
    // println!("The longest string is {}", result); // 编译错误!
}

5.2 结构体中的生命周期

Rust
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,
    };
    println!("Important excerpt: {}", i.part);
}

5.3 生命周期省略规则

Rust 编译器有三条省略规则,让我们在大多数情况下无需手动标注生命周期:

规则一:每个引用参数都获得各自的生命周期参数

Rust
// 编写时省略生命周期:
fn print_str(s: &str) { println!("{}", s); }
// 编译器补全后等价于:
// fn print_str<'a>(s: &'a str) { println!("{}", s); }

// 两个参数各自获得独立的生命周期:
fn longer(a: &str, b: &str) { /* ... */ }
// 编译器补全后等价于:
// fn longer<'a, 'b>(a: &'a str, b: &'b str) { /* ... */ }

规则二:如果只有一个输入生命周期参数,它被赋予所有输出生命周期参数

Rust
// 编写时省略生命周期:
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}
// 编译器补全后等价于:
// fn first_word<'a>(s: &'a str) -> &'a str { ... }
// 只有一个输入引用,所以返回值自动使用相同的生命周期

规则三:如果有多个输入生命周期参数,但其中一个是 &self&mut selfself 的生命周期被赋予所有输出生命周期参数

Rust
struct Parser {
    input: String,
}

impl Parser {
    // 编写时省略生命周期:
    fn parse(&self, prefix: &str) -> &str {
        &self.input[prefix.len()..]
    }
    // 编译器补全后等价于:
    // fn parse<'a, 'b>(&'a self, prefix: &'b str) -> &'a str { ... }
    // 返回值的生命周期跟随 &self,而非 prefix
}

注意:当三条规则都无法确定返回值的生命周期时,编译器会报错,要求你手动标注:

Rust
// 编译错误!两个输入引用且没有 &self,编译器无法推断返回值生命周期
// fn longest(x: &str, y: &str) -> &str { ... }

// 必须手动标注:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

5.4 静态生命周期

'static 生命周期表示引用在整个程序运行期间都有效:

Rust
fn main() {
    let s: &'static str = "I have a static lifetime.";
    println!("{}", s);
}

📝 练习题

练习 1:所有权转移

Rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    // TODO: 修复代码,使其能编译通过
    println!("{}", s1);
}

练习 2:函数返回值

Rust
fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    println!("The length of '{}' is {}.", s2, len);
}

// TODO: 实现 calculate_length 函数
fn calculate_length(s: String) -> (String, usize) {
    // 返回字符串和其长度
    todo!()
}

练习 3:借用规则

Rust
fn main() {
    let mut s = String::from("hello");
    // TODO: 修复代码,使其能编译通过
    let r1 = &s;
    let r2 = &mut s;
    println!("{} and {}", r1, r2);
}

练习 4:生命周期

Rust
fn main() {
    let string1 = String::from("long string is long");
    let string2 = String::from("xyz");
    let result = longest(string1.as_str(), string2.as_str());
    println!("The longest string is {}", result);
}

// TODO: 添加生命周期标注
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

💡 最佳实践

1. 优先使用不可变引用

Rust
// 好
fn read_data(data: &Data) {
    // 只读取数据
}

// 避免不必要的可变引用
fn read_data(data: &mut Data) {
    // 如果不需要修改,不要使用可变引用
}

2. 最小化借用范围

Rust
// 好
fn process(data: &mut Data) {
    let temp = data.calculate_temp();
    data.update(temp);
}

// 避免
fn process(data: &mut Data) {
    let temp = data.calculate_temp();
    let other = data.calculate_other();
    data.update(temp);
    data.update_other(other);
}

3. 使用 Cow 避免不必要的克隆

Rust
use std::borrow::Cow;

fn process_string(s: &str) -> Cow<str> {
    if s.contains("special") {
        // 需要修改,返回拥有的 String
        let mut result = s.to_string();
        result.push_str("_processed");
        Cow::Owned(result)
    } else {
        // 不需要修改,返回借用
        Cow::Borrowed(s)
    }
}

⚠️ 常见错误

1. 使用已移动的值

Rust
// 错误
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // 编译错误

// 正确
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}", s1);

2. 多个可变引用

Rust
// 错误
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // 编译错误

// 正确
let mut s = String::from("hello");
{
    let r1 = &mut s;
    println!("{}", r1);
}
let r2 = &mut s;
println!("{}", r2);

3. 生命周期不匹配

Rust
// 错误
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y // 编译错误!y 的生命周期可能比 'a 短
    }
}

// 正确
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

📚 扩展阅读

🎯 本章小结

本章深入讲解了 Rust 的所有权系统:

  • ✅ 理解所有权的三大规则
  • ✅ 掌握移动、拷贝和克隆的区别
  • ✅ 学会使用不可变和可变引用
  • ✅ 理解借用规则和悬垂引用
  • ✅ 掌握字符串切片
  • ✅ 理解生命周期标注和省略规则

下一章: 我们将学习结构体与枚举,了解如何定义自定义数据类型。