起步:先把地基和你会的那门语言对齐
语法速通:给已经会编程的人
这一篇怎么读
runoob 那套 Rust 教程用七篇讲基础语法(「运算符」「注释」各占一篇)。 对你来说那七篇里有六篇是在复述你早就会的东西。
所以这里只列差异,按「你会下意识写错的顺序」排。 读完你应该能看懂任意一段 Rust 代码的骨架,剩下看不懂的一定是所有权, 那是下一章的事。
变量与类型
let a = 42; // 不可变,类型推断成 i32
let mut b = 42; // 可变
let c: f64 = 42.0; // 显式标注
let d = 42u8; // 后缀
三件事和你会的那门不一样:
- 默认不可变。 要改就得写
mut,编译器还会在你写了mut却没改时警告。 - 整数要选宽度:
i8/i16/i32/i64/i128/isize和对应的u系列。 不写就是i32。 - 不做隐式数值转换 —— 见《为什么是 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(() 不能打印)。
同一条规则解释了另外两件事:
- 函数体最后一行不写
return也不写分号 —— 写了分号返回类型就对不上。 return只在提前返回时才需要。
控制流:全是表达式
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 字节就是判别标记。**优化只对「有非法位模式可借用」的类型成立。**