📚 Rust 课程系列

  1. 课程概览
  2. 基础语法(一):变量、数据类型与字符串(本文)
  3. 切片(Slice):序列的借用视图
  4. 基础语法(二):运算符、表达式与控制流
  5. 函数与输入输出
  6. 所有权、借用与生命周期
  7. 结构体
  8. 枚举
  9. 模式匹配
  10. 类型系统:泛型、trait 与多态
  11. 集合与容器
  12. 错误处理与 Panic 恢复
  13. 模块、属性与宏
  14. 智能指针、迭代器与闭包
  15. 并发与异步编程
  16. Unsafe Rust 与常用 trait 详解
  17. 工具链、Cargo 与外部 crate
  18. 最佳实践、性能与调试

Rust 是一门注重内存安全、性能和并发性的系统编程语言。本篇聚焦变量、数据类型与字符串–Rust 类型系统与所有权机制的地基,涵盖变量绑定与常量、标量与复合类型、字符串的 UTF-8 设计及类型转换。

注释与文档注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 单行注释:最常用,注释到行尾

/* 块注释:可跨多行,支持嵌套(C 的 /* */ 不可嵌套) */

/// 文档注释:为紧随其后的项生成文档,支持 Markdown
///
/// # Examples
///
/// ```
/// assert_eq!(add(2, 3), 5);
/// ```
fn add(a: i32, b: i32) -> i32 {
a + b
}

//! 模块级文档注释:描述所在模块/文件整体
//! 通常放在文件或模块的最顶部

💡 提示cargo doc --open 会把 /// 文档注释渲染成 HTML 文档;文档注释里的示例代码块会被 cargo test 当作文档测试(doc test)实际编译运行–这是保证示例代码不腐化的利器,开源库广泛使用。

