阶段一 · 起步与变量

变量,可变性与基础类型

一句话总结

Rust 变量默认不可变,要改得显式 mut;整数必须明确位宽且溢出会被检查;类型转换不会隐式发生,一律显式 asFrom.
这三点处处体现 Rust "显式优于隐式"的设计哲学,也是 Go 程序员最先要适应的地方.

变量默认不可变

第一个反直觉设定:let 声明的变量默认不可变(immutable),赋值后不能再改.

let x = 5;
x = 6; // 编译错误:cannot assign twice to immutable variable `x`

let mut y = 5;
y = 6; // 正常,y 用 mut 声明为可变

Go 里变量天然可变,要靠 const 才能锁定;Rust 正好相反--默认锁定,要改才声明 mut.这不是为难--它让"这个值会不会变"成为代码里一眼可见的信息,也和后面所有权,借用规则环环相扣.

最佳实践:能不可变就不可变

默认写 let,只在确实需要修改时才加 mut.这能让读代码的人(包括未来的自己)立刻知道哪些状态是稳定的.

Clippy 还会主动提示"声明了 mut 却从未修改"的多余可变性.

变量遮蔽(Shadowing)

Rust 允许用同名 let 重新声明,覆盖前一个变量--这叫遮蔽,它甚至能改变类型:

let spaces = "   ";        // &str 类型
let spaces = spaces.len(); // 重新绑定为 usize,合法

let x = 5;
let x = x + 1; // x 变成 6,这是一个全新变量
let x = x * 2; // x 变成 12

遮蔽与 mut 是两回事,别混淆:

mut 修改 遮蔽(shadowing)
本质 改同一个变量的值 用新变量覆盖旧名字
能否换类型 不能
典型用途 循环计数,状态累积 解析转换后复用同名,如 str → 数值

遮蔽像给旧名字"换了个人顶岗":名字没变,但背后是全新的变量(可以是不同类型).
mut 则是同一个人换了身衣服,人(类型)还是那个人.

const:编译期常量

不可变的 let 是运行期绑定;真正的"常量"用 const,它在编译期确定,必须标注类型,且只能用常量表达式初始化:

const MAX_POINTS: u32 = 100_000; // 命名惯例:全大写蛇形
const SECONDS_PER_HOUR: u32 = 60 * 60;

const 才更接近 Go 的 const.普通的 let x = 5 只是"不会变的运行期变量",二者不同.

标量类型:整数必须标位宽

Rust 有四类标量:整数,浮点,布尔,字符.和 Go 最大的不同是整数类型必须明确位宽和符号.

类别 Rust Go 对应 说明
有符号整数 i8 i16 i32 i64 i128 isize int8 ... int64 int 未标注时默认推断 i32
无符号整数 u8 u16 u32 u64 u128 usize uint8 ... uint 集合索引/长度用 usize
浮点 f32 f64 float32 float64 默认推断 f64
布尔 bool bool 一致
字符 char rune 4 字节 Unicode 标量值
let a: i64 = 1_000_000; // 下划线作千分位分隔,仅为可读性
let b = 3.14; // 推断为 f64
let big = 1_000u32; // 字面量后缀直接标类型
let hex = 0xff; // 十六进制,还支持 0o 八进制,0b 二进制
let heart = '❤'; // char,可存任意 Unicode 标量

usize:索引就用它

给集合做下标,表示长度时,类型是 usize(其位宽随平台,64 位机上是 64 位).

这是高频类型,记住"凡是和'多少个,第几个'相关的,基本都是 usize".

整数溢出:Rust 会管,Go 不管

一个重要的行为差异.Go 整数溢出静默回绕(255u8 + 1 == 0,不报错);Rust 则区分构建模式:

  • debug 构建:溢出直接 panic,帮助当场抓到 bug.
  • release 构建:溢出回绕(出于性能),行为同 Go.

真正想表达某种溢出语义时,用明确的方法,而不是依赖默认行为:

let a: u8 = 255;
let b = a.wrapping_add(1); // 回绕:得到 0
let c = a.checked_add(1); // 检查:得到 None(Option,溢出即 None)
let d = a.saturating_add(1); // 饱和:得到 255(封顶,不再增长)
let (e, overflowed) = a.overflowing_add(1); // 返回 (结果, 是否溢出)

