Skip to content

变量与类型

本章把 Rust 的「语法地基」一次铺完:绑定(binding)与可变性、标量与复合类型、类型转换、函数与返回值、语句/表达式的区分、全部流程控制,以及模式匹配(pattern matching)。前置知识:环境搭建与工具链(会写 cargo new、会读 rustc 报错)。

本章目标

  • 能说清 letlet mut、遮蔽(shadowing)与 const/static 各自的适用场景,并解释它们为什么不是同一种东西。
  • 能默写整数/浮点/bool/char 的类型名、位宽与默认类型,能主动选择 wrapping_*/checked_*/saturating_*/overflowing_* 处理溢出。
  • 能区分元组、数组 [T; N]、切片 &[T]String&str 的存储位置与所有权含义。
  • 能写出带参数、带返回值、带发散(diverging)分支的函数,并理解「最后一个表达式即返回值」与分号陷阱。
  • 能把 if/match/块当表达式使用,并用 loopbreak value、标签(label)与 for 的三种迭代形式写出地道循环。
  • 能用 match/if let/let else/matches! 完成穷尽的分支处理,并看懂 non-exhaustive patterns 类报错。

let 与可变性:默认不可变的设计

Rust 的绑定默认不可变(immutable)。这不是「建议」,而是类型系统的一部分:没有 mut 就无法取得可变权限。

rust
fn main() {
    let x = 5;
    println!("{x}");
    // x = 6; // 取消注释:error[E0384]: cannot assign twice to immutable variable `x`
}

💡 对照:Python/JS 里 x = 5 之后随便改;Java 要写 final int x = 5;Go 的变量一直可写。Rust 反过来——默认冻结,需要时才解冻

mut 与遮蔽的本质区别

这是初学者最容易混淆的一对概念。

rust
fn main() {
    // 方式 A:mut —— 同一块内存/同一个绑定被写入,类型不能变
    let mut a = 1;
    a += 1;
    // a = "text"; // error[E0308]: mismatched types:mut 不改变类型

    // 方式 B:遮蔽 —— 造一个全新绑定,旧绑定被「遮住」,类型可以变
    let b = 1;
    let b = b + 1;        // 新绑定,类型仍是 i32
    let b = b as f64;     // 再遮蔽一次,类型变成 f64
    let b = format!("{b}"); // 又变成 String

    println!("{a} {b}");
}
维度let mut xlet x = ...;let x = ...
绑定的数量1 个多个(旧的被遮蔽,仍在作用域末尾按序析构)
类型能否改变不能
能否取 &mut只有最新绑定可写,且需要 mut
语义「这个值之后会被改写」「我换了一个值/换了含义」
典型用途累加器、缓冲区、循环状态类型转换、把 Option 解开、改变变量的「含义」

🧠 原理:遮蔽不是赋值,而是新绑定。旧绑定仍然存在,只是名字被覆盖,它会在作用域结束时被析构(drop)。因此在〈所有权〉一章会看到:let s2 = s1;String移动(move),而 let mut s2 = s1; 只是把同一个值搬到了可写的绑定上。

遮蔽的作用域也遵循块规则:

rust
fn main() {
    let n = 10;
    {
        let n = n * 2; // 只在这个块内生效
        println!("inner: {n}"); // inner: 20
    }
    println!("outer: {n}"); // outer: 10
}

⚠️ 陷阱:遮蔽会让后面的变量悄悄换类型,长函数里连续遮蔽 let data = ... 会让读者迷惑。社区惯例是:只在紧接着的一两行内遮蔽,并且让新值语义上仍是「同一个概念」(例如 let input = input.trim();)。

常量 const 与静态变量 static

rust
const MAX_LEVEL: u8 = 10;              // 编译期常量,必须标注类型
static APP_NAME: &str = "demo";        // 静态变量,有固定地址
static mut COUNTER: u32 = 0;           // 可变静态变量:危险,见下

