起步:先把地基和你会的那门语言对齐

语法速通:给已经会编程的人

这一篇怎么读

runoob 那套 Rust 教程用七篇讲基础语法(「运算符」「注释」各占一篇)。 对你来说那七篇里有六篇是在复述你早就会的东西。

所以这里只列差异,按「你会下意识写错的顺序」排。 读完你应该能看懂任意一段 Rust 代码的骨架,剩下看不懂的一定是所有权, 那是下一章的事。

变量与类型

let a = 42;          // 不可变,类型推断成 i32
let mut b = 42;      // 可变
let c: f64 = 42.0;   // 显式标注
let d = 42u8;        // 后缀

三件事和你会的那门不一样:

  1. 默认不可变。 要改就得写 mut,编译器还会在你写了 mut 却没改时警告。
  2. 整数要选宽度:i8/i16/i32/i64/i128/isize 和对应的 u 系列。 不写就是 i32。
  3. 不做隐式数值转换 —— 见《为什么是 Rust》。

类型推断很强,但推不出来时会明确要求你说:

let d = "42".parse::<i64>().unwrap();   // turbofish 指定
let d: i64 = "42".parse().unwrap();     // 或者标注左边

不给任何线索就编译不过: parse 能解析成任何实现了 FromStr 的类型,编译器没法替你挑一个。

error[E0284]: type annotations needed
  |     let d = "42".parse().unwrap();
  |         ^        ----- type must be known at this point

⇒ E0284 / E0282 这一类都是在说「这里我猜不出来,你说一个」。 ⚠️ 具体报哪个码取决于推断卡在哪一步 —— 这条实测报的是 E0284 (卡在关联类型 <_ as FromStr>::Err 上),而 let v = Vec::new(); 报的是 E0282。 记形状别记号。

基本类型速查:

Rust 对应
bool / char char 是一个 Unicode 标量值,实测 4 字节,不是一个字节
(i32, f64) 元组,.0 / .1 取值
[i32; 5] 定长数组,长度是类型的一部分
Vec<i32> 变长,堆上
String / &str 见切片那篇。实测 24 / 16 字节
() 单元类型,相当于 void,但它是个真的值

🚨 分号是有语义的

这是最容易踩的一条,因为它看起来只是排版。

块是表达式,最后一行不带分号就是块的值:

let x = {
    let a = 3;
    let b = 4;
    a * a + b * b      // ← 没有分号
};

加上分号,x 就变成了 (), 然后 println!("{x}") 报 E0277(() 不能打印)。

同一条规则解释了另外两件事:

控制流:全是表达式

let label = if n % 2 == 0 { "偶" } else { "奇" };

没有三元运算符。代价是两个分支类型必须一致 (见《为什么是 Rust》)。

三种循环各有分工:

写法 用途
loop { } 无限循环,break 出来 —— 只有它能 break 出值
while cond { } 带条件
for x in 可迭代 { } 遍历。没有 C 风格的三段式

loop 能 break 出值这一条值得单记:

let found = loop {
    n += 1;
    if n * n > 50 { break n; }      // ← 带值
};

「循环找第一个满足条件的东西」这个形状,在这里不需要临时变量加标志位。

🚨 for 的 & 加不加,差的是所有权:

for x in &v { }    // 借用,之后 v 还能用
for x in v  { }    // 移动,[之后 v 就没了](#ex-syntax-for-borrows-or-moves)

对照项去掉那个 & 之后,循环外面再用 v 就是 E0382。 这是从别的语言过来最容易撞的一下,因为那些语言里 for 从不夺走容器。

函数

fn add(a: i32, b: i32) -> i32 {
    a + b
}

签名里的类型一个都不能省 —— 参数和返回值都是。 函数内部的局部变量可以推,签名不行。

这是刻意的:签名是契约,改函数体不该悄悄改变调用方看到的东西。 (同样的理由也解释了生命周期那边 「编译器只看签名不看函数体」。)

📌 fn 可以嵌套定义在另一个 fn 里,但内层函数看不见外层的局部变量 —— 要捕获环境得用闭包。

注释与文档

// 普通注释
/* 块注释,可以嵌套 */