⚠️ 注意:Rust 注释规范约定:解释"为什么"用普通注释(//),解释"做什么、怎么用"用文档注释(///)。文档注释是公共 API 的一部分,会出现在 docs.rs 上。

变量与常量

声明方式可变性类型标注作用域典型用途
let x = 5;不可变(默认)可推断块级一般绑定
let mut y = 10;可变可推断块级需要修改的值
const MAX: u32 = 100;不可变必须标注可全局编译期常量
static G: &str = "hi";不可变(默认)必须标注全局静态生命周期值

💡 提示:Rust 变量默认不可变–这是与 C++/Go/Java 最大的区别之一。不可变让编译器能做更多优化,也从根本上杜绝"意外修改"类 bug。需要修改时显式写 mut,让意图一目了然。

let 解构绑定

let 不仅能绑定单个变量,还能直接解构元组、数组与结构体–这是 Rust 模式匹配能力在绑定语法上的体现(完整模式匹配见《模式匹配》)。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 元组解构:按位置绑定
let (x, y, z) = (1, 2.0, 'a');

// 数组解构:长度必须匹配
let [a, b, c] = [1, 2, 3];
let [first, .., last] = [1, 2, 3, 4, 5];

// 嵌套解构
let ((p, q), r) = ((1, 2), 3);

// 解构时绑定可变性
let (mut a, b) = (1, 2);
a += 1;

💡 提示:数组解构中的 .. 是通配模式,可忽略任意数量的元素。解构在函数参数中同样适用:fn f((x, y): (i32, i32)) 直接按位置取参数,省去中间变量。

遮蔽(Shadowing)

1
2
3
4
5
6
7
let x = 5;
let x = x + 1; // 遮蔽:创建新变量,可改变类型
let x = "hello"; // 合法!遮蔽允许类型变化

let mut y = 10;
y = 15; // 可变赋值:类型不能变
// y = "hi"; // 错误!mut 不能改变类型

🔄 对比:遮蔽 ≠ 可变赋值。遮蔽创建新绑定(可改变类型),mut 修改现有绑定(类型固定)。C++ 中同名变量在同级作用域会报错,Rust 允许遮蔽。

⚠️ 注意:遮蔽常用于值转换链(如 let s = s.trim();),但过度使用会降低可读性。同一块内遮蔽超过 2-3 次时应换用更有意义的变量名。

忽略变量与未使用前缀

1
2
3
4
5
6
7
8
9
10
// _ 是占位符:不绑定值,立即丢弃
let _ = compute();

// _x:绑定但允许未使用(下划线前缀)
let _x = 42; // 不报 unused 警告

// 关键区别:_ 不绑定,值立即 drop
// _x 绑定,值存活到作用域结束
let _ = String::from("a"); // 立即 drop
let _s = String::from("b"); // 存活到块结束

⚠️ 注意__x 的语义差异常被忽略。let _ = lock.lock() 会让锁守卫立即释放,几乎总是 bug;而 let _x = lock.lock() 则让锁存活到块结束。处理 MutexGuardRc 等有副作用的类型时,务必用 _x 而非 _

作用域与块级 drop

Rust 变量遵循 RAII(Resource Acquisition Is Initialization)模式:变量在离开作用域时自动调用 drop,释放其拥有的资源(堆内存、文件句柄、锁等),无需手动释放。

1
2
3
4
5
6
{
let s = String::from("hi");
let f = std::fs::File::open("x").unwrap();
// s、f 在此块内有效
} // <- 离开块:先 drop f,再 drop s
// 释放堆内存、关闭文件描述符

💡 提示:drop 顺序与声明顺序相反(LIFO),这一规则保证后创建的变量(可能引用了先创建的)先被释放。作用域与所有权的完整推导见所有权、借用与生命周期

常量与静态变量

1
2
const MAX_VALUE: u32 = 100_000;         // 编译期求值,内联到使用处
static GREETING: &str = "Hello, Rust!"; // 固定内存地址,全局唯一

⚠️ 注意static mut 是 unsafe 的–多线程并发访问会导致数据竞争。需要全局可变状态时,用 Atomic* 类型或 Mutex 封装,或用 once_cell::sync::Lazy 延迟初始化。

const fn 扩大了编译期求值的范围,但仍有严格限制:

1
2
3
4
5
6
7
8
// const fn:可在编译期求值的函数
const fn double(x: i32) -> i32 {
x * 2
}
const N: i32 = double(21); // 编译期求值

// const fn 的限制:不能调用非 const fn、
// 不能使用堆分配、不能用 mutable static
特性conststatic
内存布局内联到使用处固定地址,全局唯一
取地址每处可能不同程序生命周期内不变
求值时机编译期内联编译期初始化
可变性不可变static mut(unsafe)
1
2
3
4
5
6
const C: u32 = 42;
static S: u32 = 42;

// 取地址行为不同
let p1: *const u32 = &C; // 可能每次不同
let p2: *const u32 = &S; // 全局唯一地址

💡 提示:需要编译期常量用 const,需要固定地址或全局可变状态用 staticconst fn 的限制在 Rust 版本迭代中逐步放宽,但堆分配、浮点比较等仍不可用。

类型别名与字面量后缀

1
2
3
4
5
6
type Kilometers = i32;                          // 类型别名
type MyResult<T> = std::result::Result<T, String>;

let x = 42u8; // 后缀指定类型
let y = 3.14f32;
let z = 100_000i64; // 下划线提高可读性

数据类型

标量类型速查

类别类型备注
有符号整数i8 i16 i32 i64 i128默认推断为 i32
无符号整数u8 u16 u32 u64 u128字节操作常用 u8
架构相关isize / usize与指针同宽,索引/长度用 usize
浮点数f32 f64默认推断为 f64
布尔booltrue / false,占 1 字节
字符charUnicode 标量值,占 4 字节

💡 提示char 是 Unicode 标量值(4 字节),不是 C 的 1 字节 char'😊' 是合法的 char,但字符串内部是 UTF-8 编码,一个 Unicode 字符可能占 1-4 字节–按字节切片可能截断字符。

bool 详解

bool 看似简单,但 Rust 对它的约束比多数语言更严格:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let t: bool = true;
let f = false;

// 占 1 字节,但只有 true/false 两个值
assert_eq!(std::mem::size_of::<bool>(), 1);

// 不可做算术运算
// let x = true + false; // 编译错误

// 源于比较表达式
let gt = 5 > 3; // 类型为 bool

// 不能用 as 转 bool
// let b = 1 as bool; // 编译错误
let b = 1 != 0; // 必须显式比较

⚠️ 注意:Rust 不像 C/C++ 那样将整数隐式转为布尔值,也没有 as bool 转换–这是刻意设计,杜绝 if (x = 1) 这类经典错误。判断非零必须写 x != 0,让意图显式。

char 方法表

方法返回类型说明
is_ascii()bool是否 ASCII 字符
is_alphabetic()bool是否字母(含中文等)
is_numeric()bool是否数值字符
is_digit(radix)bool是否指定进制数字
to_digit(radix)Option<u32>转为数字值
to_lowercase()ToLowercase迭代器(一对多)
to_uppercase()ToUppercase迭代器(一对多)
len_utf8()usizeUTF-8 编码字节数
len_utf16()usizeUTF-16 编码单元数
encode_utf8(buf)&mut str写入缓冲区
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let c = 'A';
c.is_ascii(); // true
c.is_alphabetic(); // true
c.is_ascii_uppercase(); // true

let d = '5'.to_digit(10); // Some(5)

// to_lowercase / to_uppercase 返回迭代器,
// 而非 char--因 Unicode 可能一对多
for lc in 'É'.to_lowercase() {
println!("{}", lc); // 'é'
}

// UTF-8 编码信息
let n = '中'.len_utf8(); // 3
let mut buf = [0u8; 4];
'中'.encode_utf8(&mut buf);

💡 提示to_lowercase/to_uppercase 返回迭代器而非 char,是因为 Unicode 大小写转换可能一对多–例如德语 ß 大写为 SS(两个字符)。len_utf8() 在编译期即可求值,常用于预分配缓冲区。

类型内存占用

1
2
3
4
5
6
7
8
9
use std::mem::size_of;

assert_eq!(size_of::<bool>(), 1);
assert_eq!(size_of::<char>(), 4); // Unicode 标量值
assert_eq!(size_of::<i32>(), 4);
assert_eq!(size_of::<f64>(), 8);
assert_eq!(size_of::<usize>(), 8); // 64 位平台
assert_eq!(size_of::<&str>(), 16); // 胖指针:指针 + 长度
assert_eq!(size_of::<String>(), 24); // 指针 + 容量 + 长度

💡 提示&str&[T] 等切片引用是胖指针(fat pointer),除了数据地址还携带长度信息,所以是普通引用的两倍宽。size_of::<T>() 在编译期即可求值,是理解零成本抽象与内存布局的入门工具。

整数字面量与进制

形式示例
十进制98_22298222
十六进制0xff255
八进制0o7763
二进制0b1111_0000240
字节字面量b'A'65(类型为 u8

💡 提示_ 分隔符可放在字面量任意位置提高可读性(1_000_0000b1101_0010),编译器直接忽略。字节字面量 b'A' 只适用于 ASCII 字符,类型是 u8 而非 char

整数溢出行为

Rust 对整数溢出的处理是两阶段的,这是其他语言少有的精细设计:

  • Debug 模式:溢出时 panic(如 255u8 + 1),帮助尽早发现 bug
  • Release 模式:按二进制补码回绕(wrapping),255u8 + 1 -> 0,静默不报错

需要明确的溢出语义时,用对应的显式方法族:

1
2
3
4
5
6
7
let a: u8 = 255;

let r = a.wrapping_add(1); // 0:回绕
let r = a.checked_add(1); // None:溢出返回 None
let r = a.saturating_add(1); // 255:饱和(钳位到类型边界)
// (0, True):回绕后的值 + 溢出标志
let (v, overflowed) = a.overflowing_add(1);

⚠️ 注意:不要依赖 release 模式的回绕行为–那只是未定义语义的"托底",不是特性。需要回绕语义时显式写 wrapping_*;需要检测溢出用 checked_*(返回 Option);图像/音频处理常用 saturating_*。若想让 release 也 panic,可在 Cargo.toml 中设置 [profile.release] overflow-checks = true(有少量性能开销)。

数值方法族总览

类别方法说明
算术abs()绝对值
算术signum()符号值(-1/0/1)
算术pow(exp)幂运算
算术powi(exp)整数幂(i32 指数)
算术powu(exp)无符号幂(u32 指数)
算术sqrt() / cbrt()平方根 / 立方根(浮点)
算术min(b) / max(b)最小 / 最大值
算术clamp(lo, hi)钳位到区间
算术div_euclid(b)欧几里得除法(向下取整)
算术rem_euclid(b)欧几里得余数(非负)
位运算count_ones()1 的位数
位运算trailing_zeros()末尾 0 的个数
位运算leading_zeros()前导 0 的个数
位运算swap_bytes()字节序交换
位运算to_be() / to_le()转大端 / 小端
位运算rotate_left(n)循环左移
位运算rotate_right(n)循环右移
1
2
3
4
5
6
7
8
9
let a: i32 = -5;
a.abs(); // 5
a.signum(); // -1
a.clamp(0, 10); // 0(钳位到非负区间)

let n: u32 = 0b1010_1100;
n.count_ones(); // 4
n.trailing_zeros(); // 2
n.rotate_left(4); // 0b1100_1010

💡 提示div_euclid / rem_euclid 提供与 Python // / % 一致的向下取整语义,处理负数除法时比默认的向零取整更直观。位运算方法在协议解析、哈希、加密等场景常用。

运算符是方法调用的语法糖

Rust 的 +-== 等运算符并非内建魔法,而是 std::ops 模块中 trait 方法的语法糖;反过来,方法调用 a.abs() 本身也是完全限定语法 i32::abs(a) 的糖。

表达式语法糖展开对应 trait
a + ba.add(b)Add
a - ba.sub(b)Sub
a * ba.mul(b)Mul
a / ba.div(b)Div
a % ba.rem(b)Rem
a += ba.add_assign(b)AddAssign
-aa.neg()Neg
!aa.not()Not
a == ba.eq(&b)PartialEq
a < ba.lt(&b)PartialOrd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::ops::{Add, AddAssign};

let a = 1 + 2; // 运算符写法
let b = 1i32.add(2); // 展开:trait 方法调用
let c = Add::add(1, 2); // 完全限定语法(UFCS)
assert_eq!(a, b);
assert_eq!(b, c);

let mut x = 10;
x += 5; // 糖
x.add_assign(5); // 展开:AddAssign::add_assign
assert_eq!(x, 20);

let n = -5i32;
n.abs(); // 方法调用
i32::abs(n); // 完全限定语法,二者等价

💡 提示:对整数、浮点等基本类型,编译器会把运算符直接编译为单条机器指令,不经过 trait 调用–语法糖零开销。但语义上二者完全等价,因此自定义类型只要实现对应 trait 即可重载运算符(如实现 AddPoint + Point 合法),详见类型系统:泛型、trait 与多态

⚠️ 注意:方法调用糖还包含自动引用/解引用规则–s.len() 实际展开为 String::len(&s),编译器自动补上 &&mut*。这就是为什么 String 的方法能直接用在 &String 上。完整推导见智能指针、迭代器与闭包

自增与自减:Rust 为何没有 ++/--

从 C/C++/Java 转来的开发者常会下意识地写 i++--count,结果 Rust 编译器直接报错–Rust 没有 ++ / -- 运算符。自增自减改用复合赋值:

1
2
3
let mut i = 0;
i += 1; // 等价于其他语言的 i++ / ++i
i -= 1; // 等价于其他语言的 i-- / --i

🔄 对比:C/C++/Java 的 ++ / -- 区分前缀(++i,返回新值)与后缀(i++,返回旧值),二者语义不同且容易在复杂表达式中混淆。Rust 刻意省去这对运算符,让"自增一步"统一写成 i += 1,意图明确、无歧义。

⚠️ 注意+= 1 本质是 AddAssign trait 的方法调用(i.add_assign(&1)),返回单元类型 ()–它不能嵌入表达式求值。这从根上杜绝了 a[i++] = i++ 这类依赖求值顺序的经典 bug。

若需要"取旧值并自增"(模拟后缀 i++),分两步即可:

1
2
3
4
let mut i = 0;
let old = i;
i += 1;
// old 为 0,i 已变为 1

💡 提示:需要"取旧值并推进"的场景(如生成自增 ID),更地道的做法是用迭代器:std::iter::repeat_with 配合计数器,或直接 Iterator::enumerate / (0..).zip(...),把可变状态收敛到迭代器中,避免散落各处的 i += 1

溢出处理四族对照

方法族溢出行为返回类型
wrapping_add回绕T
checked_add返回 NoneOption<T>
saturating_add钳位到边界T
overflowing_add回绕 + 标志(T, bool)
1
2
3
4
5
let a: u8 = 255;
a.wrapping_add(1); // 0
a.checked_add(1); // None
a.saturating_add(1); // 255
a.overflowing_add(1); // (0, true)

💡 提示:四族方法对所有算术运算(add/sub/mul/div/rem/neg/shl/shr)均有对应版本。选择策略:检测溢出用 checked_*,图像/信号处理用 saturating_*,密码学/哈希用 wrapping_*,同时需要值与标志用 overflowing_*

浮点数的特殊性

1
2
3
4
5
6
7
8
9
let x = 0.1 + 0.2;
println!("{}", x); // 0.30000000000000004(IEEE 754 精度)

let nan = f64::NAN;
println!("{}", nan == nan); // false!NaN 不等于自身

// 正确的比较方式
assert!((x - 0.3).abs() < f64::EPSILON); // 误差范围内比较
let ord = 1.0f64.partial_cmp(&2.0); // Some(Less),返回 Option

⚠️ 注意f32/f64 只实现了 PartialOrd 而非 Ord–NaN 的存在破坏了全序关系(total order)。因此浮点数不能用作 BTreeMap 的键,数组也不能直接 .sort()。排序要用 sort_by(|a, b| a.partial_cmp(b).unwrap()),或用 ordered_float crate 的 OrderedFloat 包装类型。

💡 提示:金融计算不要用 f64,改用 rust_decimal 等十进制 crate,或以"分"为单位的整数。浮点精度问题在所有语言中都存在,Rust 只是把"不可全序"这一事实在类型系统中暴露了出来。

单元类型 () 与 Never 类型 !

1
2
3
4
5
6
7
8
// 单元类型:表示"没有有意义的返回值"
let unit: () = println!("hello"); // println! 的返回类型是 ()

// Never 类型:表示"永远不会返回"
fn forever() -> ! {
loop {}
}
// panic!、break、continue 表达式的求值类型也是 !

🔄 对比() 类似 C++ 的 void,但它是真正的类型且只有一个值 (),可以参与泛型–例如 Result<(), Error> 表示"只关心成功与否"。! 表示发散(diverging),可强制转换为任意类型,因此 match 分支中写 panic!() 不会破坏类型统一:

1
2
3
4
let x: i32 = match some_option {
Some(v) => v,
None => panic!("missing"), // ! 可转为 i32,分支类型一致
};

类型推断与默认类型

1
2
3
4
let x = 42;              // 推断为 i32(整数默认类型)
let y = 3.14; // 推断为 f64(浮点默认类型)
let v = Vec::new(); // ❌ 编译错误:无法推断元素类型
let v: Vec<i32> = Vec::new(); // ✅ 显式标注

💡 提示:Rust 的推断是全函数体的–变量类型可由后续使用反推:

1
2
let mut v = Vec::new();
v.push(1i64); // 编译器回推 v: Vec<i64>

但函数签名、结构体字段等跨函数边界必须显式标注类型–这是刻意设计,保证错误信息本地化、API 稳定且文档自明。

复合类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 元组:固定长度,异构
let tup: (i32, f64, char) = (500, 6.4, 'z');
let (x, y, z) = tup; // 解构
let first = tup.0; // 索引访问

// 数组:固定长度,同构,栈分配
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 100]; // [0, 0, ..., 0] 重复初始化

// Vec:动态数组,堆分配
let mut v = vec![1, 2, 3];
v.push(4);

// 切片:对连续序列的借用视图,不拥有数据
let slice: &[i32] = &arr[1..4]; // [2, 3, 4]
let hello: &str = &s[0..5]; // 字符串切片

🔄 对比:切片 &[T] / &str 类似 Go 的 slice header(指针+长度),但不包含容量信息。Rust 切片是纯"视图",不能 append。

💡 提示:数组 vs Vec 的选择–长度固定且小用数组(栈分配,零开销),需要动态增长用 Vec(用 with_capacity 减少重分配)。切片的完整用法见《切片(Slice)》

元组详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 单元类型 ():空元组,唯一值 ()
let unit: () = ();

// 单元素元组:必须有尾逗号
let single: (i32,) = (42,);
let not_tuple = (42); // 这是 i32,非元组

// 解构:按位置绑定
let tup = (1, 2.0, 'a');
let (a, b, c) = tup;
let (x, .., z) = (1, 2, 3, 4); // 忽略中间

// 索引访问(.0, .1, ...)
let first = tup.0; // 1
let second = tup.1; // 2.0

💡 提示() 是唯一的单元类型值,常作为"无返回值"的占位(如 Result<(), Error>)。单元素元组 (x,) 的尾逗号不可省–(42) 是括号包裹的表达式而非元组。元组结构体(struct Point(i32, i32))是元组与结构体的混合体,详见《结构体》

array vs &slice vs Vec 对比

特性[T; N] 数组&[T] 切片Vec<T>
内存位置栈(默认)借用外部数据
长度编译期固定运行时固定可动态增长
所有权拥有数据不拥有拥有数据
容量capacity
典型用途固定大小数据函数参数通用动态集合

数组实用技巧与 const generics

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let mut arr = [3, 1, 4, 1, 5];
arr.sort(); // 原地排序
arr.reverse(); // 反转
let has = arr.contains(&4); // true
let first = arr.first(); // Option<&i32>,越界安全的访问

// 数组与切片互转
let slice: &[i32] = &arr; // 自动解引用为切片

// const generics:函数可接受任意长度的数组
fn sum<const N: usize>(arr: [i32; N]) -> i32 {
arr.iter().sum()
}
let s1 = sum([1, 2, 3]); // N = 3
let s2 = sum([1, 2, 3, 4, 5]); // N = 5

💡 提示:const generics(Rust 1.51+)让数组长度成为类型参数,标准库因此能为任意长度的 [T; N] 统一实现 trait。早期 Rust 只覆盖长度 0…=32 的数组,这一历史遗留问题已被 const generics 彻底解决。

字符串与类型转换

Rust 的字符串设计比大多数语言更细致–核心原因是 UTF-8 编码与零成本抽象的矛盾。理解 String&strcharu8 四者的关系,是掌握 Rust 字符串的关键。

字符串类型总览

类型内存所有权编码典型来源
String堆分配,可增长拥有数据UTF-8String::from()to_string()format!
&str借用(栈上胖指针:指针+长度)不拥有UTF-8字面量 "hello"&s[..]s.as_str()
[u8] / &[u8]原始字节-无保证s.as_bytes()、文件 I/O、网络数据
Vec<char>堆分配拥有Unicode 码点s.chars().collect()
OsStr / OsString平台相关-平台原生文件路径、环境变量
CString / CStr兼容 C-NUL 结尾字节FFI 调用

💡 提示:日常开发 99% 的时间只需关注 String&str&strString 的借用视图–类似 &[T]Vec<T> 的关系(切片详见《切片(Slice)》)。函数参数优先用 &str(更通用,可接受 &String 的自动解引用),返回值按需选择:有所有权返回 String,借用返回 &str

1
2
3
4
5
6
7
8
9
10
let s1 = String::from("hello");   // 堆分配,可增长
let s2: &str = "world"; // 字面量,嵌入二进制中的 UTF-8 数据
let s3 = s1 + " " + s2; // String 拼接(s1 被消费)
let len = s3.len(); // 字节长度,非字符数!

// String ↔ &str 互转
let owned: String = "hello".to_string(); // &str -> String
let owned2: String = String::from("hello");
let borrowed: &str = &owned; // String -> &str(自动 Deref)
let borrowed2: &str = owned.as_str(); // 显式转换

String 的堆增长机制

String 内部是 Vec<u8>,堆上存储 UTF-8 字节,并维护三个字段:指针、长度(len)和容量(capacity)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let mut s = String::new();
assert_eq!(s.capacity(), 0);

s.push_str("hello");
// capacity 通常倍增(策略由分配器决定)
// len = 5, capacity >= 5

// 预分配容量,避免多次重分配
let mut s = String::with_capacity(100);
s.push_str("hello");
assert_eq!(s.capacity(), 100);

// 缩减到实际长度,释放多余堆内存
let mut s = String::from("hello world");
s.truncate(5);
s.shrink_to_fit(); // capacity 降为 5

💡 提示push / push_str 在容量足够时零分配(仅更新 len);超出容量时触发重分配(新容量通常翻倍,拷贝旧数据到新堆块)。高频拼接场景用 with_capacity 预分配可显著减少重分配次数。shrink_to_fit 用于"先大量 push 再只读"的模式,释放多余堆内存。

String&str 的内存布局对比–&str 是栈上胖指针(ptr + len),String 是三元组(ptr + len + cap),二者均指向堆上的 UTF-8 字节序列:

graph TB
    subgraph 栈 Stack
        S["&str (16B)<br/>ptr | len"]
        T["String (24B)<br/>ptr | len | cap"]
    end
    subgraph 堆 Heap
        H["UTF-8 字节序列<br/>h e l l o"]
    end
    S -->|借用视图| H
    T -->|拥有数据| H

&str 借用的数据可变性

&str不可变借用–通过它永远无法修改底层数据。这一点与绑定本身的可变性是两回事,初学者很容易混淆:

1
2
3
4
5
6
7
8
9
let s = String::from("hello");

let slice: &str = &s;
// slice.make_ascii_uppercase(); // ❌ 需要 &mut str

// mut 修饰的是绑定,不是指向的数据
let mut view: &str = &s;
view = "world"; // ✅ 合法:改指针指向另一段数据
// view.push_str("!"); // ❌ &str 没有修改方法

字符串字面量的类型是 &'static str,数据直接嵌入二进制的只读段,天然不可变:

1
2
let lit: &'static str = "hello";
// 试图通过 unsafe 强转写入是未定义行为,可能段错误

那 Rust 有没有"可变的字符串借用"?有–&mut str 存在但极少用,它只能原地等长修改,不能改变长度:

1
2
3
4
5
let mut s = String::from("hello");
let view: &mut str = &mut s;
view.make_ascii_uppercase(); // ✅ 原地大写,长度不变
// view.push('!'); // ❌ 会改变长度,不允许
// view.truncate(3); // ❌ 同样不允许

⚠️ 注意:改变长度必须通过拥有者 String 本身(push_strtruncate 等)。&mut str 的设计保证切片的长度字段永远与数据一致–借用的"视图"不可能越界。

借用期间还受借用规则约束:只要 &str 还存活,String 就不能被修改:

1
2
3
4
5
6
let mut s = String::from("hello");
let r: &str = &s;
// s.push_str(" world"); // ❌ 有不可变借用存活,不能改
println!("{}", r); // r 最后一次使用后借用结束

s.push_str(" world"); // ✅ NLL:借用已结束

🔄 对比:C++ 的 string_view / Go 的子串可以共享底层数据,而原字符串随时可改,可能读到"半新半旧"的数据。Rust 用借用规则在编译期杜绝了这类"迭代器失效"问题–要么共享只读视图,要么独占修改权,二者不可兼得。可变性规则的完整推导见所有权、借用与生命周期

&mut str 详解:等长约束下的原地修改

&mut str 是字符串上唯一允许修改的借用形式,但受等长约束–只能改写已有字节,不能增删。它的使用场景窄而明确:原地、保长的文本处理。

获取 &mut str 有三种方式(同一时刻只能存活一个):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let mut s = String::from("hello world");

// 方式 1:Deref 强制转换(最常用)
{
let v: &mut str = &mut s;
v.make_ascii_uppercase();
}
// 方式 2:显式调用 as_mut_str()
{
let v: &mut str = s.as_mut_str();
v.make_ascii_lowercase();
}
// 方式 3:可变索引切片,只取子串修改
{
let v: &mut str = &mut s[0..5];
v.make_ascii_uppercase();
}
assert_eq!(s, "HELLO world");

str 上接收 &mut self 的方法只有三个–这份"白名单"如此之短,正是因为任何可能改变长度或破坏 UTF-8 的操作都不能放进 &mut str

方法作用说明
make_ascii_uppercase()ASCII 转大写原地,O(n),长度不变
make_ascii_lowercase()ASCII 转小写原地,O(n),长度不变
as_bytes_mut()&mut [u8]unsafe,调用者须保证结果仍是合法 UTF-8

需要字节级控制时(如脱敏掩码),只能走 unsafe 的 as_bytes_mut

1
2
3
4
5
6
7
8
9
let mut s = String::from("password: 123456");
let v: &mut str = &mut s[10..];
// unsafe 的原因:写入非法字节会破坏 UTF-8 不变量
for b in unsafe { v.as_bytes_mut() } {
if b.is_ascii_digit() {
*b = b'*';
}
}
assert_eq!(s, "password: ******");

对比 Unicode 大小写转换–ß 大写为 SS,长度会变,所以它只能返回新的 String,永远不可能成为 &mut str 的方法:

1
2
3
let s = String::from("straße");
let upper = s.to_uppercase(); // 新分配:"STRASSE"
assert_eq!(upper, "STRASSE"); // 字符数 6 变 7

作为函数参数,&mut str 适合"原地归一化"类算法:

1
2
3
4
5
6
7
fn normalize_id(id: &mut str) {
id.make_ascii_lowercase();
}

let mut input = String::from("User_ABC");
normalize_id(&mut input); // &mut String 自动转为 &mut str
assert_eq!(input, "user_abc");

💡 提示:函数参数的可变借用优先用 &mut String 而非 &mut str–前者能力超集(可改长度),后者只在"明确承诺不改长度"的算法里才有表达力优势(如 ASCII 归一化、掩码、就地编码转换)。这与 &str vs &String 的推荐方向正好相反,因为只读借用追求的是通用性,可变借用追求的是能力。

字符串拼接方式对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let a = String::from("hello");
let b = String::from("world");

// 1. + 运算符:消费左侧 String,右侧必须为 &str
let c = a + " " + &b; // a 被移动,之后不可用

// 2. format! 宏:不消费任何参数,最灵活
let d = format!("{} {}", "hello", "world");

// 3. push / push_str:原地修改,零分配(如果容量足够)
let mut e = String::with_capacity(20);
e.push_str("hello");
e.push(' ');
e.push_str("world");

// 4. concat! 宏:编译期拼接字面量
const GREETING: &str = concat!("hello", " ", "world");

🔄 对比+ 拼接类似 Java 的 + 但会消费左侧所有权;format! 类似 Python 的 f-string / C++ 的 fmt::format,不移动所有权。高频拼接优先用 push_str + 预分配容量,避免反复重分配。

字符串修改方式总结

前文涉及的修改手段,按"谁来改、能否改长度、是否新分配"三个维度归纳:

类别操作形式改变长度内存行为代表方法
拥有者修改&mut String原地(超容量时重分配)push_strtruncateclear
切片等长修改&mut str严格原地make_ascii_uppercase、unsafe as_bytes_mut
消费式拼接String + &str复用左侧缓冲区a + " " + &b
格式化生成format!总是新分配format!("{a} {b}")
转换生成返回 String 的方法总是新分配to_uppercasereplacerepeat
编译期拼接concat!-零运行时开销concat!("a", "b")

选择时的三步判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let mut s = String::with_capacity(32);

// 1. 不改长度?→ 用 &mut str 的等长方法(零分配)
{
let v: &mut str = &mut s;
v.make_ascii_lowercase();
}

// 2. 改长度且想复用缓冲区?→ 拥有者操作(预分配后零分配)
s.push_str("hello");
s.truncate(2);

// 3. 需要全新的字符串?→ format! 或转换方法(总是新分配)
let t = format!("{}-{}", s, 42);
let u = t.replace('-', "_"); // 返回新 String

💡 提示:记忆口诀–等长找 &mut str,变长找 String,新值找 format!。性能敏感的循环内只做前两类(配合 with_capacity 可做到全程零分配),format! 与转换类方法留给最终结果的组装。

format! 格式化说明符

说明符含义示例输出
{}Display trait42
{:?}Debug trait[1, 2]
{:#?}Debug(美化缩进)多行展开
{:>8}右对齐宽 842
{:<8}左对齐宽 842
{:^8}居中宽 842
{:.2}2 位小数3.14
{:x}十六进制ff
{:o}八进制77
{:b}二进制1111
{:e}科学计数法1.5e3
{:p}指针地址0x..
1
2
3
4
5
6
7
println!("{}", 42);       // 42
println!("{:?}", [1, 2]); // [1, 2]
println!("{:>8}", 42); // ' 42'
println!("{:.2}", 3.14159); // 3.14
println!("{:x}", 255); // ff
println!("{:b}", 0b1111); // 1111
println!("{:p}", &42); // 0x...

💡 提示:说明符可组合,如 {:>08x} 表示右对齐宽 8、前补零、十六进制。自定义类型实现 Display{})或 Debug{:?})trait 即可被格式化;Debug 可用 #[derive(Debug)] 自动生成。

