第 02 篇
变量与类型
学习变量、可变性和常用数据类型,拆解 let 绑定、类型推断、数字后缀和显式转换的规则。
Hex4C59 14 分钟阅读 更新于 2026-10-17
编程·原创·实战教程
上一节的程序只能打印固定文字。这一节用变量保存姓名和学习时长,计算还差多少分钟,再输出结果。学完后,你应该能写一个简单的费用或时长计算程序,并解释每个变量保存了什么。
继续使用第一节创建的 hello-rust 项目。标为“完整程序”的 Rust 代码块都可以替换整个 src/main.rs,保存后在项目目录运行 cargo run。各个示例相互独立,不要把几个 fn main 写在同一个文件里。
2.1 用 let 给数据起名字 #
先运行这个完整程序:
fn main() {
let name = "小林";
let minutes = 20;
println!("{name} 今天学习了 {minutes} 分钟");
}输出:
小林 今天学习了 20 分钟let name = "小林"; 建立一个名为 name 的变量绑定,之后可以通过这个名字使用对应的值。let minutes = 20; 保存一个整数。等号右边是要保存的值,左边是变量名称。
这里的 = 表示赋值或初始化,不表示“比较是否相等”。比较相等使用 ==,稍后会用到。
变量名通常采用英文小写字母和下划线,例如 total_minutes。名字区分大小写,minutes 和 Minutes 是两个名称。选择能说明用途的名字,比全部写成 a、b 更容易检查代码。
把 minutes 改成 35,再运行,确认输出的变化。只需修改变量的值,不必到每条输出中查找数字 20。
2.2 把变量和计算结果放进输出 #
println!("{minutes}") 使用当前可访问的 minutes,打印它的值。花括号必须写在字符串里。
完整程序:
fn main() {
let today = 20;
let yesterday = 30;
let total = today + yesterday;
println!("今天:{today} 分钟");
println!("昨天:{yesterday} 分钟");
println!("两天合计:{total} 分钟");
println!("直接计算也可以:{} 分钟", today + yesterday);
}合计输出为 50。最后一行的 {} 对应后面的表达式,Rust 先计算 today + yesterday,再把结果填进去。如果写了多个 {},后面的参数就按顺序填入。
"total" 是五个字母组成的固定文本,"{total}" 才会把变量的值填进去。第一次打印变量时,这个区别容易漏掉。
2.3 默认不能重新赋值,需要修改时加 mut #
下面的完整程序故意无法编译:
fn main() {
let minutes = 20;
minutes = 30;
println!("{minutes}");
}Rust 默认不允许给这个绑定重新赋值,通常报告 E0384。确实需要修改时,在声明中加 mut:
fn main() {
let mut minutes = 20;
println!("修改前:{minutes}");
minutes = 30;
minutes += 10;
println!("修改后:{minutes}");
}修复后的程序先输出 修改前:20,再输出 修改后:40。mut 表示允许修改,minutes += 10 等价于 minutes = minutes + 10。-=、*=、/= 也有类似含义。
不需要给所有变量都加 mut。变量不需要重新赋值时,就保留默认写法,让读代码的人也能看出这一点。编译器提示某个 mut 多余时,可以去掉它。
前面的错误例子也保存在下载包的 compile-fail/immutable.rs。在 rust-course 目录可运行:
rustc --edition 2024 compile-fail/immutable.rs
rustc --explain E0384第一条预期失败,第二条查看这种错误的解释。
2.4 类型说明一个值能怎样使用 #
每个值有类型。整数可以参与加减乘除,布尔值表示真或假,文本用来保存文字。编译器会检查操作是否适用于这些类型。
完整程序:
fn main() {
let temperature: i32 = -5;
let minutes: u32 = 30;
let average: f64 = 22.5;
let finished: bool = false;
let initial: char = '林';
let name: &str = "小林";
println!("温度:{temperature},分钟数:{minutes}");
println!("平均值:{average},完成了吗:{finished}");
println!("姓名:{name},姓氏:{initial}");
}冒号后的名称是类型标注。先掌握这些常用类型:
| 类型 | 表示什么 | 本节怎样选 |
|---|---|---|
i32 | 32 位有符号整数,可以为负 | 温度、整数练习;无其他约束的整数常推断为它 |
u32 | 32 位无符号整数,只能是非负值 | 分钟数、普通计数 |
usize | 与目标平台地址宽度对应的无符号整数 | 数组长度和索引 |
f64 | 双精度浮点数,用于近似小数 | 平均值、比例 |
bool | 真或假 | true 或 false |
char | 一个 Unicode 标量值 | 单引号里的 '林'、'A' |
&str | 只读地查看一段已有文本,不另建一份文本 | 本节用于双引号里的固定文本 |
&str 中的 & 涉及第四节的借用,也就是使用已有数据而不接管它;现在只需要会写固定文本。char 使用单引号,字符串使用双引号,'林' 与 "林" 是不同类型。
Unicode 是用于表示文字和符号的标准。char 表示其中的一个标量值,不一定等于一个人眼看到的完整字符;组合文字和某些表情的细节放到第五节。
不必为每个变量都写类型标注。像 let minutes = 20; 这样的声明,编译器往往能根据上下文推断类型。如果整数没有其他类型约束,通常采用 i32;带小数点的数值通常采用 f64。刚开始时,可以给需要明确范围的数字加上标注,方便检查。
mut 也不会让同一个绑定随意改变类型。把一个文本变量重新赋为整数,会得到类型不匹配错误。下载包中的 compile-fail/mismatched_types.rs 演示了这种情况。
2.5 算术运算与整数除法 #
常见运算符是 +、-、*、/ 和 %,分别表示加、减、乘、除和取余数。
完整程序:
fn main() {
let total_minutes = 125;
let hours = total_minutes / 60;
let minutes = total_minutes % 60;
println!("125 分钟 = {hours} 小时 {minutes} 分钟");
println!("整数除法:{}", 5 / 2);
println!("浮点除法:{}", 5.0 / 2.0);
println!("先加再乘:{}", (2 + 3) * 4);
}预期输出:
125 分钟 = 2 小时 5 分钟
整数除法:2
浮点除法:2.5
先加再乘:20整数除法向零截断,所以 5 / 2 得到整数 2。事后把结果转成小数,也补不回已经截掉的小数部分。需要小数结果时,参与除法的数值就应使用适当的浮点类型。
Rust 不会自动让任意两种数值类型混算。例如 u32 分钟数与 f64 目标比例参与同一运算时,需要明确转换。下面是完整程序:
fn main() {
let minutes: u32 = 30;
let target: u32 = 45;
let progress = f64::from(minutes) / f64::from(target) * 100.0;
println!("学习进度:{progress:.1}%");
}f64::from(minutes) 把这里的 u32 转成 f64,再进行浮点除法。输出为 学习进度:66.7%。:.1 表示输出时保留一位小数,不会改变原变量保存的值。
浮点数表示近似值,不能假定任意十进制小数的运算都精确无误。后面的金额练习会用整数“分”,暂时避开小数金额的舍入问题。
2.6 比较得到布尔值 #
完整程序:
fn main() {
let minutes = 30;
let target = 45;
let reached_target = minutes >= target;
let has_record = minutes > 0;
let needs_more = has_record && !reached_target;
println!("已达标:{reached_target}");
println!("有学习记录:{has_record}");
println!("有记录并且还需继续:{needs_more}");
}输出依次为 false、true、true。
== 比较相等,!= 比较不相等,>、>=、<、<= 比较大小。这些表达式的结果都是 bool。
&& 要求左右都为真,|| 要求至少一边为真,! 把真假反转。下一节会把这些条件交给 if,决定执行哪段代码。
2.7 同名的新绑定叫遮蔽 #
再次用 let 声明同名变量,会建立一个新绑定,称为遮蔽(shadowing)。完整程序:
fn main() {
let minutes = 20;
let minutes = minutes + 10;
println!("{minutes}");
let input = " 42 ";
let input = input.trim();
println!("去掉首尾空白:{input}");
}第二条 let minutes 先用旧值算出 30,再建立同名的新绑定,供后面的代码使用。第一个绑定不需要加 mut。
input.trim() 调用一个方法,取得去掉首尾空白的那段文本,不会修改原文本。新的同名绑定指向处理后的这段文本,后面解析输入时还会用到这种写法。
遮蔽也允许新绑定采用不同类型,因为这是重新声明。给已有的 mut 绑定赋值则必须保持兼容类型。两者的区别是有没有重新写 let。
不用为了练习遮蔽,把所有中间结果都起成同一个名字。用 total_minutes、remaining_minutes 分别表达不同含义,通常更便于检查计算。
2.8 花括号界定可以使用名字的范围 #
作用域就是可以使用这个变量名的代码范围。完整程序:
fn main() {
let minutes = 20;
{
let minutes = 30;
println!("里面:{minutes}");
}
println!("外面:{minutes}");
}程序先输出 30,再输出 20。内层的同名绑定只在内层起作用;离开那对花括号后,使用的仍是值为 20 的外层绑定。
如果一个变量只在内层声明,外层就不能直接使用它。后续写函数和分支时,也要留意声明位置。
2.9 用元组和数组放在一起 #
元组可以把几个不同类型的值放在一起。完整程序:
fn main() {
let record = ("小林", 30, true);
println!("姓名:{},分钟:{},已登记:{}", record.0, record.1, record.2);
let (name, minutes, registered) = record;
println!("{name},{minutes},{registered}");
}.0、.1、.2 按位置访问元组。let (name, minutes, registered) = record; 把各项分别绑定到名字,称为解构。
数组保存同一类型、固定数量的元素。完整程序:
fn main() {
let days: [u32; 3] = [20, 30, 25];
println!("第一天:{} 分钟", days[0]);
println!("第三天:{} 分钟", days[2]);
println!("一共记录了 {} 天", days.len());
}[u32; 3] 表示三个 u32 元素。这里方括号里的分号分隔元素类型和长度,不是语句结尾。days[0] 访问第一项,索引从 0 开始,所以第三项是 days[2]。
访问索引前要确认范围。运行时越界索引会 panic,停止正常执行;编译器能直接确定越界时,也可能提前拒绝编译。第五节会学习用 get 处理“不存在这一项”的情况。
语法细读:绑定、推断和数值转换 #
本章语法细读里的每个 Rust 程序可以分别替换自己项目的 src/main.rs:正常例子运行 cargo run,故意失败的例子运行 cargo check。下载包另保存为独立文件,直接编译的方法见下载包里的 syntax/README.md。各个程序相互独立。
前面的例子能运行,还需要解释每个位置为什么能这样写。下面按绑定的结构、类型约束和数值转换继续拆开。
let 的左边是模式,不一定只有一个名字 #
这是语法示意,方括号表示可以省略的部分,不是要输入的 Rust 字符:
let 模式 [: 类型] [= 初始化表达式];let minutes: u32 = 20; 中,minutes 是最简单的名字模式,冒号后约束整个绑定的类型,等号右边产生初始值,分号结束这条声明。let (hours, minutes) = (2, 5); 的左边是元组模式,一次建立两个名字;模式的详细规则放到第六章。
初始化表达式可以省略,但在代码读取变量之前,必须已经给它一个初始值。如果程序有几条可能执行的分支,每一条会读取变量的分支都要满足这个条件。let、初始化和之后的赋值不是一回事:首次初始化不可变绑定不需要 mut,重新赋值才需要。暂时优先在声明处初始化,避免把“某个分支没有赋值”带进简单程序。
const NAME: Type = expression; 声明常量,需要显式类型,并要求编译器在生成程序时就能算出初始值。常量不能加 mut,也不是省略 mut 的普通局部变量。本章只用整数常量;static 涉及固定存储位置,是另一个概念。
类型推断会看使用位置,不能只盯住等号右边 #
完整程序,下载包中为 syntax/syntax02-inference.rs:
const DEFAULT_MINUTES: u8 = 30;
fn accept(value: u8) {
println!("参数:{value}");
}
fn main() {
let inferred = 12;
let annotated: u8 = 12;
let suffixed = 12_u8;
accept(inferred);
let _: u8 = inferred;
println!("三种写法:{inferred} / {annotated} / {suffixed}");
println!("常量:{DEFAULT_MINUTES}");
}预期输出:
参数:12
三种写法:12 / 12 / 12
常量:30inferred 后来传给接收 u8 的函数,因此整数文字 12 在这里被约束为 u8。推断不是先把它永久定成 i32,再在调用处偷偷转换。第二种写法把约束写在变量上,第三种把类型后缀写在数字文字上;这三个绑定最终都是 u8。
编译器会一起检查声明、运算、函数参数和返回值等位置的约束。如果其他位置没有给出足够的约束,整数和浮点文字通常分别回退到 i32 和 f64;如果约束互相冲突,就报告错误。
以下完整程序故意无法编译,下载包中为 compile-fail/syntax02-reassign.rs:
fn main() {
let mut value = 20_u32;
value = "二十";
println!("{value}");
}预期诊断包含 E0308。
这里 mut 只允许重新赋值,不能取消已经确定的 u32 类型。遮蔽则会建立新绑定,所以 let value = "二十"; 可以采用新类型。
类型标注、类型后缀和 as 各做什么 #
| 写法 | 作用 |
|---|---|
let n: u8 = 12; | 约束绑定的类型 |
let n = 12_u8; | 约束这个数字文字的类型 |
value as u8 | 对已有表达式执行允许的显式转换 |
u32::from(value) | 使用这个类型提供的 From 转换 |
以下完整程序故意无法编译,下载包中为 compile-fail/syntax02-mixed.rs:
fn main() {
let small = 10_u8;
let wide = 20_u16;
let total = small + wide;
println!("{total}");
}预期诊断包含 E0308。
编译器不会在这次加法里自行选择把哪一边转换成另一种类型。可以先把 small 扩宽为 u16,再和 wide 相加;选择转换方向时也要考虑结果范围。
完整程序,下载包中为 syntax/syntax02-conversion.rs:
fn main() {
let wide = 300_u16;
let narrow = wide as u8;
let extended = u32::from(wide);
assert_eq!(narrow, 44);
assert_eq!(extended, 300);
println!("窄化:{narrow},扩宽:{extended}");
}预期输出:
窄化:44,扩宽:300整数从 u16 窄化为 u8 时,只保留低 8 位,截掉放不下的高位。后者的数值范围是 0~255;300 等于 256 加 44,截断后留下 44,所以不能把 as 当成范围校验。这里的 u32::from(wide) 是扩宽转换,保留原数值。需要“范围超出就报错”的转换时,应在学过第七章的 Result 后使用相应的 TryFrom,而不是先截断再检查。
自查:不运行程序,解释 let n = 12; accept(n); 中的类型如何确定,再说明把 let 改成赋值为什么不能改变类型。若要把 300 存进 u8,先写清需求是拒绝输入还是允许截断。
2.10 把本节内容组合起来 #
把姓名、学习时长、进度计算和数组记录放进同一个程序,就得到下面的例子。用它替换自己的整个 main.rs:
fn main() {
let name = "小林";
let mut minutes: u32 = 20;
minutes += 10;
let target: u32 = 45;
let remaining = target - minutes;
let finished: bool = minutes >= target;
let progress = f64::from(minutes) / f64::from(target) * 100.0;
let record = (name, minutes);
let days = [20, 30, 25];
println!("{} 今天学习了 {} 分钟", record.0, record.1);
println!("还差 {remaining} 分钟,完成了吗:{finished}");
println!("进度:{progress:.1}%");
println!("第一天:{} 分钟", days[0]);
let input = " 42 ";
let input = input.trim();
println!("去掉首尾空白:{input}");
}预期输出:
小林 今天学习了 30 分钟
还差 15 分钟,完成了吗:false
进度:66.7%
第一天:20 分钟
去掉首尾空白:42下载包中对应 basics/examples/ch02_variables.rs,在 rust-course 目录运行:
cargo run -p rust-basics --example ch02_variables先预测:把初始分钟数从 20 改成 25,哪些输出会改变?实际运行后,应看到学习 35 分钟、还差 10 分钟、进度 77.8%;days[0] 来自另一份数组,仍是 20。
如果把时长改得超过目标,当前 u32 减法 target - minutes 会下溢,因为结果不能表示为非负整数。整数类型有范围,不能无限加减;例如 u32 的范围是 0~4,294,967,295。
计算“还差多少”时,可以把那一行改成:
let remaining = target.saturating_sub(minutes);只替换这一行即可。这个方法会把低于 0 的差值限制为 0。如果需要判断是否发生溢出,可以使用 checked_sub;它的返回形式留到第六节学过 Option 后再解释。不要依赖整数溢出后的值,调试与 release 配置的默认检查行为可能不同。
2.11 练习一:把分钟换成小时和分钟 #
给定 total_minutes = 137,独立打印:
137 分钟 = 2 小时 17 分钟再把输入改成 60、59、0,分别预测结果。
参考答案:先自己完成再展开
fn main() {
let total_minutes: u32 = 137;
let hours = total_minutes / 60;
let minutes = total_minutes % 60;
println!("{total_minutes} 分钟 = {hours} 小时 {minutes} 分钟");
}输入 60 得到 1 小时 0 分钟,59 得到 0 小时 59 分钟,0 得到 0 小时 0 分钟。
2.12 练习二:计算商品总价 #
单价 1290 分,数量 3。用变量计算总分,再打印 总价:38.70 元。要求全程使用整数,不先转成浮点数。分不足两位时,在前面补零。
参考答案:先自己完成再展开
fn main() {
let price_cents: u32 = 1290;
let quantity: u32 = 3;
let total_cents = price_cents * quantity;
println!("总价:{}.{:02} 元", total_cents / 100, total_cents % 100);
}{:02} 表示至少两位,不足时补零。下载包中运行 cargo run -p rust-basics --example answer02。
再把单价改成 105 分、数量改成 1,预期 总价:1.05 元。用这个输入检查,就能发现是否把金额错印成了 1.5。
2.13 进入下一节前检查 #
尝试不看示例,写出一个程序:记录今天和昨天的学习分钟数,计算总和与平均值,判断总和是否达到 60,并打印结果。平均值用浮点除法。
你还应能解释 let 与 let mut 的区别,预测 5 / 2 与 5.0 / 2.0,区分 = 与 ==,说明三个元素的数组为什么不能访问第 3 号索引。
下一节将根据这些真假条件选择不同操作,再把重复的计算写成函数。
官方延伸阅读与验证 #
文档查阅与示例复核日期:2026-10-13。正文完整程序使用 macOS arm64、Rust 1.99.0、Cargo 1.99.0 验证;标为故意无法编译的例子按失败条件检查。
一起讨论
选中正文中的文字,可以针对这段内容发起讨论。
正在读取讨论…
在 GitHub 查看与管理讨论 ↗