fn main() {
    // const 在字面量、数组长度、类型参数里都能用
    const N: usize = 4;
    let arr = [7u8; N];

    println!("{MAX_LEVEL} {APP_NAME} {arr:?}");

    // Rust 2024:访问 static mut 必须用 unsafe,且不能随便造引用
    unsafe {
        COUNTER += 1;
        let snapshot = *&raw const COUNTER; // 用裸指针(raw pointer)读,避免静态可变引用
        println!("{snapshot}");
    }
}

输出:10 demo [7, 7, 7, 7]1

维度conststaticlet
求值时机编译期(const 上下文)运行期启动时初始化运行到该行时
是否有固定地址没有(每个使用点内联,可能有多份副本)有(整个程序一份)有(栈或所属对象内)
能否取引用只能引用临时副本&'static T 合法受生命周期限制
可否可变只有 static mut(需 unsafelet mut
典型用途魔法数字、配置上限全局只读表、AtomicUsize 计数器、FFI局部数据

为什么 static mut 危险:它带来的是全局可变状态,任何线程都能无同步地读写,编译器的借用检查(borrow checking)对它完全不起作用——检查只在函数内部做数据流分析,无法跨线程追踪谁在何时写了它。结果是数据竞争(data race),也就是未定义行为(undefined behavior)。Rust 2024 edition 把「创建 static mut 的引用」升级为硬错误static_mut_refs lint 默认 deny),正是为了逼你改用 AtomicU32Mutex<T>OnceLock<T>

🚀 进阶:需要全局可变状态时,正确顺序是——先问「能不能用 const/static 只读」→ 再考虑 AtomicUsize(只需计数/标志)→ 需要复合数据用 Mutex<T>/RwLock<T>(可能配合 OnceLock<T> 做懒初始化)。

命名规范

Rust 编译器会用 non_snake_casenon_upper_case_globals 等 lint 主动纠正你:

对象规范例子
变量、函数、参数、模块、字段snake_casetotal_priceparse_level
类型、traitenum 变体UpperCamelCaseHttpClientCoin::Quarter
常量、staticSCREAMING_SNAKE_CASEMAX_LEVELDEFAULT_TIMEOUT_MS
泛型参数单个大写字母或 UpperCamelCaseTKVAlloc
生命周期参数短小写,常为 'a'a'de
snake_case!println!matches!
未使用但必须绑定的值前缀 __unused_guard
解构时忽略的字段单个 _let (_, b) = pair;

⚠️ 陷阱_x_ 不等价。_x绑定并立即析构(值还在,只是不警告);_不绑定,对 let _ = value; 而言值会被立刻丢弃。〈所有权〉一章讲 RAII 时这个差别很关键。


标量类型:整数、浮点、boolchar

标量(scalar)指不可再分的单值类型。Rust 一共四种:整数、浮点数、boolchar

整数

有符号无符号位宽取值范围(有符号)取值范围(无符号)
i8u88−128 ~ 1270 ~ 255
i16u1616−32768 ~ 327670 ~ 65535
i32u3232±约 21.4 亿0 ~ 约 42.9 亿
i64u6464±约 9.22×10¹⁸0 ~ 约 1.84×10¹⁹
i128u128128±约 1.7×10³⁸0 ~ 约 3.4×10³⁸
isizeusize指针宽度(64 位平台即 64 位)随平台随平台
rust
fn main() {
    let a = 42;              // 默认 i32
    let b: u8 = 200;
    let c = 5_000_000_000i64; // 超出 i32 必须显式后缀
    println!("{} {} {}", a, b, c);

    // usize 是「与指针同宽」的整数,索引、长度、容量一律用它
    let len: usize = [1, 2, 3].len();
    println!("{len}");
}