String 与 &str 的选择决策

场景推荐类型理由
函数参数&str通用,接受 &String / &str / 字面量
返回值(有所有权)String调用方获得所有权
返回值(借用输入)&str零拷贝,生命周期跟随输入
结构体字段String拥有数据,生命周期独立
全局常量&'static str字面量直接嵌入二进制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 参数优先 &str:可接受 &String、&str、
// 字面量,无需关心调用方拥有何种类型
fn greet(name: &str) {
println!("hi, {}", name);
}

// 返回值:有所有权需求返回 String
fn make_greeting() -> String {
"hello".to_string()
}

// 返回借用 &str:借用已有数据
// (生命周期需匹配,详见第 4 章)
fn first_word(s: &str) -> &str {
s
}

⚠️ 注意:返回 &str 时,其生命周期不能超过输入–编译器会强制检查。若需返回全新字符串,必须返回 String(拥有堆内存)。完整的生命周期推导见所有权、借用与生命周期

字符串的遍历

UTF-8 是变长编码(1-4 字节),因此字符串有三种遍历粒度:字节Unicode 码点(char)字形簇(grapheme cluster)。选错粒度是 Rust 字符串 bug 的常见来源。

1
2
let s = "café";  // 4 字符,5 字节(é 2 字节)
let s_cn = "你好Rust"; // 5 个 Unicode 字符,9 个 UTF-8 字节

