cv.cm

编码 · 编程语言

Rust 入门教程(零基础)

阅读约 9 分钟

简短回答

Rust 是一门编译型、静态类型的系统编程语言,速度与 C 和 C++ 相当,却能在编译期杜绝内存错误,并且不需要垃圾回收器。用 rustup 安装,然后运行 cargo new hello 和 cargo run。核心概念包括:所有权与借用(由借用检查器检查)、带 match 的结构体和枚举、Vec 和 HashMap 等集合,以及用 Result 和 ? 运算符处理错误。

打开二进制转换

Rust 是什么,用来做什么

Rust 起初是 Graydon Hoare 的个人项目,后来由 Mozilla 赞助,并在 2015 年发布 1.0 版本。现在由 Rust Foundation 负责管理。Rust 用于对速度和可靠性都有要求的场景:命令行工具(ripgrep)、Web 服务、Firefox 的浏览器组件、Linux 内核和 Windows 的部分模块、嵌入式设备和 WebAssembly。它的编译器很严格,但作为回报,在安全的 Rust 代码中,释放后使用(use-after-free)和数据竞争这类整类 bug 都不可能出现。

安装 Rust 并运行 Hello, world

从 rustup.rs 用 rustup 安装工具链。它提供 rustc(编译器)和 Cargo(构建工具与包管理器)。创建一个项目:

cargo new hello
cd hello
cargo run
# Hello, world!

Cargo 会生成 src/main.rs:

fn main() {
    println!("Hello, world!");
}

println! 以 ! 结尾,因为它是宏而不是函数。宏在编译期生成代码;这里的格式字符串会在编译时被检查。

变量与基本类型

变量默认不可变;加上 mut 才允许修改。类型通常可以推断,但每个值都恰好有一个静态类型,数值转换需要用 as 显式进行。

类型示例说明
i32, i64, u8, usizelet n: i32 = 42;有符号(i)和无符号(u);usize 用于索引
f64let x = 3.14;默认的浮点类型
boollet ok = true;true 或 false
charlet c = 'é';一个 Unicode 标量值,4 字节
&strlet s = "hi";借用的字符串切片
StringString::from("hi")拥有所有权、可增长的 UTF-8 文本
元组、数组(1, "a"), [0; 3]固定大小
fn main() {
    let name = "Ada";
    let mut count = 7;
    count += 1;
    let avg = count as f64 / 2.0;
    println!("{name} has {count} items, avg {avg}"); // Ada has 8 items, avg 4
    let parsed: i32 = "12".parse().unwrap();
    println!("{}", parsed + 1); // 13
}

流程控制与函数

if、match 和代码块都是表达式:它们会产生值。函数最后一个不带分号的表达式就是它的返回值。

fn square(x: i32) -> i32 {
    x * x
}

fn main() {
    let score = 72;
    let grade = if score >= 90 { "A" } else if score >= 70 { "B" } else { "C" };
    println!("{grade}"); // B

    for i in 0..3 {
        print!("{} ", square(i)); // 0 1 4
    }
    println!();

    let mut n = 10;
    while n > 1 {
        n /= 2;
    }
    let label = loop {
        break "done"; // loop can return a value with break
    };
    println!("{n} {label}"); // 1 done
}

标志性特性:所有权与借用

每个值恰好有一个所有者。所有者离开作用域时,该值就会被丢弃(释放)。把 String 这样的堆上的值赋给另一个变量会发生移动,旧的名字就不能再使用了。若要在不取得所有权的情况下使用某个值,就用引用借用它:同一时间可以有任意多个共享引用 &T,或者恰好一个可变引用 &mut T。

fn length(s: &str) -> usize {
    s.len() // borrows, does not take ownership
}

fn add_exclamation(s: &mut String) {
    s.push('!');
}

fn main() {
    let a = String::from("hello");
    let b = a; // ownership moves to b
    // println!("{a}"); // error: borrow of moved value: `a`
    let mut c = b.clone(); // explicit deep copy
    add_exclamation(&mut c);
    println!("{} {} {}", b, c, length(&c)); // hello hello! 6
}

这些规则由借用检查器在编译期强制执行,这就是 Rust 无需垃圾回收器也能保证内存安全的方式。整数等简单类型实现了 Copy,所以会被复制而不是移动。

结构体、枚举与模式匹配

结构体把字段组织在一起,impl 块为其添加方法。枚举的每个变体都可以携带数据,而 match 必须处理每一个变体,这一点由编译器检查。标准的 Option<T> 枚举(Some(value) 或 None)取代了 null。