/// 文档注释,写在**它下面那个东西**上
/** 也是文档注释 */

//! 模块级文档注释,写在**文件开头**描述这个模块本身

⭐ /// 里的代码块会被 cargo test 真的编译并运行(文档测试)—— 这是 Rust 很特别的一点,测试那篇细说。

这一篇没讲什么

  • 所有权、借用、生命周期 —— 那是下一章的全部内容, 也是这套教程真正花篇幅的地方。
  • enum 与 match —— 它们在 Rust 里的分量远超「语法」,单独一章讲。
  • trait、泛型、闭包、宏 —— 各有各的位置。

如果你现在去读一段真实的 Rust 代码,看不懂的地方应该集中在 &、&mut、'a 和 ? 上。那正好是接下来两章。

下一步

先补上最后一块地基 —— 见《可变性、shadowing 与常量》: mut 到底修饰谁、shadowing 为什么不是「改变量」、以及 const 和 static 的区别。

全部篇目见 Rust 教程首页。

本篇示例

下面每一条都是完整的、能单独编译的程序,由npm run test:rust 在每次构建前用真的 rustc 跑一遍。 「编译不过、报 E0382」这种话在这里是被验证过的断言。 报错原文和对照项的结果由同一道闸门自动回写,会随工具链更新,但不作为断言。

类型能推就推,推不出来必须你说

fn main() {
    let a = 42;
    let b = 42u8;
    let c: f64 = 42.0;
    let d = "42".parse::<i64>().unwrap();
    println!("{a} {b} {c} {d}");
}

编译通过 · 输出 "42 42 42 42\n"

对照:去掉 `parse::<i64>` 里的类型
fn main() {
    let a = 42;
    let b = 42u8;
    let c: f64 = 42.0;
    let d = "42".parse().unwrap();
    println!("{a} {b} {c} {d}");
}

编译不过:error[E0284]

四种写法:让它推(默认 `i32`)、后缀 `42u8`、标注 `: f64`、turbofish `::<i64>`。对照项报 `E0282` —— `parse` 能解析成任何实现了 `FromStr` 的类型,而这里没有任何线索能定下来是哪个。⇒ 看到 `E0282` 就是在说「这里我猜不出来,你说一个」。

同样是「推不出来」,报的码可能不一样

fn main() {
    let v = Vec::new();
    println!("{}", v.len());
}

编译不过 · error[E0282]

rustc 原文
error[E0282]: type annotations needed for `Vec<_>`
 --> syntax-e0282-vs-e0284.rs:2:9
  |
2 |     let v = Vec::new();
  |         ^   ---------- type must be known at this point
  |
help: consider giving `v` an explicit type, where the type for type parameter `T` is specified
  |
2 |     let v: Vec<T> = Vec::new();
  |          ++++++++
对照:标注成 `Vec<i32>`
fn main() {
    let v: Vec<i32> = Vec::new();
    println!("{}", v.len());
}

编译通过,输出:"0\n"

这条报 `E0282: type annotations needed for \`Vec<_>\``,而 `"42".parse().unwrap()` 那条报的是 `E0284` —— 后者卡在关联类型 `<_ as FromStr>::Err` 上,属于「约束满足不了」而不是「类型未知」。⇒ **记形状别记号**:两者都是「这里我猜不出来,你说一个」。

块是表达式:最后一行**不带分号**就是它的值

fn main() {
    let x = {
        let a = 3;
        let b = 4;
        a * a + b * b
    };
    println!("{x}");
}

编译通过 · 输出 "25\n"

对照:给最后一行加个分号
fn main() {
    let x = {
        let a = 3;
        let b = 4;
        a * a + b * b;
    };
    println!("{x}");
}

编译不过:error[E0277]

🚨 **分号在 Rust 里是有语义的**:带分号 = 语句,值被丢掉,块的值变成 `()`。对照项于是把 `()` 塞给了 `x`,而 `()` 不能打印(`E0277`)。这也是函数体最后那行不写 `return` 也不写分号的原因。

`loop` 可以 `break` 出一个值

fn main() {
    let mut n = 0;
    let found = loop {
        n += 1;
        if n * n > 50 {
            break n;
        }
    };
    println!("{found}");
}