1. 按字节遍历:.bytes()

1
2
3
4
5
6
7
8
let s = "café";
for b in s.bytes() {
println!("{}", b); // 99, 97, 102, 195, 169
}
// 等价写法
for b in s.as_bytes() {
println!("{}", b); // &u8 引用
}

💡 提示.bytes() 返回 Iter<u8>,每次产出 u8(值类型);.as_bytes() 返回 &[u8],需再迭代产出 &u8。大多数场景用 .bytes() 更方便。字节遍历适用于 ASCII 文本处理、协议解析、哈希计算等不关心字符语义的场景。

2. 按 Unicode 码点遍历:.chars()

1
2
3
4
5
6
7
8
9
10
11
12
let s = "café";
for c in s.chars() {
println!("{}", c); // 'c', 'a', 'f', 'é'
}

let s_cn = "你好Rust";
for c in s_cn.chars() {
println!("{}", c); // '你', '好', 'R', 'u', 's', 't'
}

// 获取字符数(注意:O(n) 操作,需遍历整个字符串)
let char_count = s.chars().count(); // 4

⚠️ 注意.chars() 产出的是 Unicode 标量值(scalar value),不是用户感知的"字符"。组合字符如 é 可以是单个码点 U+00E9,也可以是 e + \u{0301}(e + 组合重音)两个码点–.chars() 会将后者拆成两个 char。印地语、泰语、阿拉伯语等文字中组合序列更常见。