#[derive(Debug)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
    fn distance(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

enum Shape {
    Circle(f64),
    Rect { w: f64, h: f64 },
}

fn area(s: &Shape) -> f64 {
    match s {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rect { w, h } => w * h,
    }
}

fn main() {
    let p = Point { x: 3.0, y: 4.0 };
    println!("{:?} {}", p, p.distance()); // Point { x: 3.0, y: 4.0 } 5
    println!("{}", area(&Shape::Rect { w: 2.0, h: 3.0 })); // 6
    let nums = [10, 20];
    if let Some(v) = nums.first() {
        println!("first is {v}"); // first is 10
    }
}

trait 定义共享的行为,类似于接口:#[derive(Debug)] 让编译器实现 Debug trait,这样 {:?} 就能打印该结构体。

集合:Vec 和 HashMap

use std::collections::HashMap;

fn main() {
    let mut nums = vec![3, 1, 2];
    nums.push(5);
    nums.sort();
    println!("{:?} {}", nums, nums.len()); // [1, 2, 3, 5] 4

    let evens: Vec<i32> = nums.iter().filter(|n| *n % 2 == 0).map(|n| n * n).collect();
    println!("{:?}", evens); // [4]

    let mut ages: HashMap<&str, u32> = HashMap::new();
    ages.insert("ann", 31);
    ages.insert("bob", 25);
    *ages.entry("ann").or_insert(0) += 1;
    println!("{:?} {:?}", ages.get("ann"), ages.get("zed")); // Some(32) None
}

带闭包(|n| ...)的迭代器,如 filter、map 和 collect,编译出的代码与手写循环一样快。

用 Result 和 ? 处理错误

可能失败的函数返回 Result<T, E>:要么是 Ok(value),要么是 Err(error)。? 运算符会提前把错误返回给调用者,因此错误处理路径保持简短。无法恢复的 bug 使用 panic!。

use std::num::ParseIntError;

fn parse_age(text: &str) -> Result<u32, ParseIntError> {
    let age: u32 = text.trim().parse()?; // returns Err early on bad input
    Ok(age)
}

fn main() {
    match parse_age("41") {
        Ok(age) => println!("age {age}"), // age 41
        Err(e) => println!("error: {e}"),
    }
    match parse_age("abc") {
        Ok(age) => println!("age {age}"),
        Err(e) => println!("error: {e}"), // error: invalid digit found in string
    }
}

一个完整的小程序:词频统计

把它放进 src/main.rs,然后运行 cargo run -- notes.txt。

use std::collections::HashMap;
use std::{env, fs, process};

fn main() {
    let path = env::args().nth(1).unwrap_or_else(|| {
        eprintln!("usage: words FILE");
        process::exit(1);
    });
    let text = match fs::read_to_string(&path) {
        Ok(t) => t.to_lowercase(),
        Err(e) => {
            eprintln!("cannot read {path}: {e}");
            process::exit(1);
        }
    };

    let mut counts: HashMap<&str, usize> = HashMap::new();
    for word in text.split(|c: char| !c.is_alphanumeric()).filter(|w| !w.is_empty()) {
        *counts.entry(word).or_insert(0) += 1;
    }

    let mut top: Vec<(&str, usize)> = counts.into_iter().collect();
    top.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
    for (word, n) in top.iter().take(5) {
        println!("{n:5}  {word}");
    }
}

HashMap 存放的是借用自 text 的 &str 切片,因此没有复制任何单词;借用检查器保证 text 的存活时间足够长。

新手常见错误

  • 在值被移动之后继续使用它。请借用它(&value),或有意地克隆它。
  • 试图用 s[0] 对 String 做索引。字符串是 UTF-8,所以请使用 s.chars(),或在字符边界处使用字节切片。
  • 持有指向 Vec 的引用的同时又向它 push;push 可能会重新分配内存,所以编译器禁止这样做。
  • 混淆 String(拥有所有权)和 &str(借用)。函数参数接受 &str 就能同时支持两者。

接下来学什么

阅读《The Rust Programming Language》(俗称 the book,可在 doc.rust-lang.org 免费阅读),并通过 Rust by Example 查看简短的可运行示例。然后深入学习 trait 与泛型、生命周期、用 cargo test 做测试,以及 crates.io 上的 crate,例如用于序列化的 serde 和用于异步程序的 tokio。运行 cargo clippy 可以获得更多代码检查建议。

常见问题

Rust 难学吗?

最初的几周比大多数语言都更难,因为所有权和借用是全新的概念。熟悉之后,编译器会及早发现许多 bug,很多开发者也觉得 Rust 代码更容易维护。

Rust 有垃圾回收器吗?

没有。内存会在其所有者离开作用域时自动释放,这由所有权规则在编译期决定。需要共享所有权时,可以使用引用计数类型 Rc 和 Arc。

什么是 unsafe Rust?

unsafe 块中的代码可以做编译器无法验证的事情,例如解引用裸指针或调用 C 函数。应当谨慎使用,通常会把它封装在安全的 API 之内。

Rust 和 Go 该学哪个?

Go 更简单,学起来更快,在网络服务领域很流行。Rust 提供更强的控制力和性能以及更严格的保证,代价是学习曲线更陡。

全部编码教程