Rust 入门:前端开发者的视角

今年开始学 Rust,背景是 TypeScript/Node.js。记录学习过程。

为什么学 Rust

动机 说明
性能 Node.js 不擅长 CPU 密集型
工具链 很多新工具用 Rust 写(Turbopack、Rome)
WASM Rust 是 WebAssembly 的首选语言
成长 学习新范式

与 JavaScript 的对比

变量绑定

// Rust 默认不可变
let x = 5;
x = 6;  // 报错!

// 需要显式声明可变
let mut x = 5;
x = 6;  // OK
// JavaScript 默认可变
let x = 5;
x = 6;  // OK

内存管理

JavaScript 有垃圾回收,Rust 用所有权系统:

fn main() {
  let s1 = String::from("hello");
  let s2 = s1;  // s1 的所有权转移给 s2

  // println!("{}", s1);  // 报错!s1 已经失效
  println!("{}", s2);    // OK
}

这叫“移动语义”,JavaScript 程序员最不习惯的地方。

错误处理

// Result 类型,强制处理错误
fn read_file(path: &str) -> Result<String, io::Error> {
  fs::read_to_string(path)
}

fn main() {
  match read_file("test.txt") {
    Ok(content) => println!("{}", content),
    Err(e) => eprintln!("Error: {}", e),
  }
}
// JavaScript 用 try-catch
try {
  const content = fs.readFileSync('test.txt', 'utf-8');
  console.log(content);
} catch (e) {
  console.error('Error:', e);
}

Rust 强制你处理错误,虽然麻烦但更安全。

所有权系统

这是 Rust 最核心的概念。

规则

  1. 每个值有一个所有者
  2. 值在同一时刻只能有一个所有者
  3. 所有者离开作用域,值被释放

借用

不转移所有权,只是“借来用用”:

fn main() {
  let s = String::from("hello");

  let len = calculate_length(&s);  // 借用 s

  println!("{} has length {}", s, len);  // s 还能用
}

fn calculate_length(s: &String) -> usize {
  s.len()
}  // s 只是借用,不会释放

可变借用

fn main() {
  let mut s = String::from("hello");

  change(&mut s);

  println!("{}", s);  // "hello world"
}

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

规则:同一时刻,要么有一个可变引用,要么有多个不可变引用,不能同时存在。

所有权示意图

生命周期

最让新手头大的概念:

// 编译器需要知道返回的引用来自哪里
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, &s2);  // OK,s1 活得够长
  }

  // println!("{}", result);  // 报错!s2 已经没了
}

编译器在编译时检查引用是否有效,防止悬垂引用。

实战:写一个 CLI 工具

用 Rust 写命令行工具体验很好。

项目结构

cargo new mycli
cd mycli

代码

use clap::Parser;

#[derive(Parser)]
#[command(name = "mycli")]
#[command(about = "A simple CLI tool", long_about = None)]
struct Cli {
  #[arg(short, long)]
  name: String,

  #[arg(short, long, default_value_t = 1)]
  count: u8,
}

fn main() {
  let cli = Cli::parse();

  for _ in 0..cli.count {
    println!("Hello {}!", cli.name);
  }
}

运行

$ cargo run -- --name World --count 3
Hello World!
Hello World!
Hello World!

$ cargo run -- --help
A simple CLI tool

Usage: mycli [OPTIONS] --name <NAME>

Options:
  -n, --name <NAME>
  -c, --count <COUNT>  [default: 1]
  -h, --help           Print help

clap 会自动生成帮助信息,非常方便。

与 Node.js 交互

NAPI-RS

用 Rust 写 Node.js 原生模块:

use napi_derive::napi;

#[napi]
fn add(a: i32, b: i32) -> i32 {
  a + b
}

编译后在 JavaScript 里调用:

const { add } = require('./index.node');

console.log(add(1, 2));  // 3

WebAssembly

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
  a + b
}

在浏览器里用:

import { add } from './pkg/my_wasm.js';

console.log(add(1, 2));  // 3

生态对比

领域 JavaScript Rust
Web 框架 Express、Koa Actix、Axum
ORM Prisma、TypeORM Diesel、SeaORM
CLI Commander Clap
测试 Jest 内置
包管理 npm cargo

学习资源

资源 说明
The Rust Book 官方教程,必读
Rust by Example 代码示例学习
Rustlings 练习题
Crates.io 包仓库

学习曲线

总结

Rust 学习曲线确实陡,尤其是所有权和生命周期。但过了这个坎,就能体会到它的魅力:

  1. 编译器帮你找 bug
  2. 没有空指针异常
  3. 性能接近 C/C++
  4. 工具链完善

给前端同行的建议:不要想着快速上手,慢慢啃,理解原理。Rust 改变的是思维方式。