Agent X-Ray
RuntimeNotesAbout
Notes/代码工程/Rust 深度教材/第2章

第2章:语言基础 —— 变量、类型、函数与流程控制

7 分钟 · 更新于 2026-09-01

第2章:语言基础 —— 变量、类型、函数与流程控制

本章通过一个“批次质量报告”程序串起基础语法。阅读重点不是记忆关键字,而是追踪值怎样被绑定、转换并流经表达式:哪些变化必须显式声明,哪些类型由上下文推导,哪些代码块会产生结果。


一、从一条数据流开始

先阅读一个完整但不复杂的程序:

rust
fn pass_rate(results: &[bool]) -> f64 {
    let passed = results.iter().filter(|ok| **ok).count();
    passed as f64 / results.len() as f64
}

fn level(rate: f64) -> &'static str {
    if rate >= 0.95 {
        "excellent"
    } else if rate >= 0.80 {
        "stable"
    } else {
        "needs-review"
    }
}

fn main() {
    let checks = [true, true, false, true, true];
    let rate = pass_rate(&checks);
    println!("rate={rate:.0%}, level={}", level(rate));
}

不要急着逐行背诵,先找出数据流:数组进入函数,函数借用数组并计算比例,if 产生分类文本,最后格式化输出。接下来的章节会拆开其中的绑定、类型、表达式、转换和控制流。

阅读基础语法的顺序 先问“这个表达式产生什么值”,再问“值是什么类型”,最后问“这个绑定之后是否允许改变”。这个顺序比按关键字背定义更容易迁移到真实代码。

二、变量绑定:默认不可变

rust
fn main() {
    let score = 90;
    println!("{score}");
}

默认情况下不能再次赋值:

rust
let score = 90;
score = 95; // 编译错误

确实需要改变时显式使用 mut

rust
let mut score = 90;
score += 5;
println!("{score}");

不可变默认值让变化变得可见。读到 let total = 100,可以假设后面不会重新赋值;读到 let mut total = 100,则知道后续逻辑会改变它。

mut 不允许变量随意换类型:

rust
let mut spaces = "   ";
spaces = spaces.len(); // &str 不能变成 usize

三、遮蔽:复用名字,创建新绑定

新的 let 可以遮蔽旧变量:

rust
fn main() {
    let input = " 42 ";
    let input = input.trim();
    let input: i32 = input.parse().expect("请输入整数");

    println!("解析结果:{input}");
}

三个 input 是三个不同绑定,因此可以从原始文本变为去空白文本,再变为整数。

方式新绑定可改变类型适合场景
let mut value同一含义的值持续变化
再次 let value同一概念经历转换步骤

作用域内部的遮蔽不会改变外层绑定:

rust
let x = 5;
{
    let x = x * 2;
    println!("内部:{x}");
}
println!("外部:{x}");

四、常量、忽略值与解构

常量使用 const,必须标注类型:

rust
const MAX_RETRIES: u32 = 3;
const TIMEOUT_MS: u64 = 5_000;

常量不能使用 mut,值必须能在编译期求出,名称通常使用全大写蛇形命名。

明确暂时不用的变量可加下划线;单独的 _ 表示忽略值:

rust
let _result = 42;
for _ in 0..3 {
    println!("执行一次");
}

let 还能解构复合值:

rust
let user = (1001_u64, "Alice", true);
let (id, name, active) = user;
println!("id={id}, name={name}, active={active}");

五、静态类型、推导与标注

Rust 必须在编译期知道每个值的类型,但通常可以推导:

rust
let count = 10;       // i32
let ratio = 0.75;     // f64
let enabled = true;   // bool
let letter = 'R';     // char

上下文不足时需要标注。例如字符串可以解析成多种数字:

rust
let port: u16 = "8080".parse().expect("端口必须是数字");
let backup = "8081".parse::<u16>().expect("端口必须是数字");

适合显式标注的场景包括:编译器无法推导、函数边界需要表达契约、数值范围有业务含义,或标注能明显提升可读性。

六、整数:先选范围,再选溢出策略

位数有符号无符号
8i8u8
16i16u16
32i32u32
64i64u64
128i128u128
指针宽度isizeusize

普通整数默认用 i32,集合长度与索引常用 usize。需要表达负数时不要选无符号类型。

rust
let population = 1_400_000_000_u64;
let mask = 0b1111_0000_u8;
let color = 0xff_u8;

Rust 不随意执行隐式数值转换:

rust
let small: u16 = 500;
let large: u32 = 1_000;
let total = u32::from(small) + large;

可能溢出时,应明确策略:

rust
let max = u8::MAX;
assert_eq!(max.checked_add(1), None);
assert_eq!(max.wrapping_add(1), 0);
assert_eq!(max.saturating_add(1), u8::MAX);

