3. Rust 基础语法:变量与可变性、数据类型、函数、控制流、所有权与借用
好,咱们直接进入正题。Rust 的语法初看有点怪,但一旦你理解了它的设计哲学,就会觉得——嗯,真香。我个人习惯把这一章叫做「Rust 的硬核基本功」。说白了,后面的所有项目落地,都建立在这些概念之上。
核心要点:Rust 不是那种「你随便写写就能跑」的语言。它强迫你在编译时就思考清楚:这个变量能不能变?这个数据归谁管?这个引用会不会悬空?——这些约束,恰恰是它安全的根源。
3.1 变量与可变性
先聊变量。Rust 里变量默认是不可变的。对,你没看错。我第一次用的时候也愣了一下:什么?变量不能变?
let x = 5;
x = 6; // 编译错误!
为什么会这样?Rust 的设计者认为:默认不可变,能让代码更安全、更容易推理。你在读别人代码时,看到 let 就知道这个值不会变,省心很多。
那真要改怎么办?加个 mut 关键字:
let mut y = 5;
y = 6; // 没问题
我的习惯:能用不可变就用不可变。只有明确需要修改时,才加 mut。我在项目中遇到过好几次,因为滥用 mut 导致后续重构时搞不清数据流向,最后花了大半天追 bug。
还有个有意思的——隐藏(Shadowing)。你可以用同一个变量名声明新变量:
let x = 5;
let x = x + 1; // 新的 x,旧的被隐藏了
let x = "hello"; // 甚至可以改变类型
这和 mut 不同。隐藏是创建了一个全新的变量,旧的那个还在,只是被遮住了。我经常在需要临时转换数据时用这个技巧,比如把字符串转成数字再处理。
3.2 数据类型
Rust 是静态强类型语言。每个变量在编译时就必须知道类型。但别怕,编译器很聪明,大部分时候能自动推断。
标量类型
整数、浮点数、布尔、字符。这些和其他语言差不多,但 Rust 的整数类型特别多:i8, u8, i16, u16……一直到 i128。为什么搞这么复杂?
说白了,嵌入式开发、网络协议解析这些场景,对内存大小极其敏感。你想想看,一个 u8 只占 1 字节,而 i64 占 8 字节。选对了能省不少内存。
| 类型 | 长度 | 范围 |
|---|---|---|
i32 |
4 字节 | -2^31 到 2^31-1 |
u32 |
4 字节 | 0 到 2^32-1 |
f64 |
8 字节 | 双精度浮点 |
bool |
1 字节 | true / false |
char |
4 字节 | Unicode 标量值 |
注意:Rust 的 char 是 4 字节,不是 C 语言里的 1 字节。它支持 Unicode,所以一个 char 可以表示任何语言的字符。我曾经在解析用户输入时踩过这个坑——以为 char 是 ASCII,结果遇到中文直接崩了。
复合类型
元组和数组。元组可以放不同类型的数据:
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup; // 解构
println!("y 的值是 {}", y);
数组则要求所有元素类型相同,长度固定:
let arr: [i32; 3] = [1, 2, 3];
let first = arr[0];
数组越界?Rust 会在运行时检查。这一点比 C 安全得多。
3.3 函数
函数用 fn 关键字声明。Rust 使用蛇形命名法(snake_case):
fn add(x: i32, y: i32) -> i32 {
x + y // 注意:没有分号,这是表达式
}
这里有个关键区别:语句(Statement) 和 表达式(Expression)。语句执行操作但不返回值,表达式会计算并返回值。Rust 中,函数体最后一个表达式就是返回值。
避坑指南:我曾经在写函数时,不小心在返回值后面加了个分号。结果函数返回了 ()(空元组),而不是我期望的值。编译器会警告,但如果你不注意,很容易忽略。记住:有分号就是语句,没分号才是表达式。
函数也可以接受多个参数,甚至返回元组:
fn swap(x: i32, y: i32) -> (i32, i32) {
(y, x)
}
3.4 控制流
if 表达式在 Rust 里也是表达式,可以返回值:
let condition = true;
let number = if condition { 5 } else { 6 };
println!("number 是 {}", number);
注意:if 的两个分支必须返回相同类型。否则编译器会报错。
循环有三种:loop、while、for。
loop 是无限循环,可以用 break 带出值:
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("result 是 {}", result);
for 循环最常用,尤其是遍历集合:
let arr = [10, 20, 30];
for element in arr.iter() {
println!("值是 {}", element);
}
我的建议:能用 for 就别用 while 加索引。Rust 的 for 循环更安全,不会出现索引越界。我在代码审查时,看到手动索引的循环都会多看一眼。
3.5 所有权与借用
这是 Rust 最核心、也最独特的概念。很多初学者在这里卡住。别急,我们慢慢来。
所有权规则
- Rust 中每个值都有一个所有者(owner)
- 同一时间只能有一个所有者
- 当所有者离开作用域,值会被自动释放
看个例子:
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权转移给了 s2
// println!("{}", s1); // 编译错误!s1 已经失效
为什么?String 的数据在堆上。如果 s1 和 s2 都指向同一块内存,释放时就会 double free。Rust 通过所有权转移避免了这个问题。
我曾经:在写一个配置解析器时,把 String 传进函数后,还想在调用方继续用。结果编译器报错,我愣了半天才反应过来——所有权已经转移了。后来我学会了用借用(引用)来解决。
借用与引用
不想转移所有权?那就借用。用 & 符号:
fn calculate_length(s: &String) -> usize {
s.len()
}
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("{} 的长度是 {}", s1, len); // s1 还能用
引用默认也是不可变的。要修改借用的值,得用可变引用 &mut:
fn change(s: &mut String) {
s.push_str(", world");
}
let mut s = String::from("hello");
change(&mut s);
但有个限制:同一时间只能有一个可变引用,或者多个不可变引用。不能同时存在可变和不可变引用。
为什么这么设计?防止数据竞争。你想想看,如果两个线程同时修改同一个数据,结果会怎样?Rust 在编译时就杜绝了这种可能。我在写多线程网络服务时,这个特性帮我避免了好几次潜在的并发 bug。
切片(Slice)
切片是另一种没有所有权的数据类型。它引用集合中的一段连续元素:
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
切片本质上是一个胖指针:包含起始位置和长度。它不拥有数据,只是借用。
知识体系总览
下面这张图概括了本章的核心概念和它们之间的关系:
这张图把本章内容串起来了。你可以看到,所有权与借用是 Rust 的基石,其他所有特性都围绕它展开。理解了这个,后面的泛型、生命周期、智能指针都会轻松很多。
好了,这一章的内容就到这里。记住:变量默认不可变、所有权转移、借用规则——这三个概念是 Rust 的「三座大山」。翻过它们,后面的路就好走了。