正在加载,请稍候…

Rust 所有权与借用:面向其他语言开发者的实用指南

从零理解 Rust 的所有权系统:所有权规则、借用、生命周期、借用检查器,以及如何将 JavaScript/Python/Go 的常见模式翻译为 Rust 代码

Rust 所有权与借用:面向其他语言开发者的实用指南

为什么 Rust 的学习曲线存在(以及为什么值得)

每个学习 Rust 的开发者都会遇到同样的障碍:借用检查器拒绝了那些“显然应该能工作”的代码。那些在 C、Python、Go 或 JavaScript 中能正常编译的代码,却会因晦涩的生命周期错误而失败。

这不是一个 bug——而是一个特性。借用检查器是一个编译时工具,它消除了整类 bug:释放后使用、数据竞争、空指针解引用、双重释放错误。理解为什么它会拒绝你的代码,将解锁系统编程中最强大的保证之一。

本指南面向有其他语言经验的开发者。我们将通过对比来建立直觉。

Rust 所有权与借用:面向其他语言开发者的实用指南 插图

Rust 解决的核心问题

内存安全漏洞是 C/C++ 代码库中约 70% 安全漏洞的根源(据微软、谷歌、NSA)。例如:

// C:释放后使用——未定义行为
char* ptr = malloc(10);
free(ptr);
*ptr = 'A'; // Bug!读取已释放的内存

// C:空指针解引用
char* name = NULL;
printf("%s", name); // 崩溃或未定义行为

// C:数据竞争(两个线程写入同一内存)
// 无编译器警告——运行时崩溃或静默错误结果

Rust 的所有权系统使得所有这些都无法编写——编译器在代码运行之前就拒绝了它们。

所有权:三条规则

规则 1:Rust 中的每个值都有一个所有者。
规则 2:同一时间只能有一个所有者。
规则 3:当所有者离开作用域时,该值将被丢弃(内存释放)。
fn main() {
    let s1 = String::from("hello"); // s1 拥有 String
    
    let s2 = s1; // 所有权 MOVED 到 s2
    
    // println!("{}", s1); // ❌ 编译器错误:"value borrowed here after move"
    println!("{}", s2); // ✅ s2 是所有者
    
    // 当 s2 离开作用域(main 结束)时,String 被丢弃
} // s2 在此处被丢弃——内存自动释放,无需垃圾回收器

这与 JavaScript/Python/Go 完全不同:

// JavaScript:两个引用都有效(垃圾回收)
let s1 = "hello"
let s2 = s1
console.log(s1, s2) // 两者都工作
// Rust:只有一个所有者——使用 clone() 进行深拷贝
let s1 = String::from("hello");
let s2 = s1.clone(); // 显式复制堆数据

println!("{}", s1); // ✅ s1 仍然有效(我们克隆了,而不是移动)
println!("{}", s2); // ✅ s2 有自己的副本

复制类型

基本类型实现了 Copy trait——它们会被复制而不是移动:

let x = 5;
let y = x; // x 被复制(整数存储在栈上)

println!("{}", x); // ✅ x 仍然有效——整数实现了 Copy
println!("{}", y); // ✅

// 实现了 Copy 的类型:整数、浮点数、bool、char、包含 Copy 类型的元组
// 未实现 Copy 的类型:String、Vec、HashMap、Box 以及任何堆分配的数据

借用:不带所有权的引用

将所有权传递给每个函数会非常繁琐。借用让你可以在不获取所有权的情况下使用一个值:

fn calculate_length(s: &String) -> usize { // &String = 对 String 的引用
    s.len()
} // s 离开作用域,但它不拥有 String——没有东西被丢弃

fn main() {
    let s1 = String::from("hello");
    
    let len = calculate_length(&s1); // 传递引用(借用,而非移动)
    
    println!("The length of '{}' is {}.", s1, len); // ✅ s1 仍然有效!
}

可变引用

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

fn main() {
    let mut s = String::from("hello"); // 变量必须是 mut
    
    change(&mut s); // 可变引用
    
    println!("{}", s); // "hello, world"
}

Rust 所有权与借用:面向其他语言开发者的实用指南 插图

引用规则

规则 1:在任何给定时间,你可以拥有 EITHER:
         - 一个可变引用 (&mut T),或者
         - 任意数量的不可变引用 (&T)
         但不能同时拥有两者。

规则 2:引用必须始终有效(无悬垂引用)。
fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;  // 不可变引用——OK
    let r2 = &s;  // 另一个不可变引用——OK
    println!("{} and {}", r1, r2);
    // r1 和 r2 在此处不再使用(NLL——非词法生命周期)
    
    let r3 = &mut s; // 可变引用——OK,因为 r1 和 r2 不再使用
    println!("{}", r3);
}

fn simultaneous_borrows() {
    let mut s = String::from("hello");
    
    let r1 = &s;        // OK
    let r2 = &mut s;    // ❌ 编译器错误!不可变引用存在时不能有可变引用
    
    println!("{}, {}", r1, r2);
}

为什么有这个规则:对同一数据的多个可变引用会导致数据竞争。这个规则在编译时使数据竞争成为不可能。

生命周期

生命周期确保引用不会超过它们所指向的数据的生命周期:

// ❌ 悬垂引用——借用检查器阻止的情况:
fn dangle() -> &String {
    let s = String::from("hello"); // s 在此处创建
    &s // 返回对 s 的引用
} // s 在此处被丢弃——引用将无效!

// ✅ 返回 String(转移所有权):
fn no_dangle() -> String {
    let s = String::from("hello");
    s // 所有权移交给调用者
}