let (value, overflowed) = max.overflowing_add(1);
assert_eq!((value, overflowed), (0, true));
  • checked_*:溢出返回 None
  • wrapping_*:按补码环绕
  • saturating_*:限制在边界值
  • overflowing_*:返回结果和溢出标记

七、浮点、布尔、字符与单元类型

Rust 有 f32 与默认的 f64。二进制浮点数无法精确表示许多十进制小数:

rust
let actual = 0.1_f64 + 0.2;
let expected = 0.3_f64;
assert!((actual - expected).abs() < 1e-10);

不要草率地用 == 比较计算结果。货币通常应使用最小货币单位的整数,或专门的十进制定点库。未定义运算可能产生 NaN,可用 .is_nan() 检查。

bool 只有 truefalseif 条件必须产生布尔值,不能把整数或字符串隐式当作真假。

char 表示一个 Unicode 标量值,占 4 字节:

rust
let latin = 'R';
let chinese = '中';
let text = "Rust";

字符用单引号,字符串用双引号。

() 是单元类型。没有显式返回值的普通函数会返回它:

rust
fn log_message(message: &str) {
    println!("{message}");
}

八、数组与范围

数组长度固定,元素类型相同:

rust
let numbers = [10, 20, 30, 40];
let zeros = [0; 5];
println!("{} {}", numbers[0], zeros.len());

数组类型包含长度,例如 [i32; 4]

范围常用于循环:

rust
for i in 1..5 {      // 1、2、3、4
    println!("{i}");
}

for i in 1..=5 {     // 包含 5
    println!("{i}");
}

九、语句与表达式:分号会丢弃值

let 绑定是语句;算术、函数调用、if 和代码块可以是表达式。表达式求值后产生值。

rust
fn main() {
    let area = {
        let width = 4;
        let height = 6;
        width * height
    };

    println!("面积:{area}");
}

代码块最后的 width * height 没有分号,因此成为代码块的值。加上分号后,代码块返回 ()

分号不是纯格式 当函数提示“expected i32, found ()”时,优先检查尾表达式是否多了分号。

let 自身不是可继续赋值的表达式,因此不能写 let b = (let a = 8);

十、函数:参数显式,结果可以隐式返回

rust
fn add(left: i32, right: i32) -> i32 {
    left + right
}

fn main() {
    let result = add(20, 22);
    println!("{result}");
}

函数参数必须标注类型,-> i32 声明返回类型,最后一个无分号表达式作为返回值。

提前退出使用 return

rust
fn absolute(value: i32) -> i32 {
    if value < 0 {
        return -value;
    }

    value
}

常见风格是提前退出用 return,正常结果用尾表达式。永不返回的函数可标注 !

rust
fn stop(message: &str) -> ! {
    panic!("{message}");
}

十一、if 是表达式

rust
fn main() {
    let score = 86;

    let result = if score >= 90 {
        "优秀"
    } else if score >= 60 {
        "通过"
    } else {
        "未通过"
    };

    println!("{result}");
}

所有可能返回的分支必须类型兼容:

rust
let value = if condition { 1 } else { 2 };       // 正确
let value = if condition { 1 } else { "two" };   // 错误

Rust 不要求条件外加圆括号,但条件必须是 bool

十二、for:遍历集合的首选

rust
let values = [3, 5, 8, 13];
for value in values {
    println!("{value}");
}

需要索引时使用 .enumerate()

rust
let values = ["Rust", "Go", "Python"];
for (index, value) in values.iter().enumerate() {
    println!("第 {} 项:{value}", index + 1);
}

只重复固定次数且不使用计数值:

rust
for _ in 0..3 {
    println!("重试");
}

能直接遍历时不要先维护下标。手动索引更容易越界,也没有直接表达“访问每个元素”。

十三、whileloop 与循环控制

条件驱动的循环使用 while

rust
let mut remaining = 3;
while remaining > 0 {
    println!("{remaining}");
    remaining -= 1;
}

loop 表示无条件循环,还可以通过 break 返回值:

rust
let mut count = 0;
let doubled = loop {
    count += 1;

    if count == 10 {
        break count * 2;
    }
};

break 结束整个循环,continue 跳过本轮剩余代码:

rust
for number in 1..=6 {
    if number % 2 == 0 {
        continue;
    }
    if number > 5 {
        break;
    }
    println!("{number}");
}

遍历集合优先 for,条件循环用 while,需要无限循环或循环返回值时用 loop

十四、格式化输出