💡 对照:Java 的 int 固定 32 位、long 64 位,没有无符号;Python 整数是任意精度(想要溢出还溢不出);C 的 int 宽度由实现决定。Rust 的做法是宽度写进类型名,且 usize 明确表示「用于索引和内存大小的整数」,让「索引必须和指针同宽」这件事显式化。

浮点、boolchar

rust
fn main() {
    let f = 1.5;            // 默认 f64
    let g: f32 = 1.5;       // 需要省内存/对接图形 API 时才用 f32
    let ok = true;
    let letter = '中';       // char 是 Unicode 标量值,不是字节
    let emoji = '🦀';

    println!("{f} {g} {ok} {letter} {emoji}");
    println!("{} {}", size_of::<char>(), size_of::<f32>()); // 4 4
}
  • f32:单精度,有效十进制数字远少于 f64f64:双精度,是默认选择。Rust 不隐式提升 f32f64,混用直接报类型错误。
  • bool 只有 true/false不能和整数互转(没有 1 == true 这回事)。
  • charUnicode 标量值(Unicode scalar value),占 4 字节,覆盖 U+0000..=U+D7FFU+E000..=U+10FFFF;它不是「一个字节」,也不能直接当字符串用。用单引号 'a'char,双引号 "a"&str

🧠 原理:为什么 char 要 4 字节?因为 Rust 承诺 char 一定是合法的 Unicode 标量值,任何 char 都能安全地编码进 UTF-8。这换来两件事:字符串按字节索引会得到「半个字符」的风险被消灭(&s[0] 直接编译不通过),代价是 char 比 C 的 char 大 3 倍。

整数溢出:debug panic、release 环绕

rust
fn main() {
    let x: u8 = 255;
    println!("{}", x + 1); // cargo run(debug)会 panic:attempt to add with overflow
}

⚠️ 陷阱x + 1cargo run(debug)下 panic,在 cargo run --release 下会环绕(wrap)0。这是 Rust 的刻意设计:开发期把错误暴露出来,发布期不让它变成未定义行为。