最佳实践:用语义明确的运算方法

处理可能溢出的计算(如累加用户输入,做容量计算)时,别裸用 +,而要根据需求选 checked_*(要安全),saturating_*(要封顶)或 wrapping_*(要回绕).

把意图写进代码,比依赖"debug 会 panic"可靠得多.

类型转换:没有隐式转换

Rust 绝不隐式转换数值类型--连 i32i64 相加都不行.这和 Go 一致(Go 也要求显式转换),但 Rust 更严格,手段也更明确:

let a: i32 = 10;
let b: i64 = 20;
// let c = a + b; // 编译错误:类型不匹配
let c = a as i64 + b; // as:适用于数值的简单转换

// 更安全的转换:From / TryFrom
let x: i64 = i64::from(a); // From:不会失败的转换(小转大)
let y: Result<u8, _> = u8::try_from(300); // TryFrom:可能失败的转换,返回 Result

as 转换会截断,要当心

as 在大类型转小类型时会静默截断(300i32 as u8 == 44),不报错也不 panic.

涉及可能丢失精度/范围的转换,优先用 try_from--它返回 Result,溢出时给出 Err 而非默默截断.as 留给确定安全的场景(如小转大,与底层 API 交互).From/TryFrom 的完整机制在第 14 篇错误处理还会再遇到.

char 不是字节

Rust 的 char 占 4 字节,表示一个 Unicode 标量值,和 Go 的 rune 一致;但要注意它不等于字符串里的字节.Rust 干脆禁止用整数下标取字符串字符,逼开发者明确"按字节"还是"按字符":

let s = "héllo";
// let first = s[0]; // 编译错误:String 不支持整数索引

for c in s.chars() { // 按 Unicode 字符遍历
print!("{c} "); // h é l l o
}
println!("字节数:{}", s.len()); // 6(é 占 2 字节)
println!("字符数:{}", s.chars().count()); // 5

字符串细节留到后面

Rust 字符串有 String&str 之分,还有 UTF-8 编码带来的种种细节,本系列第 08 篇会专门讲集合时展开.

这里只需记住:char 是字符,不是字节,字符串不能整数下标.

元组与单元类型

顺带认识两个会频繁出现的类型.元组(tuple) 把多个值组合在一起,常用于函数返回多值:

let point: (i32, i32) = (3, 4);
let (x, y) = point; // 解构
println!("{}", point.0); // 也可用 .索引 访问

fn min_max(v: &[i32]) -> (i32, i32) {
(v[0], v[v.len() - 1]) // 返回元组,类比 Go 的多返回值
}

单元类型 ()(unit)是"没有有意义的值"的类型,只有一个值 ().没有显式返回值的函数,返回的就是它--相当于其他语言的 void.

快速回顾

  • 默认不可变:let 不可变,mut 才可变;最佳实践是能不可变就不可变.
  • 遮蔽:同名 let 创建新变量,可换类型,区别于 mut 改值;const 是编译期常量.
  • 整数标位宽:默认 i32/f64;索引/长度用 usize.
  • 溢出:debug 下 panic,release 回绕;按需用 checked_/saturating_/wrapping_ 方法表达意图.
  • 无隐式转换:用 as(简单但会截断)或 From/TryFrom(安全);有损转换优先 try_from.
  • char 不等于字节:char 是 4 字节 Unicode 标量,字符串不能整数下标;元组组合多值,() 是 void.

动手练习

  1. 不可变与遮蔽:声明不可变变量并尝试修改,观察报错,再用 mut 修复;然后用 shadowing 把一个字符串变量重新绑定为它的长度.
  2. 溢出方法对比:对 let x: u8 = 250 分别调用 checked_add(10),saturating_add(10),wrapping_add(10),打印并解释三者结果差异.
  3. 转换安全性:用 u8::try_from(300) 观察返回的 Err,再用 300i32 as u8 对比截断结果.
  4. 多返回值:写一个返回 (i32, i32, f64) 的函数,返回一组数的最小值,最大值,平均值,并在调用处解构接收.
  5. UTF-8 验证:对一个含非 ASCII 字符的字符串,分别打印它的 .len()(字节数)和 .chars().count()(字符数),解释为何不同.