rust
fn main() {
    let language = "Rust";

    print!("正在学习 ");
    println!("{language}");

    let message = format!("欢迎进入 {language}");
    eprintln!("调试信息:{message}");
}
  • print!:输出但不换行
  • println!:输出并换行
  • format!:不打印,返回 String
  • eprintln!:输出到标准错误

{} 面向用户可读输出,{:?} 面向调试:

rust
let point = (3, 4);
println!("{point:?}");
println!("{point:#?}");

常用格式:

rust
let pi = 3.1415926;
let value = 27;
println!("{pi:.2}");
println!("{value:05}");
println!("{value:#b}");
println!("{value:#x}");

不必一次背完格式规则,先掌握普通输出、调试输出、精度和宽度。

十五、综合示例:成绩统计

rust
fn grade(score: u32) -> &'static str {
    if score >= 90 { "A" }
    else if score >= 80 { "B" }
    else if score >= 60 { "C" }
    else { "D" }
}

fn main() {
    const PASS_LINE: u32 = 60;
    let scores = [95, 72, 88, 59, 81];
    let mut total = 0_u32;
    let mut passed = 0;

    for (index, score) in scores.iter().enumerate() {
        total += score;
        if *score >= PASS_LINE {
            passed += 1;
        }

        let result = if *score >= PASS_LINE { "通过" } else { "未通过" };
        println!("学生 {}:{score} 分,等级 {},{result}", index + 1, grade(*score));
    }

    let mean = total as f64 / scores.len() as f64;
    println!("平均分:{mean:.1},通过人数:{passed}");
}

为什么迭代时会看到引用、为什么某些集合遍历后仍能使用,将在下一章通过所有权和借用解释。

十六、常见误区

  1. 所有变量都加 mut:只有确实需要改变时才加
  2. 混淆遮蔽与修改:再次 let 创建的是新绑定
  3. 尾表达式加分号:函数可能因此返回 ()
  4. 把整数当布尔条件:应明确写 count > 0
  5. 直接比较浮点计算结果:应使用符合业务精度的误差范围
  6. 遍历时先想到下标:优先直接迭代,需要索引再 .enumerate()
  7. as 随意缩窄:可能越界时优先 u8::try_from(value) 等可检查转换

十七、实践练习

练习一:运费分级

编写 shipping_fee(weight: f64, express: bool) -> f64。用 if 表达式计算基础费用,要求所有分支返回同一类型,并处理重量小于等于零的输入。

练习二:批次编号

遍历 100..=110:能被 3 整除输出 tri,能被 5 整除输出 penta,同时满足则输出 tri-penta,否则输出编号。先用 for,再尝试用 match 搭配元组布尔值改写。

练习三:统计有效值

给定 [12, -1, 8, 0, 21, -3],忽略负数,统计有效值数量、总和与最大值。要求最大值使用 Option<i32> 表达“可能不存在”。

练习四:分号实验

分别让代码块的尾部是 value + 1value + 1;,观察类型如何从整数变成 ()。写出你对“语句执行动作、表达式产生值”的解释。

练习五:显式数值策略

把一个 u16 与一个 u32 相加,并分别使用 checked_addsaturating_add 演示溢出策略。不要用未经说明的 as 缩窄转换。

十八、总结

  • let 创建绑定,默认不可变;mut 表示同一绑定的值会变化,遮蔽则创建新绑定。
  • Rust 要求类型在编译期确定,但会尽量从上下文推导;边界和业务范围不清时应显式标注。
  • 数值类型之间不随意隐式转换,溢出与缩窄都应选择明确策略。
  • 代码块、ifloop 都能产生值;尾表达式后的分号会把结果丢弃为 ()
  • for 适合遍历,while 适合条件循环,loop 适合显式退出或返回值。
  • 阅读 Rust 代码时,应同时追踪值、类型和可变性,而不是孤立识别关键字。

下一章把“值如何流动”推进到资源层面:赋值与传参何时复制、何时移动,引用又如何在不接管资源的情况下访问数据。

请继续阅读:第3章:所有权 —— 借用、切片与内存模型。


  • 第1章:开篇 —— Rust 为什么值得学与环境搭建
  • 第3章:所有权 —— 借用、切片与内存模型

本章目录
一、从一条数据流开始二、变量绑定:默认不可变三、遮蔽:复用名字,创建新绑定四、常量、忽略值与解构五、静态类型、推导与标注六、整数:先选范围,再选溢出策略七、浮点、布尔、字符与单元类型八、数组与范围九、语句与表达式:分号会丢弃值十、函数:参数显式,结果可以隐式返回十一、if 是表达式十二、for:遍历集合的首选十三、while、loop 与循环控制十四、格式化输出十五、综合示例:成绩统计十六、常见误区十七、实践练习十八、总结Related Documents
苏ICP备2025204887号-2