编译通过 · 输出 "8\n"

对照:把 `break n;` 改成 `break;`
fn main() {
    let mut n = 0;
    let found = loop {
        n += 1;
        if n * n > 50 {
            break;
        }
    };
    println!("{found}");
}

编译不过:error[E0277]

三种循环各有分工:`loop` 是「无限循环,用 `break` 出来」而且**只有它能 break 出值**;`while` 带条件;`for` 遍历。⇒ 「循环找第一个满足条件的东西」这个形状在 Rust 里不需要临时变量加标志位。

`for` 只有一种写法,但 `&` 加不加差很多

fn main() {
    let v = vec![10, 20, 30];
    let mut total = 0;
    for x in &v {
        total += x;
    }
    for i in 0..3 {
        total += i;
    }
    for (i, x) in v.iter().enumerate() {
        total += i as i32 * x;
    }
    println!("{total}");
}

编译通过 · 输出 "143\n"

对照:把第一个 `&v` 的 `&` 去掉
fn main() {
    let v = vec![10, 20, 30];
    let mut total = 0;
    for x in v {
        total += x;
    }
    for i in 0..3 {
        total += i;
    }
    for (i, x) in v.iter().enumerate() {
        total += i as i32 * x;
    }
    println!("{total}");
}

编译不过:error[E0382]

没有 C 风格的三段式 `for`,只有 `for x in 可迭代的东西`。🚨 但 `for x in v` 会**把 `v` 移动进循环**(对照项之后再用 `v` 就是 `E0382`),`for x in &v` 才是借用。范围 `0..3`、`enumerate()` 都是同一个形状。

函数签名里的类型一个都不能省

fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    println!("{}", add(1, 2));
}

编译通过 · 输出 "3\n"

对照:去掉返回类型 `-> i32`
fn add(a: i32, b: i32) {
    a + b
}

fn main() {
    println!("{}", add(1, 2));
}

编译不过:error[E0308]、error[E0277]

函数内部的局部变量可以让编译器推,但**签名必须写全** —— 参数类型和返回类型都是。这是刻意的:签名是契约,改函数体不该悄悄改变调用方看到的东西。对照项省掉 `-> i32` 之后返回类型就成了 `()`,而函数体最后一行还在返回 `i32`,当场 `E0308`。

几个类型到底占多少字节(64 位平台实测)

use std::mem::size_of;

fn main() {
    println!(
        "char={} String={} &str={} &i32={} Option<&i32>={} Option<i32>={}",
        size_of::<char>(),
        size_of::<String>(),
        size_of::<&str>(),
        size_of::<&i32>(),
        size_of::<Option<&i32>>(),
        size_of::<Option<i32>>(),
    );
}

编译通过 · 输出 "char=4 String=24 &str=16 &i32=8 Option<&i32>=8 Option<i32>=8\n"

对照:把两个引用换成值(看 `Option` 还能不能白嫖)
use std::mem::size_of;

fn main() {
    println!(
        "char={} String={} &str={} &i32={} Option<&i32>={} Option<i32>={}",
        size_of::<char>(),
        size_of::<String>(),
        size_of::<&str>(),
        size_of::<i32>(),
        size_of::<Option<i32>>(),
        size_of::<Option<i32>>(),
    );
}

编译通过,输出:"char=4 String=24 &str=16 &i32=4 Option<&i32>=8 Option<i32>=8\n"

三个数值得记:**`char` 是 4 字节**(一个 Unicode 标量值,不是一个字节);**`String` 在栈上占 24 字节**(指针 + 长度 + 容量,各 8);**`&str` 占 16**(指针 + 长度,没有容量 —— 它不拥有那块内存)。\n\n⭐ 最有意思的是后两列:`&i32` 和 `Option<&i32>` **一样大(都是 8)** —— 这叫**空指针优化**,编译器拿「引用不可能为空」这个事实,直接用全 0 表示 `None`,一个字节都不多花。\n\n⚠️ 而对照项把它换成值类型就白嫖不成了:`i32` 是 4,`Option<i32>` 是 8 —— 多出来的那 4 字节就是判别标记。**优化只对「有非法位模式可借用」的类型成立。**