Rust基础语法 —— 像搭积木一样学编程
语法是编程语言的”字母表”。在你能写出优美的Rust程序之前,得先认识这些基础的”积木块”。好消息是,如果你有其他编程语言经验,很多概念你已经见过了——只是Rust给它们加了一些独特的”规矩”。
📋 开篇自测:你已经知道多少?
- Rust中变量默认是不可变的,你知道为什么这样设计吗?
i32、u64、f64——你能猜出这些类型名称的含义吗?- Rust的
if表达式能返回值,你知道这意味着什么吗?
一、变量与可变性——Rust的”保守主义”
1.1 用let声明变量
在Rust中,声明变量用let关键字:
fn main() {
let city = "杭州";
let temperature = 28;
println!("{}今天气温{}度", city, temperature);
}
到这里一切都很正常。但如果你尝试修改变量的值:
fn main() {
let score = 85;
score = 90; // 编译错误!
println!("分数是:{}", score);
}
编译器会毫不留情地报错:cannot assign twice to immutable variable。
这就是Rust的第一条”规矩”:变量默认是不可变的(immutable)。
1.2 为什么默认不可变?
想象你在图书馆借了一本书。如果每个借阅者都能随意修改书的内容,等你还回来的时候,谁也不知道书里写的还是不是原来的东西。Rust的设计哲学是:数据默认是”只读”的,只有你明确表示”我需要修改它”时,才允许修改。
这不是给你添麻烦,而是帮你减少bug。在大型项目中,一个变量在你不知道的地方被偷偷改掉了——这是最难排查的bug之一。
1.3 用mut让变量可变
当你确实需要修改变量时,加上mut关键字:
fn main() {
let mut score = 85;
println!("期中成绩:{}", score);
score = 92;
println!("期末成绩:{}", score);
}
mut是mutable(可变的)的缩写。它像是一个明确的声明:“我知道这个变量以后会被改,这是有意为之的。“
1.4 变量遮蔽(Shadowing)
Rust有一个有趣的特性叫变量遮蔽——你可以用同名的新变量”盖住”旧变量:
fn main() {
let x = 5;
let x = x + 1; // 新的x遮蔽了旧的x
let x = x * 2; // 再次遮蔽
println!("x的值是:{}", x); // 输出12
}
遮蔽和mut有本质区别:遮蔽是创建了一个全新的变量(甚至可以改变类型),而mut是修改同一个变量的值。
fn main() {
let input = "42"; // 字符串类型
let input: i32 = input.parse().unwrap(); // 遮蔽:变成了整数类型!
println!("数值是:{}", input);
}
1.5 常量
如果一个值从头到尾都不会变,而且你想在编译期就确定它,可以用const:
const MAX_PLAYERS: u32 = 100;
const PI: f64 = 3.14159265358979;
常量必须标注类型,名字用全大写加下划线的风格,而且值必须是编译期就能确定的——你不能把一个函数调用的结果赋给常量。
🤔 想一想 在你之前用的编程语言中,变量默认是可变还是不可变的?如果默认不可变,会对你的编程习惯产生什么影响?
二、数据类型——Rust是个”类型控”
Rust是静态类型语言——每个变量在编译时都有确定的类型。不过大多数时候,编译器能自动推断类型,你不需要手动标注。
2.1 整数类型
Rust的整数类型名称非常直观:
| 类型 | 大小 | 范围 | 说明 |
|---|---|---|---|
i8 | 8位 | -128 ~ 127 | 有符号(i=integer) |
u8 | 8位 | 0 ~ 255 | 无符号(u=unsigned) |
i16 | 16位 | -32768 ~ 32767 | |
u16 | 16位 | 0 ~ 65535 | |
i32 | 32位 | 约±21亿 | 默认整数类型 |
u32 | 32位 | 0 ~ 约42亿 | |
i64 | 64位 | 约±9.2×10¹⁸ | |
u64 | 64位 | 0 ~ 约1.8×10¹⁹ | |
i128 | 128位 | 约±1.7×10³⁸ | |
u128 | 128位 | 0 ~ 约3.4×10³⁸ | |
isize | 平台相关 | 和CPU位数一致 | |
usize | 平台相关 | 常用于索引和长度 |
命名规则:i代表有符号整数,u代表无符号整数,后面的数字代表占多少位。简单粗暴,一目了然。
fn main() {
let age: u8 = 25; // 年龄不会是负数,用u8足够
let balance: i64 = -50000; // 余额可能是负数
let count = 42; // 编译器自动推断为i32
// 数字可以用下划线分隔,提高可读性
let population: u64 = 1_400_000_000;
println!("人口:{}", population);
}
2.2 浮点数类型
Rust有两种浮点数:f32(单精度)和f64(双精度,默认)。
fn main() {
let pi = 3.14159; // 默认f64
let temp: f32 = 36.5; // 明确指定f32
println!("圆周率:{},体温:{}", pi, temp);
}
2.3 布尔类型
fn main() {
let is_rust_fun: bool = true;
let is_easy = false; // 类型自动推断为bool
println!("Rust好玩吗?{}", is_rust_fun);
}
2.4 字符类型
Rust的char类型使用单引号,而且它支持Unicode——意味着你可以存储中文字符甚至emoji:
fn main() {
let letter = 'A';
let chinese = '龙';
let emoji = '🦀'; // Rust的吉祥物是螃蟹!
println!("{} {} {}", letter, chinese, emoji);
}
注意:char占4个字节(32位),因为它需要能表示任何Unicode字符。
2.5 元组(Tuple)
元组可以把多个不同类型的值打包在一起:
fn main() {
let person: (&str, u8, f64) = ("小明", 28, 175.5);
// 用点号加索引访问
println!("姓名:{},年龄:{},身高:{}", person.0, person.1, person.2);
// 解构(destructuring)
let (name, age, height) = person;
println!("{}今年{}岁,身高{}cm", name, age, height);
}
元组就像一个”临时打包袋”——当你需要把几个相关的值捆在一起传来传去时,它很方便。但如果这组数据有固定的结构和含义,用结构体(struct)会更好——这个我们后面会讲。
2.6 数组(Array)
Rust的数组是固定长度的,所有元素类型必须相同:
fn main() {
let weekdays = ["周一", "周二", "周三", "周四", "周五"];
let scores: [i32; 5] = [88, 92, 76, 95, 83];
println!("第一天:{}", weekdays[0]);
println!("第三科成绩:{}", scores[2]);
// 创建一个包含5个0的数组
let zeros = [0; 5]; // [0, 0, 0, 0, 0]
println!("零数组:{:?}", zeros);
}
如果你需要可变长度的数组,用Vec(向量),它是Rust中最常用的集合类型之一:
fn main() {
let mut fruits = vec!["苹果", "香蕉", "橙子"];
fruits.push("葡萄");
println!("水果篮:{:?}", fruits);
}
🤔 想一想 为什么Rust要区分
i32和u32、f32和f64这么多类型?在Python中一个int就搞定了。想想在嵌入式设备或高性能计算场景下,精确控制数据大小有什么好处?
三、函数——代码的”工具箱”
3.1 定义和调用函数
fn greet(name: &str) {
println!("你好,{}!欢迎学习Rust!", name);
}
fn add(a: i32, b: i32) -> i32 {
a + b // 注意:没有分号!这是返回值表达式
}
fn main() {
greet("小红");
let sum = add(3, 7);
println!("3 + 7 = {}", sum);
}
几个要点:
- 函数参数必须标注类型——Rust不会帮你猜参数类型
- 返回类型用
->箭头标注 - 函数体最后一个表达式(没有分号)就是返回值
- 如果加了分号,那就变成了语句,返回的是
()(空元组,类似其他语言的void)
3.2 表达式 vs 语句
这是Rust中一个微妙但重要的概念:
- 语句(statement):执行某个动作,不返回值。以分号结尾。
- 表达式(expression):计算并产生一个值。不以分号结尾。
fn main() {
// 这是语句
let x = 5;
// 花括号内部也可以是表达式
let y = {
let inner = 10;
inner * 2 // 没有分号 → 这个值(20)就是整个代码块的值
};
println!("y = {}", y); // y = 20
}
这就像做数学题:3 + 5是一个表达式,它”产出”了8这个值。而”把8写在纸上”是一个动作(语句)。
3.3 提前返回
虽然”最后一个表达式作为返回值”是惯用写法,但你也可以用return关键字提前返回:
fn check_age(age: u32) -> &'static str {
if age < 18 {
return "未成年";
}
"成年" // 最后一个表达式,等同于 return "成年";
}
四、控制流——让程序学会”做决定”
4.1 if表达式
fn main() {
let score = 85;
if score >= 90 {
println!("优秀!");
} else if score >= 70 {
println!("良好!");
} else if score >= 60 {
println!("及格");
} else {
println!("不及格,继续努力!");
}
}
Rust的if有一个超能力——它是表达式,可以直接返回值:
fn main() {
let age = 20;
let category = if age >= 18 { "成年人" } else { "未成年人" };
println!("你是{}", category);
}
这比三元运算符(condition ? a : b)更加清晰。注意:当if用作表达式时,两个分支的返回类型必须一致。
4.2 循环——loop、while、for
Rust提供了三种循环,各有用武之地:
loop:无限循环
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // break可以带返回值!
}
};
println!("结果:{}", result); // 输出20
}
loop和while true的区别在于:loop能通过break返回值,编译器也能更好地推断类型。
while:条件循环
fn main() {
let mut fuel = 100;
while fuel > 0 {
println!("剩余燃料:{}", fuel);
fuel -= 25;
}
println!("燃料耗尽!");
}
for:遍历循环(最常用!)
fn main() {
// 遍历数组
let cities = ["北京", "上海", "广州", "深圳"];
for city in cities {
println!("欢迎来到{}", city);
}
// 遍历范围
for i in 1..=5 { // 1到5,包含5
println!("第{}轮", i);
}
for i in 0..5 { // 0到4,不包含5
println!("索引:{}", i);
}
// 带索引的遍历
let scores = [88, 92, 76, 95];
for (index, score) in scores.iter().enumerate() {
println!("第{}科成绩:{}", index + 1, score);
}
}
for是Rust中最常用也最安全的循环——它不会出现数组越界的问题。
4.3 match——Rust的”超级switch”
match是Rust中最强大的控制流工具之一。它就像一个精密的分拣机器——把一个值和一系列模式进行匹配:
fn main() {
let grade = 'B';
let description = match grade {
'A' => "太棒了!顶尖水平!",
'B' => "不错!继续保持!",
'C' => "还行,有提升空间",
'D' => "危险了,加把劲",
'F' => "挂科了...",
_ => "无效的等级", // _ 是通配符,匹配所有其他情况
};
println!("{}", description);
}
match的一个硬性要求:必须穷尽所有可能性。如果你漏了某种情况,编译器不会放过你。这就像一个严格的收件员——每一封信都必须有去处,不允许有”不知道该放哪”的情况。
我们在后面讲枚举和模式匹配时,会看到match真正大显身手的场景。
⚠️ 常见误区
- 忘了
mut就尝试修改变量——这是新手最高频的编译错误。记住:Rust默认不可变,需要修改就加mut。- 搞混表达式和语句——函数最后一行如果加了分号,返回值就变成了
(),不是你期望的值。这个分号经常导致新手困惑。- 整数溢出与除零——在debug模式下,整数溢出会导致panic(程序崩溃)。比如
let x: u8 = 255; let y = x + 1;就会出错。但注意:在release模式下(cargo build --release),Rust会进行二进制补码回绕(类似C),溢出不会panic而是静默回绕。如果需要在所有模式下都控制溢出行为,可以使用checked_add、wrapping_add、saturating_add等方法。另外注意整数除以零在任何模式下都会panic,而浮点数除以零不会panic——1.0 / 0.0会得到Infinity,0.0 / 0.0会得到NaN。- 把
if当语句用却忘了else分支——当if用作表达式赋值时,必须有else分支,否则编译器不知道条件不满足时该返回什么。
📝 掌握度自测
- 变量:
let x = 5;和let mut x = 5;有什么区别?变量遮蔽和mut又有什么不同? - 类型:说出至少5种Rust的基本数据类型,并解释
usize通常用在什么场景。 - 函数:写一个函数
is_even(n: i32) -> bool,判断一个整数是否为偶数。注意函数体的最后一行不要加分号。 - 控制流:用
for循环打印1到100中所有能被3整除的数。 - 综合:写一个函数,接收一个温度(摄氏度,f64类型),返回对应的华氏温度。公式:
F = C * 9/5 + 32。在main函数中调用它,打印”25°C等于77°F”。
💡 自我评估
- 答对5题:基础语法已经掌握,准备好迎接Rust最核心的概念——所有权!
- 答对3-4题:再练习一下不熟悉的部分,特别注意表达式和语句的区别。
- 答对0-2题:不要着急,语法需要反复练习。建议打开编辑器,把每个代码示例都敲一遍运行一下。
购买课程解锁全部内容
内存安全 + 零成本抽象:Rust 系统编程实战
¥29.90