生命周期注解

当函数返回一个引用时,Rust 需要知道返回值与哪个参数的生命周期相关联:

// 编译器无法确定:返回值与 x 还是 y 的生命周期一样长?
fn longest(x: &str, y: &str) -> &str { // ❌ 错误:缺少生命周期说明符
    if x.len() > y.len() { x } else { y }
}

// 生命周期注解:返回值至少与 x 和 y 中较短的那个一样长
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { // ✅
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("xyz");
        result = longest(s1.as_str(), s2.as_str()); // result 的生命周期与 s2 绑定
        println!("{}", result); // ✅ s2 在此作用域内仍然有效
    }
    // println!("{}", result); // ❌ s2 被丢弃——result 将悬垂
}

结构体生命周期

// 包含引用的结构体——需要生命周期注解
struct Important<'a> {
    content: &'a str, // 此引用必须与结构体存活时间一样长
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence;
    {
        let i = Important { content: novel.split('.').next().unwrap() };
        first_sentence = i.content;
    }
    println!("{}", first_sentence); // ✅ novel 仍然存活
}

常见模式翻译

Rust 所有权与借用:面向其他语言开发者的实用指南 插图

Option 替代空值

// Rust 没有 null——使用 Option<T>
fn find_user(id: u32) -> Option<User> {
    if id == 0 { None } else { Some(User { id, name: "Alice".to_string() }) }
}

fn main() {
    match find_user(1) {
        Some(user) => println!("Found: {}", user.name),
        None => println!("Not found"),
    }
    
    // 使用 if let 的简写:
    if let Some(user) = find_user(1) {
        println!("Found: {}", user.name);
    }
    
    // 链式调用 map、and_then:
    let name = find_user(1)
        .map(|user| user.name)
        .unwrap_or_else(|| "Unknown".to_string());
}

Result<T, E> 替代异常

use std::fs;
use std::io;

fn read_file(path: &str) -> Result<String, io::Error> {
    fs::read_to_string(path) // 返回 Result<String, io::Error>
}

fn main() {
    // 匹配 Result:
    match read_file("config.json") {
        Ok(content) => println!("File: {}", content),
        Err(e) => println!("Error: {}", e),
    }
    
    // ? 运算符——向上传播错误:
    fn process() -> Result<(), io::Error> {
        let content = read_file("config.json")?; // 如果失败则返回 Err
        println!("Content: {}", content);
        Ok(())
    }
}

迭代器(函数式风格)

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // 等价于 JavaScript 的 map/filter/reduce:
    let result: Vec<i32> = numbers
        .iter()
        .filter(|&&x| x % 2 == 0)  // 保留偶数
        .map(|&x| x * x)            // 平方
        .collect();                  // 收集到 Vec
    
    println!("{:?}", result); // [4, 16]
    
    // 求和
    let sum: i32 = numbers.iter().sum();
    
    // Any/All
    let has_even = numbers.iter().any(|&x| x % 2 == 0);
    let all_positive = numbers.iter().all(|&x| x > 0);
    
    // 链式迭代器
    let combined: Vec<i32> = [1, 2, 3]
        .iter()
        .chain([4, 5, 6].iter())
        .copied()
        .collect();
}

结构体和枚举

// 结构体——类似于没有方法的类
#[derive(Debug, Clone)]
struct User {
    id: u32,
    name: String,
    email: String,
    active: bool,
}

// 实现方法
impl User {
    // 构造函数模式(Rust 没有 "new" 关键字——这是一个约定)
    pub fn new(id: u32, name: String, email: String) -> Self {
        User { id, name, email, active: true }
    }
    
    // 方法(&self = 对 self 的不可变引用)
    pub fn display_name(&self) -> &str {
        &self.name
    }
    
    // 可变方法
    pub fn deactivate(&mut self) {
        self.active = false;
    }
}

// 枚举可以携带数据(标签联合——比其他语言更强大)
#[derive(Debug)]
enum Shape {
    Circle(f64),           // 半径
    Rectangle(f64, f64),  // 宽度,高度
    Triangle { base: f64, height: f64 }, // 命名字段
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle(r) => std::f64::consts::PI * r * r,
            Shape::Rectangle(w, h) => w * h,
            Shape::Triangle { base, height } => 0.5 * base * height,
        }
    }
}

“与借用检查器斗争”阶段

初学者常与借用检查器斗争的情况以及解决方法:

// 问题:在迭代时修改集合
let mut v = vec![1, 2, 3, 4, 5];
// ❌ 迭代时不能可变借用
// for x in &v {
//     if *x == 3 { v.push(6); } // 错误!
// }

// 解决方案:收集索引或使用 retain
v.retain(|&x| x != 3); // 移除 3
// 或先收集更改:
let additions: Vec<i32> = v.iter().filter(|&&x| x > 3).map(|&x| x * 2).collect();
v.extend(additions);

// 问题:同时借用结构体的多个字段
struct Config {
    data: Vec<u8>,
    name: String,
}

impl Config {
    fn process(&mut self) {
        // ❌ 不能通过 &mut self 同时可变借用两个字段
        // self.name = transform(&self.data); // 如果 transform 接受 &mut 则错误
        
        // 解决方案:使用单独的变量
        let transformed = transform(&self.data); // 借用 data
        self.name = transformed; // 现在更新 name
    }
}

Rust 的学习曲线很陡峭,但主要集中在前期。几周后,借用检查器就会成为有用的向导,而不是障碍。回报是:一旦你的代码编译通过,整类 bug 就保证不存在了。

→ 使用 String Obfuscator 工具混淆和转换字符串数据。