Skip to content

所有权实战总结

> 综合应用所有权知识,解决实际问题

本章目标

完成本章学习后,你将能够:

  • 识别和修复常见所有权错误
  • 应用所有权最佳实践优化代码
  • 理解性能影响并做出合理选择
  • 重构代码以符合所有权规则

常见错误详解

错误 1:使用已移动的值(完整错误分析)

错误描述

最常见的所有权错误:使用已经移动的值。

错误代码

rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // s1 的所有权移动到 s2
    
    println!("{}", s1);  // ❌ 错误:使用了已移动的值
}
▶ Run

完整编译错误信息

error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:20
  |
3 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
  |            -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
4 |     let s2 = s1;
  |              -- value moved here
5 |     println!("{}", s1);
  |                    ^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` 
          which comes from the expansion of the macro `println` 
          (in Nightly builds, run with -Z macro-backtrace for more info)

help: consider cloning the value before moving it
  |
4 |     let s2 = s1.clone();
  |              ++++++++++++

For more information about this error, try `rustc --explain E0382`.

错误解析

┌─────────────────────────────────────────────────────┐
│              错误 E0382 详细分析                     │
├─────────────────────────────────────────────────────┤
│                                                     │
│  错误代码:E0382                                    │
│  名称:use of moved value                           │
│                                                     │
│  发生原因:                                          │
│  • s1 的类型是 String                              │
│  • String 没有实现 Copy trait                      │
│  • let s2 = s1 导致所有权移动                       │
│  • s1 被标记为无效                                  │
│  • 尝试使用无效的 s1                                │
│                                                     │
│  编译器提示:                                        │
│  • 建议使用 s1.clone()                             │
│  • 这样可以保留 s1 的所有权                          │
│                                                     │
│  根本原因:                                          │
│  • Rust 的所有权系统防止双重释放                    │
│  • 如果允许使用 s1,可能导致:                      │
│    - 双重释放(s1 和 s2 都释放同一内存)            │
│    - 使用已释放的内存                               │
│                                                     │
└─────────────────────────────────────────────────────┘

修复方案对比

rust
// 方案 1:克隆(保留原值)
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();  // 显式克隆
    
    println!("s1 = {}", s1);  // ✅ 正确
    println!("s2 = {}", s2);  // ✅ 正确
}

// 方案 2:使用引用(不转移所有权)
fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;  // 借用
    
    println!("s1 = {}", s1);  // ✅ 正确
    println!("s2 = {}", s2);  // ✅ 正确
}

// 方案 3:接受移动(放弃原值)
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // 移动
    
    // 不使用 s1
    println!("s2 = {}", s2);  // ✅ 正确
}
▶ Run

错误 2:双重释放尝试

错误描述

Rust 编译器防止双重释放,这是其他语言中常见的内存安全问题。

为什么 Rust 能防止?

┌─────────────────────────────────────────────────────┐
│              双重释放防止机制                        │
├─────────────────────────────────────────────────────┤
│                                                     │
│  C++ 双重释放示例(危险):                         │
│  ┌────────────────────────────────────┐            │
│  │ char* p = new char[10];            │            │
│  │ char* q = p;  // 复制指针          │            │
│  │                                    │            │
│  │ delete[] p;  // 释放               │            │
│  │ delete[] q;  // 再次释放!         │            │
│  │ // 程序崩溃或数据损坏              │            │
│  └────────────────────────────────────┘            │
│                                                     │
│  Rust 防止方式:                                    │
│  ┌────────────────────────────────────┐            │
│  │ let s1 = String::from("hello");   │            │
│  │ let s2 = s1;  // 移动              │            │
│  │                                    │            │
│  │ // s1 无效,不能使用               │            │
│  │ // 只有 s2 能释放内存              │            │
│  │ // 编译器保证唯一释放              │            │
│  └────────────────────────────────────┘            │
│                                                     │
│  内存变化过程:                                      │
│                                                     │
│  移动前:                                            │
│  栈:s1 ──▶ 堆:"hello"                             │
│                                                     │
│  移动后:                                            │
│  栈:s1 (无效)                                      │
│  栈:s2 ──▶ 堆:"hello"                             │
│                                                     │
│  作用域结束:                                        │
│  s2 离开作用域 ──▶ Rust 调用 drop(s2)              │
│  堆内存被释放                                        │
│  s1 无效,不会调用 drop                             │
│                                                     │
│  关键:编译器阻止使用无效变量                       │
│                                                     │
└─────────────────────────────────────────────────────┘

如果尝试双重释放

rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    
    // 假设 Rust 允许使用 s1(实际不允许)
    // 当 s1 和 s2 都离开作用域时:
    // - drop(s1) 会被调用
    // - drop(s2) 会被调用
    // - 同一内存被释放两次!
    // - 程序崩溃或内存损坏
    
    // 但 Rust 编译器阻止这种情况
    // 通过所有权系统保证安全
}
▶ Run

错误 3:悬垂指针尝试

错误描述

悬垂指针是指指向已释放内存的指针。Rust 编译器能检测并防止这种情况。

错误代码示例

rust
fn create_dangling() -> &String {
    let s = String::from("hello");
    &s  // ❌ 错误:返回局部变量的引用
}  // s 在这里被释放

fn main() {
    let reference = create_dangling();
    // reference 指向已释放的内存
    println!("{}", reference);
}
▶ Run

完整编译错误信息

error[E0106]: missing lifetime specifier
 --> src/main.rs:1:25
  |
1 | fn create_dangling() -> &String {
  |                         ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, 
          but there is no value for it to be created from
  = help: consider giving it a 'static lifetime

error: `s` does not live long enough
 --> src/main.rs:3:5
  |
2 |     let s = String::from("hello");
  |         - binding `s` declared here
3 |     &s
  |     ^^ borrowed value does not live long enough
4 | }
  | - `s` dropped here while still borrowed
  |
  = note: borrowed value must be valid for the lifetime 'static 
          as defined on the function signature at 1:25...

错误解析

┌─────────────────────────────────────────────────────┐
│              悬垂指针防止机制                        │
├─────────────────────────────────────────────────────┤
│                                                     │
│  问题分析:                                          │
│                                                     │
│  函数内:                                            │
│  ┌────────────────────────────────────┐            │
│  │ let s = String::from("hello");    │            │
│  │ // s 在堆上分配                    │            │
│  │                                    │            │
│  │ return &s;                         │            │
│  │ // 返回 s 的引用                   │            │
│  │                                    │            │
│  │ } // 函数结束                      │            │
│  │ // s 被释放                        │            │
│  │ // 堆内存被回收                    │            │
│  └────────────────────────────────────┘            │
│                                                     │
│  函数外:                                            │
│  ┌────────────────────────────────────┐            │
│  │ let reference = create_dangling();│            │
│  │ // reference 指向已释放的内存      │            │
│  │ // 悬垂指针!                      │            │
│  │                                    │            │
│  │ println!("{}", reference);        │            │
│  │ // 使用已释放的内存                │            │
│  │ // 未定义行为                      │            │
│  └────────────────────────────────────┘            │
│                                                     │
│  Rust 的解决方案:                                  │
│  ┌────────────────────────────────────┐            │
│  │ 方案 1:返回所有权                 │            │
│  │ fn create() -> String {           │            │
│  │     String::from("hello")         │            │
│  │ }                                  │            │
│  │                                    │            │
│  │ 方案 2:借用外部数据               │            │
│  │ fn borrow(s: &String) -> &str {  │            │
│  │     &s[..]                         │            │
│  │ }                                  │            │
│  └────────────────────────────────────┘            │
│                                                     │
│  编译器检测机制:                                    │
│  • 生命周期分析                                      │
│  • 引用有效性检查                                    │
│  • 作用域匹配                                        │
│                                                     │
└─────────────────────────────────────────────────────┘

修复方法

rust
// ✅ 正确:返回所有权
fn create_string() -> String {
    let s = String::from("hello");
    s  // 返回所有权,不返回引用
}

fn main() {
    let s = create_string();
    println!("{}", s);  // s 拥有所有权,可以使用
}

// ✅ 正确:借用外部数据
fn get_first_word(s: &String) -> &str {
    &s[..5]  // 借用外部数据,安全
}

fn main() {
    let s = String::from("hello world");
    let word = get_first_word(&s);
    println!("{}", word);  // word 借用 s 的数据
    println!("{}", s);  // s 仍然有效
}
▶ Run

错误 4:循环引用问题

错误描述

循环引用可能导致内存泄漏,因为引用计数永远不会降到零。

示例(使用 Rc 和 RefCell)

rust
use std::rc::Rc;
use std::cell::RefCell;

struct Node {
    value: i32,
    next: Option<Rc<RefCell<Node>>>,
}

fn main() {
    let a = Rc::new(RefCell::new(Node { value: 1, next: None }));
    let b = Rc::new(RefCell::new(Node { value: 2, next: None }));
    
    // 创建循环引用
    (*a.borrow_mut()).next = Some(b.clone());
    (*b.borrow_mut()).next = Some(a.clone());
    
    // a 和 b 的引用计数都是 2
    // 离开作用域时降到 1
    // 内存泄漏!
}
▶ Run

解决方案:使用 Weak

rust
use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
    value: i32,
    next: Option<Weak<RefCell<Node>>>,  // 使用 Weak
}

fn main() {
    let a = Rc::new(RefCell::new(Node { value: 1, next: None }));
    let b = Rc::new(RefCell::new(Node { value: 2, next: None }));
    
    // 使用 Weak 避免循环
    (*a.borrow_mut()).next = Some(Rc::downgrade(&b));
    (*b.borrow_mut()).next = Some(Rc::downgrade(&a));
    
    // 离开作用域时正确释放
}
▶ Run

实战场景

场景 1:构建字符串处理管道

问题描述

构建一个处理字符串的管道,每一步都可能修改字符串。

解决方案

rust
fn main() {
    let text = String::from("  hello, WORLD!  ");
    
    println!("原始: \"{}\"", text);
    
    // 方案 1:所有权传递
    let processed = trim(text)
        .and_then(to_lowercase)
        .and_then(add_prefix)
        .and_then(reverse);
    
    println!("处理后: \"{}\"", processed);
    
    // 方案 2:使用引用(如果不需要修改所有权)
    let text2 = String::from("  hello, WORLD!  ");
    println!("\n使用引用:");
    print_trimmed(&text2);
    print_lowercase(&text2);
    println!("原值仍可用: \"{}\"", text2);
}

trait StringProcessor {
    fn and_then(self, f: fn(String) -> String) -> Self;
}

impl StringProcessor for String {
    fn and_then(self, f: fn(String) -> String) -> Self {
        f(self)
    }
}

fn trim(s: String) -> String {
    s.trim().to_string()
}

fn to_lowercase(s: String) -> String {
    s.to_lowercase()
}

fn add_prefix(s: String) -> String {
    format!("PREFIX_{}", s)
}

fn reverse(s: String) -> String {
    s.chars().rev().collect()
}

fn print_trimmed(s: &str) {
    println!("修剪: \"{}\"", s.trim());
}

fn print_lowercase(s: &str) {
    println!("小写: \"{}\"", s.to_lowercase());
}
▶ Run

运行结果

原始: "  hello, WORLD!  "
处理后: "PREFIX_!dlrow ,olleh"

使用引用:
修剪: "hello, WORLD!"
小写: "hello, world!"
原值仍可用: "  hello, WORLD!  "

场景 2:集合类型所有权管理

问题描述

管理 Vec、HashMap 等集合中的所有权。

解决方案

rust
use std::collections::HashMap;

fn main() {
    println!("=== Vec 所有权 ===");
    
    // 创建 Vec
    let mut vec = Vec::new();
    
    // 添加元素(所有权移动)
    vec.push(String::from("hello"));
    vec.push(String::from("world"));
    
    println!("Vec: {:?}", vec);
    
    // 取出元素
    let first = vec.remove(0);
    println!("取出: {}", first);
    println!("剩余: {:?}", vec);
    
    println!("\n=== HashMap 所有权 ===");
    
    let mut map = HashMap::new();
    
    let key = String::from("name");
    let value = String::from("Rust");
    
    map.insert(key, value);  // 所有权移动到 HashMap
    
    // println!("key: {}", key);  // ❌ 错误:key 已移动
    
    // 获取值(借用)
    if let Some(v) = map.get("name") {
        println!("值: {}", v);
    }
    
    // 取出值(所有权转移)
    if let Some(v) = map.remove("name") {
        println!("移除: {}", v);
        println!("现在拥有: {}", v);
    }
    
    println!("\n=== 避免所有权转移 ===");
    
    // 使用 clone 避免移动
    let mut map2 = HashMap::new();
    let key2 = String::from("language");
    let value2 = String::from("Rust");
    
    map2.insert(key2.clone(), value2.clone());
    
    println!("仍可用: key={}, value={}", key2, value2);
    println!("HashMap: {:?}", map2);
}
▶ Run

运行结果

=== Vec 所有权 ===
Vec: ["hello", "world"]
取出: hello
剩余: ["world"]

=== HashMap 所有权 ===
值: Rust
移除: Rust
现在拥有: Rust

=== 避免所有权转移 ===
仍可用: key=language, value=Rust
HashMap: {"language": "Rust"}

场景 3:结构体字段所有权

问题描述

管理结构体字段的所有权。

解决方案

rust
struct User {
    name: String,      // 拥有所有权
    email: String,     // 拥有所有权
    age: u32,          // Copy 类型
}

impl User {
    // 创建用户
    fn new(name: String, email: String, age: u32) -> Self {
        Self { name, email, age }
    }
    
    // 获取名字(借用)
    fn get_name(&self) -> &str {
        &self.name
    }
    
    // 获取邮箱(借用)
    fn get_email(&self) -> &str {
        &self.email
    }
    
    // 修改邮箱(可变借用)
    fn set_email(&mut self, email: String) {
        self.email = email;
    }
    
    // 克隆用户
    fn clone_user(&self) -> User {
        User {
            name: self.name.clone(),
            email: self.email.clone(),
            age: self.age,  // Copy,自动复制
        }
    }
}

fn main() {
    println!("=== 结构体所有权 ===");
    
    let name = String::from("Alice");
    let email = String::from("alice@example.com");
    
    // 创建用户(所有权移动到结构体)
    let user = User::new(name, email, 30);
    
    // println!("name: {}", name);  // ❌ 错误:name 已移动
    
    // 通过方法访问(借用)
    println!("用户名: {}", user.get_name());
    println!("邮箱: {}", user.get_email());
    println!("年龄: {}", user.age);
    
    // 克隆
    let user2 = user.clone_user();
    println!("克隆用户: {} ({})", user2.get_name(), user2.get_email());
    
    // 修改
    let mut user3 = User::new(
        String::from("Bob"),
        String::from("bob@example.com"),
        25
    );
    user3.set_email(String::from("new@example.com"));
    println!("新邮箱: {}", user3.get_email());
}
▶ Run

运行结果

=== 结构体所有权 ===
用户名: Alice
邮箱: alice@example.com
年龄: 30
克隆用户: Alice (alice@example.com)
新邮箱: new@example.com

性能优化技巧

技巧 1:避免不必要的克隆

rust
fn main() {
    println!("=== 避免不必要克隆 ===");
    
    // ❌ 不推荐:过度克隆
    fn bad_example() {
        let s = String::from("hello");
        let s1 = s.clone();
        let s2 = s.clone();
        let s3 = s.clone();
        
        println!("{}", s1);
        println!("{}", s2);
        println!("{}", s3);
    }
    
    // ✅ 推荐:使用引用
    fn good_example() {
        let s = String::from("hello");
        
        println!("{}", s);  // 直接使用
        println!("{}", s);  // 直接使用
        println!("{}", s);  // 直接使用
    }
    
    // ✅ 推荐:传递引用给函数
    fn print_three_times(s: &str) {
        println!("{}", s);
        println!("{}", s);
        println!("{}", s);
    }
    
    print_three_times("hello");
}
▶ Run

技巧 2:预分配容量

rust
fn main() {
    println!("=== 预分配容量 ===");
    
    // ❌ 不推荐:频繁重新分配
    fn bad_example() {
        let mut s = String::new();
        for i in 0..1000 {
            s.push_str(&format!("item{}", i));  // 可能多次重新分配
        }
    }
    
    // ✅ 推荐:预分配容量
    fn good_example() {
        let mut s = String::with_capacity(10000);  // 预分配
        for i in 0..1000 {
            s.push_str(&format!("item{}", i));  // 无需重新分配
        }
    }
    
    // Vec 预分配
    let mut vec = Vec::with_capacity(100);
    for i in 0..100 {
        vec.push(i);  // 无需重新分配
    }
    
    println!("Vec 容量: {}", vec.capacity());
}
▶ Run

技巧 3:使用 Cow(Copy on Write)

rust
use std::borrow::Cow;

fn main() {
    println!("=== 使用 Cow ===");
    
    // Cow: 避免不必要的克隆
    fn process(input: Cow<str>) -> Cow<str> {
        if input.contains("error") {
            // 需要修改,克隆并修改
            let mut modified = input.into_owned();
            modified.replace_range(.., "fixed");
            Cow::Owned(modified)
        } else {
            // 不需要修改,直接返回借用
            input
        }
    }
    
    // 不需要修改的情况
    let borrowed = Cow::Borrowed("hello");
    let result = process(borrowed);
    println!("结果(借用): {}", result);
    
    // 需要修改的情况
    let owned = Cow::Borrowed("error message");
    let result = process(owned);
    println!("结果(修改): {}", result);
}
▶ Run

重构练习

练习 1:修复所有权错误

问题代码:

rust
fn main() {
    let s = String::from("hello");
    let s2 = s;
    println!("{}", s);  // 错误
}
▶ Run

要求:

  • 修复代码使其能编译
  • 保留 s2 的值
  • 提供三种不同的解决方案

参考答案:

rust
// 方案 1:克隆
fn main() {
    let s = String::from("hello");
    let s2 = s.clone();
    println!("s = {}", s);
    println!("s2 = {}", s2);
}

// 方案 2:引用
fn main() {
    let s = String::from("hello");
    let s2 = &s;
    println!("s = {}", s);
    println!("s2 = {}", s2);
}

// 方案 3:重新创建
fn main() {
    let s = String::from("hello");
    let s2 = s;
    let s = String::from("hello");  // 重新创建
    println!("s = {}", s);
    println!("s2 = {}", s2);
}
▶ Run

练习 2:函数参数重构

问题代码:

rust
fn print(s: String) {
    println!("{}", s);
}

fn main() {
    let s = String::from("hello");
    print(s);
    println!("{}", s);  // 错误:s 已移动
}
▶ Run

要求:

  • 修改函数签名使其更通用
  • 支持多次调用
  • 支持 String 和 &str

参考答案:

rust
fn print(s: &str) {
    println!("{}", s);
}

fn main() {
    let s = String::from("hello");
    print(&s);  // &String 自动转换为 &str
    print(&s);
    print("literal");  // 直接接受字面量
    println!("{}", s);  // s 仍有效
}
▶ Run

练习 3:结构体所有权设计

问题:设计一个表示博客文章的结构体

要求:

  • 包含标题、内容、标签
  • 提供获取方法(借用)
  • 提供修改方法(可变借用)
  • 实现克隆方法

参考答案:

rust
struct BlogPost {
    title: String,
    content: String,
    tags: Vec<String>,
}

impl BlogPost {
    fn new(title: String, content: String) -> Self {
        Self {
            title,
            content,
            tags: Vec::new(),
        }
    }
    
    fn get_title(&self) -> &str {
        &self.title
    }
    
    fn get_content(&self) -> &str {
        &self.content
    }
    
    fn get_tags(&self) -> &[String] {
        &self.tags
    }
    
    fn add_tag(&mut self, tag: String) {
        self.tags.push(tag);
    }
    
    fn clone_post(&self) -> BlogPost {
        BlogPost {
            title: self.title.clone(),
            content: self.content.clone(),
            tags: self.tags.clone(),
        }
    }
}

fn main() {
    let post = BlogPost::new(
        String::from("Rust所有权"),
        String::from("所有权是Rust的核心概念...")
    );
    
    println!("标题: {}", post.get_title());
    println!("内容: {}", post.get_content());
    
    let mut post = post;
    post.add_tag(String::from("Rust"));
    post.add_tag(String::from("编程"));
    
    println!("标签: {:?}", post.get_tags());
}
▶ Run

最佳实践总结

✅ 推荐做法

场景推荐原因
函数只读参数&str&T不消耗所有权,通用
函数修改参数&mut T不消耗所有权
函数需要所有权T明确所有权转移
字符串字段String拥有所有权
小型结构体#[derive(Copy)]自动复制,高效
大型数据道引用避免昂贵克隆
需要独立副本.clone()显式表达意图

❌ 避免做法

问题避免替代方案
过度消耗所有权fn f(s: String) 只为打印fn f(s: &str)
循环中移动for _ in 0..n { f(s) }使用引用
不必要克隆let s2 = s.clone(); 后不修改直接使用引用
未预分配容量Vec::new() 然后大量 pushVec::with_capacity(n)

性能考虑

移动 vs 克隆性能对比

┌─────────────────────────────────────────────────────┐
│              性能对比                               │
├──────────────────┬────────────────┬─────────────────┤
│     操作         │   移动         │   克隆          │
├──────────────────┼────────────────┼─────────────────┤
│ 栈复制           │ 24 字节        │ 24 字节        │
│ 堆复制           │ 无             │ N 字节         │
│ 时间(小数据)   │ ~1 ns          │ ~100 ns        │
│ 时间(1MB)      │ ~1 ns          │ ~1 ms          │
│ 内存占用         │ 不变           │ 增加           │
│ 原变量           │ 无效           │ 有效           │
├──────────────────┴────────────────┴─────────────────┤
│                                                     │
│  结论:                                              │
│  • 移动几乎免费                                      │
│  • 克隆大数据昂贵                                    │
│  • 优先使用移动或引用                                │
│  • 必要时才克隆                                     │
│                                                     │
└─────────────────────────────────────────────────────┘

小结

本章核心知识点

概念关键点应用场景
使用已移动值编译器阻止错误 E0382
双重释放所有权保证内存安全
悬垂指针生命周期检查错误 E0106
循环引用使用 WeakRc/RefCell
函数参数优先引用API 设计
性能优化避免克隆高效代码

学习检查清单

  • [ ] 理解常见所有权错误
  • [ ] 掌握错误修复方法
  • [ ] 能够设计安全的 API
  • [ ] 理解性能影响
  • [ ] 应用最佳实践

练习题

参见:所有权练习题


下一章

下一章将学习引用和借用,理解如何在不转移所有权的情况下使用数据。

➡️ 第 8 章:引用与借用