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

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

为什么学 Rust

动机说明
性能Node.js 不擅长 CPU 密集型
工具链很多新工具用 Rust 写(Turbopack、Rome)
WASMRust 是 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

生态对比

领域JavaScriptRust
Web 框架Express、KoaActix、Axum
ORMPrisma、TypeORMDiesel、SeaORM
CLICommanderClap
测试Jest内置
包管理npmcargo

学习资源

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

学习曲线

总结

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

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

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