Rust入门指南

0 次浏览 / 0 条评论

目录

用了一段时间 Rust 之后,想把核心概念整理一遍。这篇文章不是语法手册,而是帮你建立对 Rust 的直觉——尤其是所有权这套东西,理解了它,Rust 就通了一大半。

为什么是 Rust?

Rust 解决的核心问题是:在没有垃圾回收的情况下保证内存安全

C/C++ 让你手动管理内存,性能极高,但悬空指针、double free、数据竞争这些问题防不胜防。GC 语言(Go、Java、Python)解决了安全问题,但运行时开销和 GC 停顿是绕不开的代价。

Rust 的答案是把内存管理的规则编译进类型系统,让编译器在编译期替你检查,运行时零开销。代价是学习曲线陡。

所有权:Rust 的核心

Rust 里每个值有且只有一个所有者(owner)。所有者离开作用域,值就被释放。

{
    let s = String::from("hello"); // s 是这个字符串的所有者
    // 使用 s
} // s 离开作用域,字符串内存被释放

所有权转移(Move)

把值赋给另一个变量,所有权转移,原变量失效:

let s1 = String::from("hello");
let s2 = s1;           // 所有权转移给 s2
// println!("{}", s1); // 编译错误:s1 已经无效
println!("{}", s2);    // 正常

这就是 Rust 防止 double free 的方式——同一块内存永远只有一个所有者。

Clone:显式深拷贝

如果确实需要两份数据,用 .clone()

let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝,两者独立
println!("{} {}", s1, s2); // 都有效

Copy 类型

整数、浮点数、布尔、字符这些存在栈上的类型实现了 Copy trait,赋值时自动复制,不会转移所有权:

let x = 5;
let y = x;               // 复制,不是移动
println!("{} {}", x, y); // 都有效

借用:临时使用,不拿走

大多数时候你不想转移所有权,只是想用一下。用引用(&)来借用:

fn print_len(s: &String) {
    println!("长度: {}", s.len());
}

let s = String::from("hello");
print_len(&s);     // 借用给函数,s 的所有权没有转移
println!("{}", s); // s 仍然有效

可变借用

默认借用是只读的。要修改,需要可变借用 &mut

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

let mut s = String::from("hello");
add_world(&mut s);
println!("{}", s); // "hello, world"

借用规则

Rust 在编译期强制执行两条规则:

  1. 同一时刻,要么只有一个可变借用,要么有任意多个不可变借用
  2. 引用必须始终有效(不能有悬空引用)
let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;      // 多个不可变借用,没问题
// let r3 = &mut s; // 编译错误:不能同时存在可变和不可变借用

println!("{} {}", r1, r2);
// r1, r2 最后使用完毕,之后可以创建可变借用
let r3 = &mut s;
r3.push('!');

这套规则在编译期消灭了数据竞争。

枚举和模式匹配

Rust 的枚举比其他语言强大得多,每个变体可以携带不同类型的数据:

enum Shape {
    Circle(f64),
    Rectangle(f64, f64),
    Triangle { base: f64, height: f64 },
}

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

match 必须穷举所有变体,漏掉一个编译不过——这个特性在你添加新枚举变体时会提醒你更新所有相关处理逻辑,非常实用。

错误处理:Option 和 Result

Rust 没有 null,没有异常,用类型来表达”可能缺失”和”可能失败”。

Option:值可能不存在

fn find_first_even(nums: &[i32]) -> Option<i32> {
    for &n in nums {
        if n % 2 == 0 {
            return Some(n);
        }
    }
    None
}

match find_first_even(&[1, 3, 4, 7]) {
    Some(n) => println!("找到了: {}", n),
    None    => println!("没有偶数"),
}

Result:操作可能出错

use std::fs;

fn read_config(path: &str) -> Result<String, std::io::Error> {
    fs::read_to_string(path)
}

match read_config("config.toml") {
    Ok(content) => println!("{}", content),
    Err(e)      => eprintln!("读取失败: {}", e),
}

? 运算符:错误传播的语法糖

在返回 Result 的函数内,? 会在出错时提前返回错误:

fn process() -> Result<(), std::io::Error> {
    let content = fs::read_to_string("a.txt")?; // 出错则直接返回 Err
    let more    = fs::read_to_string("b.txt")?;
    println!("{}{}", content, more);
    Ok(())
}

相比手动 match 每个结果,? 让错误处理代码简洁很多。

Trait:定义共同行为

Trait 类似其他语言的接口,定义类型必须实现的方法:

trait Describe {
    fn describe(&self) -> String;
}

struct Dog { name: String }
struct Cat { name: String }

impl Describe for Dog {
    fn describe(&self) -> String {
        format!("{} 是一只狗", self.name)
    }
}

impl Describe for Cat {
    fn describe(&self) -> String {
        format!("{} 是一只猫", self.name)
    }
}

fn print_info(animal: &impl Describe) {
    println!("{}", animal.describe());
}

标准库里有很多常用 trait,比如 Display(格式化输出)、IteratorFrom/Into,实现它们就能融入 Rust 生态。

接下来

理解了所有权和借用,你对 Rust 的基础心智模型就建立起来了。建议的学习路径:

  • The Book — 官方教程,质量极高,适合系统学习
  • Rustlings — 通过修复小练习来熟悉语法,适合动手党
  • 找个实际项目练手,哪怕是写个命令行小工具,遇到编译器报错再查文档,进步最快

Rust 的学习曲线主要在前期跟编译器”吵架”,但过了那个阶段你会发现,编译器拦下来的那些错误,在其他语言里都是运行时 bug。