第二章:基础语法与类型
Rust 的类型系统和语法设计处处体现着“安全”与“显式”的理念。这一章你将掌握变量绑定、基本类型、复合类型、函数定义以及所有基础控制流结构,它们是你写出任何 Rust 程序的基石。
2.1 变量与绑定
let 默认不可变,let mut 可变
Rust 使用 let 关键字声明变量。与许多语言不同,let 创建的绑定默认不可变——一旦赋值,就不能再修改。
let x = 5;
x = 6; // 编译错误:cannot assign twice to immutable variable
如果需要可变变量,显式加上 mut 关键字:
let mut y = 5;
y = 6; // 允许
这种设计让你在代码中一眼就能区分哪些值会变化,有助于写出更清晰、更安全的逻辑。
变量隐藏(shadowing)
你可以用同一个名字重新 let 一个新变量,新的绑定会隐藏之前的同名变量。这称为变量隐藏。shadowing 的作用域结束之前都可以使用新变量,而且类型也可以改变。
let spaces = " ";
let spaces = spaces.len(); // 隐藏后类型从 &str 变为 usize
这与 mut 不同:mut 改变的是值,但类型必须保持不变;shadowing 实际上是一个全新的变量,只是复用了名字。
常量与静态变量
常量 用 const 声明,必须显式标注类型,并且值必须在编译期可以确定:
const MAX_POINTS: u32 = 100_000;
常量可以在任何作用域(包括全局)声明,且永远不会改变。
静态变量 用 static 声明,同样要求类型注解,并拥有固定的内存地址:
static GREETING: &str = "Hello";
- 访问或修改可变静态变量(
static mut)需要在unsafe块中操作,因为编译器无法保证其线程安全。 - 一般优先考虑常量,除非你需要表示一个全局状态(并准备处理好安全问题)。
2.2 标量类型
Rust 是一门静态类型语言,编译期必须知道所有变量的类型。大多数情况下编译器能通过类型推断自动推导,但你仍需要理解每一种标量。
整数
| 类型 | 有符号 | 无符号 |
|---|---|---|
| 8 位 | i8 | u8 |
| 16 位 | i16 | u16 |
| 32 位 | i32(默认) | u32 |
| 64 位 | i64 | u64 |
| 128 位 | i128 | u128 |
| 指针大小 | isize | usize |
- 如果你不写类型,整数默认推断为
i32。 isize和usize依赖于目标机器的指针宽度(在 64 位系统上为 64 位),常用于索引和长度。
浮点数
f32:单精度浮点数。f64:双精度浮点数,默认推断为f64。
let x = 2.0; // f64
let y: f32 = 3.0; // f32
布尔与字符
bool只有true和false两个值,占一个字节。char表示单个 Unicode 标量值,占 4 个字节,可以表示中文字符、emoji 等。
let t = true;
let c = '🦀';
字面量与分隔符
Rust 提供多种字面量书写方式和可读性分隔符:
let decimal = 1_000_000; // 1000000,下划线纯为可读性
let hex = 0xff; // 十六进制
let octal = 0o77; // 八进制
let binary = 0b1111_0000; // 二进制
let byte = b'A'; // u8 字节字面量
2.3 复合类型
复合类型可以将多个值组合成一个类型。Rust 提供了两种原生复合类型:元组和数组。
元组(tuple)
元组是固定长度的异构集合,创建后大小不可改变,但每个位置可以是不同类型。
let tup: (i32, f64, char) = (500, 6.4, '🦀');
解构元组:
let (x, y, z) = tup;
println!("{y}");
也可以通过点号(.)加索引直接访问:
let first = tup.0;
let second = tup.1;
没有任何元素的空元组 () 被称为单元(unit),它的唯一值也是 (),常用于表示“没有返回值”。
数组(array)
数组是固定长度、同构的集合,所有元素类型必须相同,长度是类型的一部分。数组分配在栈上。
let arr: [i32; 5] = [1, 2, 3, 4, 5];
// 或者用重复表达式:
let zeros = [0; 10]; // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
访问数组元素使用 arr[index],越界访问会导致运行时的 panic(程序崩溃),Rust 会对数组访问进行边界检查。
切片(slice)
切片是某个连续序列的“视图”,它不拥有数据,只是对现有数据(如数组的一部分)的借用。切片的类型写作 &[T],长度可以动态变化。
let arr = [10, 20, 30, 40, 50];
let slice: &[i32] = &arr[1..4]; // 包含元素 20,30,40
切片本质上是一个胖指针,内部保存了指向起始元素的指针和长度。你可以对数组、Vec 等类型创建切片。
类型别名
使用 type 关键字为已有类型起一个新名字,这并不会创建新类型,仅增强可读性:
type Age = u32;
let my_age: Age = 30;
2.4 函数
函数定义
函数用 fn 声明,参数必须标注类型,返回值用 -> 指定类型:
fn add(x: i32, y: i32) -> i32 {
x + y // 无分号,表达式返回
}
Rust 不关心函数定义的位置,只要在作用域内可见即可。
表达式与语句
Rust 是一门基于表达式的语言。语句执行某些操作但不返回值(如 let x = 5;),而表达式计算并产生值。
函数体内最后一个表达式不加分号,它的值会被隐式返回。加了分号则变成语句,不会作为返回值。你也可以使用 return 提前返回。
fn five() -> i32 {
5 // 返回 5
}
fn early_return(x: i32) -> i32 {
if x > 0 {
return x;
}
0
}
函数指针
函数自身也有类型,可以用函数指针来传递。函数指针类型写作 fn(参数类型) -> 返回类型。
fn add_one(x: i32) -> i32 {
x + 1
}
fn apply(f: fn(i32) -> i32, val: i32) -> i32 {
f(val)
}
let result = apply(add_one, 5); // 6
发散函数
发散函数从不真正返回,其返回类型用 ! 表示,称为“never”类型。典型例子是 panic! 或 std::process::exit。
fn forever() -> ! {
loop {
// 无限循环,永不返回
}
}
因为 ! 类型可以强制转换为任何类型,你可以将发散表达式用在任何需要某类型的地方,比如:
let x: i32 = if true { 42 } else { panic!("error") };
2.5 控制流
if 表达式
Rust 中的 if 是表达式,可以产生值,因此可以直接把 if 放在 let 语句的右侧。每个分支返回的类型必须兼容。
let number = 6;
let description = if number % 2 == 0 {
"even"
} else {
"odd"
};
// description 的类型为 &str
条件表达式必须是 bool,Rust 不会隐式转换非布尔值为布尔,这与 C/C++ 根本不同。
loop 循环
loop 创建一个无限循环,直到显式 break。它也是一个表达式,可以通过 break 返回值。
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // loop 表达式将返回这个值
}
};
// result == 20
while 循环
while 在条件为 true 时持续执行,条件同样必须是 bool。
let mut n = 3;
while n != 0 {
println!("{n}");
n -= 1;
}
for 循环
for 用来遍历迭代器,这是 Rust 中最常用的循环方式。与很多语言不同,Rust 没有 C 风格的三段式 for,而是完全依赖迭代器。
let arr = [10, 20, 30];
for element in arr {
println!("{element}");
}
使用区间的常见写法:0..n 生成 0 到 n-1 的区间(左闭右开);0..=n 则包含 n。
for i in 0..5 {
println!("{i}"); // 0,1,2,3,4
}
对集合的引用迭代时,通常使用 .iter() 或 .iter_mut(),但 for 可以直接对数组或 Vec 的引用工作(因为存在 IntoIterator 实现)。
标签(label)
当嵌套循环时,你可以给循环贴上标签,然后在 break 或 continue 后跟上标签名,来跳出或继续外层循环。
'outer: for x in 0..5 {
for y in 0..5 {
if x + y > 6 {
break 'outer; // 直接跳出外循环
}
println!("{x}, {y}");
}
}
match 基本用法
match 是 Rust 中极为强大的控制流操作符,它允许将一个值与一系列模式比较,并执行匹配到的第一条分支。编译器强制要求穷尽所有可能,因此你不可能漏掉某个情况。
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"), // 下划线匹配所有其他值
}
match 也是表达式,每个分支可以返回一个值,并且所有分支的类型必须一致:
let number = 13;
let name = match number {
1 => "one",
2 => "two",
_ => "many",
};
match 的强大远不止于此,结合枚举和解构,它可以处理极其复杂的模式,后续章节将更深入探讨。
至此,你已经覆盖了函数与基本流程结构,这足以支撑你写出各类“计算型”小程序。下一章请阅读 所有权与生命周期。
参考文献
| 资料 | 说明 |
|---|---|
| Variables | 官方书第 3 章 |
| Data types | 标量与复合 |
| Control flow | 控制流 |
相关文章
第八章:测试、文档与质量
Rust 将测试和文档视为语言的一等公民。通过内置的测试框架,你可以在项目里编写单元测试、集成测试以及随文档一起运行的示例测试,三者共用 cargo test 命令。配合 cargo doc 生成文档,形成了一套确保代码质量与可维护性…
泛型与 Trait
泛型和 trait 是 Rust 实现代码复用与多态的两大支柱。前置:Rust 基础。泛型让代码可以工作在多种类型上而不牺牲性能,trait 定义了类型间的共享行为,二者结合形成了零成本抽象的强大表达能力。本章将深入泛型定义、trai…
第七章:模块系统与包管理
Rust 的模块系统为代码组织、封装和复用提供了一套严谨但灵活的机制。包、crate、模块以及 use 路径相互配合,让你能够把项目拆解成清晰的功能单元,同时精确控制哪些对外可见。本章将带你系统掌握这些构建大型 Rust 项目所必需的…
第十章:进阶特性与模式
Rust 的核心安全保证覆盖了绝大多数日常编程场景。但当你需要打破常规——无论是编写极致通用的抽象、与 C 库交互,还是内联优化——本章将带你进入 Rust 的深层能力:声明宏与过程宏、unsafe 的超能力与封装、高级类型系统技巧…
第五章:错误处理
Rust 将错误明确分为两类:不可恢复的错误与可恢复的错误。通过 panic! 处理前一种,Result 处理后一种,这让程序的错误路径不再是隐式的控制流,而是强类型、必须处理的代码分支。配合 Option 对缺失值的处理以及丰富的组…
第三章:所有权、借用与生命周期(核心)
所有权系统是 Rust 最独特、最核心的语言特性。它让 Rust 无需垃圾回收器就能保证内存安全,并在编译期消除数据竞争。本章你将深入理解所有权如何运转、引用和借用如何被检查、生命周期如何标注,以及常见智能指针如何扩展所有权模型。
Series
rust
10 / 10