模式溢出时的行为
cargo run / cargo test(debug,overflow-checks = truepanic:attempt to add with overflow
cargo run --release(默认 overflow-checks = false二进制补码环绕(wrapping),不是 UB
编译期常量表达式(如 let x: u8 = 300;直接编译错误:literal out of range for u8

想要明确语义就别依赖模式开关,用四种显式 API:

rust
fn main() {
    let m: u8 = 250;

    // 1) checked_*:溢出返回 None,绝不 panic
    println!("{:?}", m.checked_add(10)); // None

    // 2) wrapping_*:二进制补码环绕
    println!("{}", m.wrapping_add(10)); // 4

    // 3) saturating_*:钉在类型的最大值/最小值
    println!("{}", m.saturating_add(10)); // 255

    // 4) overflowing_*:返回 (结果, 是否溢出)
    let (v, overflowed) = m.overflowing_add(10);
    println!("{v} {overflowed}"); // 4 true
}

🧠 原理checked_* 返回 Option<T>,所以在 const 上下文不可用;saturating_* 适合音频增益、颜色分量这类「夹紧」语义;overflowing_* 是实现大整数/加密算法的原语。记住判断口径:需要「绝不 panic 且不想环绕」就 checked_*,需要「数学上等价于取模 2ⁿ」就 wrapping_*

⚠️ 陷阱250u8.wrapping_add(10) 不会 panic,但 (250u8 + 10).wrapping_add(0) 会——因为括号里的 + 先在 debug 下 panic 了。wrapping_*方法级保证,不会传染给表达式里的 +


复合类型:元组、数组、切片与字符串初探

复合类型把多个值组合起来。Rust 的两大基础复合类型是元组(tuple)数组(array)

元组

rust
fn main() {
    let t: (i32, char, f64) = (1, 'a', 2.5);

    // 解构(destructuring)
    let (p, q, r) = t;
    // 按索引访问
    println!("{} {} {} {}", t.0, p, q, r);

    // 单元类型 ():零个元素的元组 = 「没有有意义的值」
    let unit: () = ();
    // 返回 () 的函数
    fn nothing() {}
    nothing();
    println!("{unit:?}");
}

输出:1 1 a 2.5()

💡 对照:Python 的 tuple 是「不可变列表」,可以循环、能变长;Rust 元组是定长异构的记录,不能遍历,元素类型可以各不同。JS 没有真正的定长异构类型(数组可以塞任何东西)。Rust 里需要「有名字的元组」时应改用结构体(struct),可读性更好——user.0 远不如 user.id

数组 [T; N]、切片 &[T]

rust
fn main() {
    let arr: [i32; 4] = [1, 2, 3, 4];
    let zeros = [0u8; 4];            // 重复表达式:4 个 0
    println!("{} {:?}", arr.len(), zeros);

    // 索引越界:运行时 panic(编译期能算出来就直接报错)
    println!("{}", arr[3]);
    // println!("{}", arr[4]); // panic: index out of bounds: the len is 4 but the index is 4

    // 切片:指向连续内存的「视图」,不含长度以外的元数据
    let part: &[i32] = &arr[1..3];
    println!("{part:?}");           // [2, 3]
    println!("{}", part.len());     // 2

    // &str 就是「指向 UTF-8 字节」的切片
    let text: &str = "hello";
    let head: &str = &text[0..2];
    println!("{head}");             // he
}
维度数组 [T; N]切片 &[T] / &strVec<T> / String
长度类型的一部分,编译期已知运行期确定(胖指针:指针 + 长度)运行期确定,可增长
存储变量就在栈上(或内联在结构体里)只是借用,不拥有数据数据在堆上
能否改变长度不能不能(视图)能(push/pop/truncate
所有权拥有不拥有(借来的)拥有,丢弃时释放
典型用途定长缓冲、查表函数参数(接受的输入形式的并集)动态数据集

🧠 原理:切片是胖指针(fat pointer)——两个字(word):地址 + 长度。所以 size_of::<&[i32]>() == 16(64 位平台)。这也是为什么给函数传 &[T] 比传 &Vec<T> 更好:&[T] 能同时接受数组、Vec、其他切片,而 &Vec<T> 只能接受 Vec

索引与切片为什么会 panic:Rust 的 a[i] 会插入边界检查(bounds check)。这是 Rust 安全承诺的代价之一——换来的是「绝不可能因为索引越界读到别的内存」。编译器通常能把循环里的检查优化掉(消除边界检查),所以地道的写法是 for v in &arr 而不是 for i in 0..arr.len() { arr[i] }

String vs &str:先认识字面量的身份

rust
fn main() {
    let literal: &'static str = "hello";     // 字面量:编译进二进制,类型是 &'static str
    let owned: String = String::from("hi");  // 堆上分配的、可增长的字符串
    let also_owned: String = "hi".to_string();

    println!("{literal} {owned} {also_owned}");
    println!("{}", owned == also_owned);     // String 之间可比较
    println!("{}", owned.as_str() == "hi");  // 借用成 &str 再比较
}

输出:hello hi hitruetrue

本章只需要记住三条:

  1. &str借用(一个胖指针),String拥有(堆缓冲区 + 长度 + 容量)。
  2. 想从借用变拥有:.to_string()String::from(...);想从拥有变借用:.as_str()&s
  3. 比较时借用类型可以混搭:owned == also_ownedStringString)和 owned.as_str() == "hi"&str&str)都能直接写,因为 PartialEq 把这些组合都实现好了。

🚀 进阶String/&str 的完整规则(UTF-8 边界、Deref 强制转换、format!)留到 所有权与借用与字符串专章。


类型转换:asFrom/IntoTryFromparse

Rust 没有隐式数值提升i32 + i64u8 as i32 必须显式。这条规则消灭了 C 的整型提升陷阱和 JS 的 "1" + 1 == "11"

rust
fn main() {
    let a: i32 = 7;
    let b: i64 = 1000;
    // let c = a + b;                 // error[E0308]: mismatched types
    let c = i64::from(a) + b;         // 无损且显式
    println!("{c}");

    let n: u32 = "42".parse().expect("不是合法数字"); // parse 返回 Result
    println!("{n}");

    let f = "3.5".parse::<f64>().expect("不是合法浮点数");
    println!("{f}");

    let small = u8::try_from(n);      // 可能失败:返回 Result<u8, TryFromIntError>
    println!("{small:?}");            // Ok(42)
    let too_big = u8::try_from(300u32);
    println!("{too_big:?}");          // Err(TryFromIntError(()))
}

as 的截断与环绕语义

rust
fn main() {
    println!("{}", 300u32 as u8);      // 44:取低 8 位,静默截断
    println!("{}", -1i32 as u32);      // 4294967295:二进制补码环绕
    println!("{}", 3.99f64 as i32);    // 3:向零截断
    println!("{}", 1000i64 as i8);     // -24:截断 + 环绕
}

⚠️ 陷阱as 永远不会失败、永远不会 panic,也默认不会被 lint 拦下x as u8 是「我知道可能丢数据」的强断言,不是安全网。要避免静默丢数据,用 u8::try_from(x)x.try_into()checked_*。另外 f64 as i32 在溢出时(如 1e30 as i32)会饱和i32::MAX,这是浮点转整数特有的规则。

rust
fn main() {
    let v: i32 = 300;
    let safe: u8 = u8::try_from(v).unwrap_or(u8::MAX);
    println!("{safe}"); // 255
}

说明:教学示例里可以用 unwrap_or/expect 保持简短,但生产代码中要按语义选择——能给出兜底值用 unwrap_or/unwrap_or_else,要向上传播错误用 ?(见 错误处理)。

From/Into/TryFrom 的分工

rust
fn main() {
    let from = u32::from(7u8);        // From:保证成功
    let into: u32 = 7u8.into();       // Into 由 From 自动提供
    println!("{from} {into}");

    // TryFrom:可能失败,返回 Result
    let ok: Result<u8, _> = u8::try_from(200u32);
    let bad: Result<u8, _> = u8::try_from(2000u32);
    println!("{ok:?} {bad:?}");
}

输出:7 7Ok(200) Err(TryFromIntError(()))

转换方式失败可能失败时行为适用场景
x as T静默截断/环绕/饱和位运算、哈希、已证明安全的窄化
T::from(x) / x.into()不可能失败无损拓宽(u8u32&strString
T::try_from(x) / x.try_into()返回 Result有损或不确定的转换
s.parse::<T>()返回 Result从文本解析

🧠 原理From/Into/TryFromtrait(类似 Java 的接口、Python 的协议)。因为 impl From<A> for B 自动给出 impl Into<B> for A,所以实现方只需写 From,调用方两种都能用。这套机制让「转换」变成可被泛型约束的能力(fn f<T: Into<String>>(x: T)),这是 Rust 泛型的日常用法。

字面量写法:下划线、进制、指数、字节字面量

rust
fn main() {
    let dec = 1_000_000;      // 下划线只是可读性分隔符,对值无影响
    let hex = 0xff;
    let oct = 0o755;
    let bin = 0b1010_1010;
    let exp = 1e3;            // 1000.0(浮点)
    let byte = b'A';          // u8 的 65
    let ch = 'A';             // char
    let ws = b'\n';           // 字节字面量支持转义

    println!("{dec} {hex} {oct} {bin} {exp} {byte} {ch} {ws}");
}

输出:1000000 255 493 170 1000 65 A 10

⚠️ 陷阱1e3f641000.0,不是整数 1000;b'A'u8 的 65,不能和 char 混用(b'A' == 'A' 编译不通过)。


延伸阅读


内容以 rustc 1.98.1 · Rust 2024 edition 为基准