3. 按字形簇遍历:需要外部 crate

1
2
3
4
5
6
7
8
9
10
11
12
// Cargo.toml: unicode-segmentation = "1"
use unicode_segmentation::UnicodeSegmentation;

let s = "é"; // e + \u{0301} 组合重音
for g in s.graphemes(true) {
println!("{}", g); // "é"(一个字形簇)
}

// 对比 chars
for c in s.chars() {
println!("{}", c); // 'e', '\u{301}'(两个码点)
}

💡 提示:字形簇(grapheme cluster)最接近用户感知的"一个字符"。若需按"视觉字符"截断、计数、对齐,用 unicode-segmentation crate。标准库不提供此功能,因字形簇边界规则极其复杂(Unicode 标准附件 #29),且多数系统编程场景不需要。

4. 遍历与索引的对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let s = "你好Rust";

// ❌ 编译错误!Rust 不支持字符串整数索引
// let ch = s[0];

// ✅ 按字节索引切片(必须对齐字符边界,否则 panic)
let sub: &str = &s[0..6]; // "你好"(每个汉字 3 字节)

// ✅ 安全获取子串
let sub = s.get(0..6); // Some("你好")
let bad = s.get(0..5); // None(截断了"好"的 UTF-8 序列)

// ✅ 按字符位置获取(需手动计算)
let char_idx = s.char_indices().nth(2); // Some('R')

⚠️ 注意:Rust 故意不支持 s[i] 整数索引–因为 UTF-8 变长编码下,O(1) 索引无法保证返回有效字符,且会诱导开发者写出 O(n) 循环却以为是 O(1)。这是 Rust “不让你轻易犯错” 设计哲学的体现。

5. char_indices():同时获取字节位置与字符

1
2
3
4
5
6
7
8
9
10
11
let s = "你好Rust";
for (byte_idx, ch) in s.char_indices() {
println!("字节位置 {}: '{}'", byte_idx, ch);
}
// 输出:
// 字节位置 0: '你'
// 字节位置 3: '好'
// 字节位置 6: 'R'
// 字节位置 7: 'u'
// 字节位置 8: 's'
// 字节位置 9: 't'

💡 提示char_indices().chars() 的增强版,额外提供每个字符在字节流中的起始位置。需要同时操作字符和字节偏移(如构建解析器、实现文本编辑器)时非常有用。

6. 遍历方式速查表

方法产出类型含义标准库时间复杂度适用场景
.bytes()u8原始 UTF-8 字节O(n)协议解析、哈希、ASCII 处理
.chars()charUnicode 标量值O(n)字符级处理、过滤、转换
.char_indices()(usize, char)字节偏移 + 字符O(n)解析器、文本编辑器
.graphemes(true)&str用户感知字形簇❌ crateO(n)UI 显示、截断、对齐
.lines()&str按行分割O(n)逐行处理文本
.split(char)&str按分隔符分割O(n)CSV、路径、词法分析

类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// &str -> 数字
let num: i32 = "42".parse().unwrap(); // 推断类型
let num2 = "42".parse::<i32>().unwrap(); // turbofish 指定类型
let fail = "abc".parse::<i32>().unwrap(); // Err(ParseIntError)

// 数字 -> String
let s = 42.to_string(); // Display trait
let s2 = format!("{}", 42); // format! 宏

// 数值类型间转换
let f = 42i32 as f64; // as 转换(可能截断/丢失精度)
let safe: u8 = 42u32.try_into().unwrap(); // 溢出返回 Err
let from_val: i64 = i32::from(42i32); // From:只允许无损转换

// String ↔ Vec<u8>
let bytes: Vec<u8> = s.into_bytes(); // String -> Vec<u8>(消费)
// from_utf8:验证 UTF-8,失败返回 Err
let s: String = String::from_utf8(bytes).unwrap();
// from_utf8_lossy:无效字节替换为
let s: String = String::from_utf8_lossy(&bytes).into(); �

// String ↔ Vec<char>
let chars: Vec<char> = s.chars().collect(); // String -> Vec<char>
let s: String = chars.into_iter().collect(); // Vec<char> -> String

// char ↔ 数字
let code: u32 = 'A' as u32; // char -> 码点值
// as 转换:无效码点会 panic,语义等同 unsafe
let ch: char = 65u32 as char;
// char::from_u32:安全版本,无效码点返回 None
let ch2: char = char::from_u32(65).unwrap();

⚠️ 注意as 转换在数值截断时静默发生(如 300i32 as u8 -> 44)。安全转换请用 try_into()From/Into trait。From 只允许无损转换(如 i32 -> i64),TryFrom 允许有损转换但返回 Result

⚠️ 注意String::from_utf8 会验证 UTF-8 有效性,失败返回 Err(FromUtf8Error)。若数据来源可信(如刚从 into_bytes() 转换而来),可用 unsafe { String::from_utf8_unchecked(bytes) } 跳过验证–但除非性能瓶颈已确认,否则别用。from_utf8_lossy 是安全折中方案,将无效字节替换为

From / Into / TryFrom / TryInto

Trait方向成功失败
From<T>T -> Self必成功-
Into<T>Self -> T必成功-
TryFrom<T>T -> SelfOkErr
TryInto<T>Self -> TOkErr

💡 提示:实现 From自动获得 Into(标准库的 blanket impl:impl<T, U> Into<U> for T where U: From<T>)。同理 TryFrom 自动获得 TryInto。因此只需实现 From / TryFrom 一侧即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 自定义类型实现 From
struct Meters(f64);
struct Feet(f64);

impl From<Feet> for Meters {
fn from(f: Feet) -> Meters {
Meters(f.0 * 0.3048)
}
}

// 实现 From 自动获得 Into
let m: Meters = Feet(10.0).into();
let m = Meters::from(Feet(10.0));

// TryFrom:可能有损,返回 Result
use std::convert::TryFrom;
let r: Result<u8, _> = u8::try_from(300u32);
// Err:300 超出 u8 范围

// TryInto:TryFrom 的镜像(自动获得)
use std::convert::TryInto;
let r: Result<u8, _> = 300u32.try_into();

⚠️ 注意From 只允许无损转换(如 i32 -> i64&str -> String),TryFrom 允许有损但返回 Result。标准库已为数值类型间实现了完整的 From / TryFrom 关系网。

as 转换总览

as 是 Rust 中唯一的显式强制转换关键字,适用于数值截断与指针转换:

转换方向示例说明
整数间42i32 as u8截断或扩展
整数->浮点42i32 as f64精确
浮点->整数3.7f64 as i32截断小数
char->u32'A' as u32取码点
u32->char65u32 as char无效码点行为未定义
指针↔usizep as usize平台相关
*const T->*mut Tunsafe不变性变化

⚠️ 注意as 转换在数值截断时静默发生(如 300i32 as u8 -> 44),不做任何检查。安全转换请优先用 From / TryFromas 适用于确知范围的场景(如 char -> u32 必然无损)。u32 -> charas 转换在无效码点时行为未定义,应改用 char::from_u32(返回 Option)。

deref coercion 与 turbofish

deref coercion&String 自动转为 &str,无需手动调用 .as_str()–这一机制由 Deref trait 驱动,在函数传参时自动发生:

1
2
3
fn takes_str(s: &str) {}
let owned = String::from("hi");
takes_str(&owned); // &String -> &str

💡 提示:deref coercion 在 &String -> &str&Vec<T> -> &[T]&Box<T> -> &T 等场景自动触发,详解见智能指针、迭代器与闭包

turbofish ::<T> 在类型推断不足时显式指定泛型参数,常与 .parse() / .collect() 配合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// turbofish 显式指定泛型类型
let n = "42".parse::<i32>().unwrap();

// collect 的目标类型
let v = [1, 2, 3].iter()
.collect::<Vec<_>>();

// 链式调用中穿插 turbofish
let first = "1 2 3"
.split_whitespace()
.next()
.unwrap()
.parse::<i64>()
.unwrap();

💡 提示:能靠上下文推断时就不用 turbofish(如 let v: Vec<i32> = ...)。turbofish 适用于类型推断断链的链式调用中间步骤,让代码在保持链式风格的同时补全类型信息。

小结

  • 变量默认不可变,遮蔽可换类型,mut 不能;const 编译期求值且必须标注类型,static 有固定内存地址;const fn 扩大编译期求值范围但有严格限制
  • let 支持元组/数组解构;_ 立即丢弃值,_x 允许未使用;变量离开作用域时按 LIFO 顺序自动 drop(RAII)
  • 标量类型中 bool 占 1 字节且不可做算术,char 是 4 字节 Unicode 标量值,二者方法族覆盖判断、转换与编码查询
  • 数值方法分算术类(abs/pow/clamp/div_euclid 等)与位类(count_ones/rotate_left 等),溢出处理有 wrapping_*/checked_*/saturating_*/overflowing_* 四族;运算符(+/== 等)是 std::ops trait 方法的语法糖,方法调用 a.abs() 则是完全限定语法 i32::abs(a) 的糖;Rust 没有 ++/-- 运算符,自增自减用 += 1 / -= 1AddAssign / SubAssign
  • 切片 &[T]/&str 是胖指针借用视图,统一数组、Vec、字符串的访问接口,详见《切片(Slice)》
  • String 堆分配且可增长(capacity/with_capacity/shrink_to_fit),&str 是栈上胖指针借用视图;函数参数优先 &str,返回值按所有权需求选择
  • 字符串修改三分法:等长原地改找 &mut strmake_ascii_*),变长找拥有者 Stringpush_str/truncate,配合 with_capacity 零分配),新值找 format! / to_uppercase 等转换方法;concat! 编译期拼接零运行时开销
  • 字符串 UTF-8 编码下遍历粒度有字节/码点/字形簇三种;format! 支持 {:?}/{:x}/{:>8} 等说明符
  • 类型转换优先用 From/Into(无损,自动互推)与 TryFrom/TryInto(有损返回 Result);as 适用于整数截断与 charu32;deref coercion 让 &String 自动转 &str

下一篇切片(Slice):序列的借用视图将讲切片的胖指针布局、创建与索引、常用方法与可变切片。