std 是 Rust 默认提供的标准库 crate。它不只是若干容器和工具函数:从 OptionResult、迭代器、格式化,到文件、网络、线程、进程和平台路径,日常 Rust 程序的大多数基础能力都从这里开始。正确的使用方式不是背模块名,而是先识别问题属于数据建模、内存所有权、I/O、并发还是平台边界,再选择最小且语义匹配的 API。

本文以稳定版、Edition 2024 为基线,示例只依赖标准库。API 是否可用仍应以项目的 MSRV 和本机工具链文档为准。

先建立边界:语言、coreallocstd

Rust 的“基础设施”分四层。它们不是互斥的四套 API,而是由下向上增加能力;std 会重新导出许多下层类型,所以普通应用通常只需写 std::...

可依赖的能力典型内容适用场景
语言与编译器语法、类型检查、借用检查、原生类型i32boolstr、引用、闭包所有 Rust 代码
core不要求堆分配、操作系统或标准运行时OptionResultIteratorFuturefmt固件、内核、no_std
alloccore 上增加全局分配器VecStringBoxRc可分配但没有完整 OS 的环境
stdcore / alloc 上增加平台与运行时抽象文件、网络、线程、进程、环境变量默认的命令行、服务端、桌面程序

默认情况下,编译器会为 crate 提供 std,并把 Rust prelude 放进每个模块的作用域。因此普通应用可以直接使用 VecStringOptionResult。这不代表它们“属于语言”;其中大多数由下层库定义并由 std 暴露。

#![no_std] 会移除这条默认路径。此时可直接依赖 core;若目标具备全局分配器,还可显式使用 alloc

1
2
3
4
5
6
#![no_std]

extern crate alloc;

use alloc::vec::Vec;
use core::fmt;

no_std 二进制还需要目标相关的启动代码、panic 处理和(如使用 alloc)全局分配器。不要为了缩小依赖而机械移除 std:只要程序需要文件、网络、线程、进程或宿主环境,std 通常就是正确边界。

名称为什么有时不用 use

Rust 有三种常见的名称来源:

  1. 完整路径std::collections::HashMap。最明确,适合偶发使用或文档说明。
  2. 显式导入use std::collections::HashMap;。适合模块稳定依赖的名称。
  3. 自动导入的 prelude:只包含高频、基础的名称,不是整个标准库。

Edition 2024 的 prelude 包含 Option / Some / NoneResult / Ok / ErrVecString,以及 CloneDefaultFromIntoTryFromIteratorIntoIteratorFuture 等常用 trait。标准宏也默认可用,例如 println!format!vec!assert!matches!

下列名称仍需要自己导入:

需要显式导入原因与常见位置
HashMapBTreeMapVecDequestd::collections 中的具体容器不是 prelude 内容
PathPathBufFile路径和文件是平台 / I/O 边界,必须显式表达依赖
ReadWriteBufReadtrait 必须在作用域内,才能调用其提供的方法
DisplayErrorAdd并非所有模块都需要的 trait
DurationInstantMutexArc时间和并发同步原语

不要通过“能否省略 use”判断一个类型的重要性。prelude 的目标只是减少几乎所有模块都会出现的样板代码;它会随 Edition 增补少量名称,但刻意保持很小。

从一个只用 std 的程序认识模块边界

下面的命令行程序读取一个文件,统计空白分隔的单词,并把结果写到标准输出。它涵盖参数、非 Unicode 路径、文件 I/O、错误传播、标准输出锁和格式化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::env;
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;

fn main() -> io::Result<()> {
let path = match env::args_os().nth(1) {
Some(arg) => PathBuf::from(arg),
None => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"usage: count <file>",
));
}
};

let content = fs::read_to_string(&path)?;
let words = content.split_whitespace().count();

let stdout = io::stdout();
let mut out = stdout.lock();
writeln!(out, "{}: {words} words", path.display())?;
Ok(())
}

这里的边界值得逐项看清:

  • args_os() 返回平台原生的 OsString,不会因文件名不是 UTF-8 而失败;若命令参数本来必须是 Unicode 文本,才选 args()
  • PathBuf 拥有路径;fs::read_to_string(&path) 只借用它,并在读取失败时返回 io::Error
  • ?io::Result 中的失败直接交给 main,没有用 unwrap() 把可预期的文件错误变成 panic。
  • stdout().lock() 让一段连续输出持有同一把输出锁;writeln! 通过 io::Write 返回可处理的 I/O 错误,而不是把输出错误隐藏掉。

这也是查标准库的实用顺序:先从问题的资源边界开始,再沿着类型和 trait 跳转,而不是从模块目录逐个浏览。

高频模块地图

标准库模块很多,但大部分业务代码集中在下面几组。先看这一张地图,再进入单个 API 的 rustdoc 页面,效率最高。

问题首选模块 / 类型首先确认的约束
缺失、失败、比较、转换optionresultcmpconverterror缺失是正常分支,还是需要向上报告的失败?
连续数据和文本Vec<T>[T]Stringstrslice是否需要拥有、修改或增长数据?
按键、去重、有序、队列collections是否需要稳定顺序、范围查询或优先级?
惰性数据变换iterIntoIterator是借用、可变借用,还是消费元素?
格式化与文本解析fmtstrstring面向用户还是调试;转换会不会分配?
文件、字节流、终端pathfsio是路径操作、文件系统操作,还是通用 reader / writer?
配置、子进程、时钟envprocesstime输入是否为 Unicode;需要墙上时间还是耗时测量?
线程和共享状态threadsync数据如何移动、共享、取消和回收?
TCP、UDP、套接字地址net是否接受阻塞 I/O;是否还需要 HTTP、TLS 或异步运行时?
OS / C 边界ffios文本是否可假设 UTF-8;资源句柄的所有权在哪里?
低层控制memptrpinallocpanic能否先用安全的高层 API;安全不变量是什么?

其中 optionresultitercmp 等很多能力实际来自 corefsnetprocessthread 等则依赖宿主平台。这个区别决定了代码能否进入 no_std 环境,也提示了可移植性边界。

数据、集合与迭代:先决定所有权,再选容器

OptionResultError

Option<T> 表示“可能不存在,但这不是异常”;Result<T, E> 表示“操作可能失败,调用方必须面对失败原因”。不要用空字符串、-1null 风格的哨兵值混淆两种语义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use std::collections::HashMap;

fn count_words(text: &str) -> HashMap<&str, usize> {
let mut counts = HashMap::new();

for word in text.split_whitespace() {
*counts.entry(word).or_insert(0) += 1;
}

counts
}

fn main() {
let counts = count_words("rust std rust");
assert_eq!(counts.get("rust"), Some(&2));
assert_eq!(counts.get("missing"), None);
}

这个函数没有为单词分配新的 String:map 的键是借用 text&str,因此返回值不能活得比输入更久。若索引要脱离原始文本保存,就把键改为 String 并在插入处显式 to_owned();分配发生的位置必须清楚。

错误处理的默认规则也很简单:

情形表达方式例子
正常缺失Option<T>map.get(key)slice.first()
可恢复失败Result<T, E>读文件、解析整数、建立连接
调用方需要统一错误契约自定义错误类型实现 std::error::Error库的公开 API
代码不变量被破坏panic! / expect()“这个 worker 不应 panic”之类的内部断言

应用入口可以先返回 io::Result<()> 或具体错误类型;库不应因为普通输入、文件不存在或网络断开而 panicstd::error::Error 负责把错误做成可组合的 trait 对象或错误链,不会替你定义业务语义;错误枚举的变体和上下文仍应由 API 设计者决定。

连续存储:数组、切片、VecstrString

类型所有权与大小默认用途
[T; N]内联拥有;长度编译期固定固定协议字段、小型缓冲区
[T] / &[T]动态长度切片;通常借用让函数接受任意连续存储的一段数据
Vec<T>堆上拥有、可增长需要在运行期追加、删除或收集元素
str / &strUTF-8 文本切片;通常借用只读文本参数和视图
String堆上拥有、可增长的 UTF-8 文本构建、修改或保存文本

优先让只读函数接受 &[T]&str,而不是 &Vec<T>&String。前者不绑定调用方的容器,也避免暗示函数需要拥有或修改它。

String 按 UTF-8 存储,不能按任意整数下标切出“第 N 个字符”。str::chars() 按 Unicode 标量值迭代,graphemes() 这类用户感知字符分割则不在标准库中,需要专门 crate。二进制数据应使用 &[u8] / Vec<u8>,不要伪装成字符串。

HashMap / HashSet 适合按键查找和去重,但遍历顺序不承诺稳定。日志、测试快照、协议输出或用户界面需要确定顺序时,应排序后输出,或直接选择 BTreeMap / BTreeSet。队列用 VecDeque,优先队列用 BinaryHeap;不要反复对 Vec 调用 remove(0)

迭代器:惰性组合,终止时才做工作

Iterator 的适配器通常惰性:mapfiltertake 只是构造处理管道,直到 collectsumfindfor 等消费者出现才执行。这样能避免中间 Vec,也把“如何遍历”和“何时分配结果”分开。

1
2
3
4
5
6
7
8
9
10
fn even_ids(ids: &[u64]) -> impl Iterator<Item = u64> + '_ {
ids.iter().copied().filter(|id| *id % 2 == 0)
}

fn main() {
let ids = [1, 2, 3, 4, 5];
let result: Vec<_> = even_ids(&ids).collect();

assert_eq!(result, [2, 4]);
}

for item in value 本质上调用 IntoIterator::into_iter(value),所以循环右侧决定所有权:for x in &items 借用元素,for x in &mut items 可变借用元素,for x in items 通常消费容器。遇到“值在循环后为什么不能再用”的报错时,先检查这一步,而不是先加 clone()

格式化、转换与 trait:把语义写进接口

标准库的大量能力是 trait:`

  • Display 是面向用户或稳定文本协议的格式;Debug 是面向开发者诊断的格式,输出不应被当成稳定协议。
  • From<T> / Into<U> 是不会失败的转换;可能失败时用 TryFrom<T> / TryInto<U>
  • AsRef<T> 提供廉价的借用视图;Borrow<T> 还承诺与原值一致的比较、排序和哈希语义,常用于 map 的异构查找。
  • Read / Write 抽象字节输入和输出;Iterator 抽象按顺序产生元素。

格式化宏的分工也不同:

API结果与成本适用位置
format!创建拥有的 String,通常会分配确实要保存或传递格式化结果
write! / writeln!写进已有 writer 或 String避免中间字符串,处理输出错误
print! / println!直接写标准输出简单 CLI 输出;写失败会 panic
format_args!构造延迟格式化参数自定义日志 / 格式化 API 的底层接口

parse() 通过 FromStr 工作,返回的目标类型常需要显式标注:let port: u16 = raw.parse()?;。这种标注不是冗余,它让“文本将被解释为何种值”在边界处可审查。

路径、文件与通用 I/O:区分文件系统和字节流

std::path 只处理路径结构;std::fs 执行文件系统操作;std::io 用 trait 抽象任何字节 reader / writer。把这三层混在一起是 I/O 代码最常见的误解。

路径不是 UTF-8 字符串

Path 对应借用视图,类似 strPathBuf 对应拥有、可修改的路径,类似 String。二者内部以 OsStr / OsString 表示,因此能保留当前平台允许的非 Unicode 文件名。

1
2
3
4
5
6
7
8
9
10
11
12
13
use std::path::{Path, PathBuf};

fn report_path(root: &Path, name: &str) -> PathBuf {
let mut path = root.join("reports");
path.push(name);
path.set_extension("txt");
path
}

fn main() {
let path = report_path(Path::new("output"), "today");
assert_eq!(path, Path::new("output/reports/today.txt"));
}

joinpushextensionfile_name 等 API 组合或检查路径,不要手写 /\\。但不要把 Path 的词法操作当作安全隔离:components()join() 不会解析符号链接,且不会替你验证不可信路径是否仍在某个根目录内。需要访问真实文件系统时再考虑 canonicalize(),并注意检查与实际打开文件之间仍可能存在竞态。

fs 适合一口气完成,io 适合流式处理

小型 UTF-8 文本可直接用 fs::read_to_string;二进制文件可用 fs::read。文件很大、需要按行处理、或处理对象不一定是文件时,使用 FileBufReaderBufWriter 与 I/O trait。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::path::Path;

fn read_non_empty_lines(path: &Path) -> io::Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut lines = Vec::new();

for line in reader.lines() {
let line = line?;
if !line.trim().is_empty() {
lines.push(line);
}
}

Ok(lines)
}

I/O 的几个硬约束:

  • Read::read 可以在未到 EOF 时只填充部分缓冲区;协议要求固定长度时用 read_exact,读到结尾时用 read_to_endread_to_string
  • 高频、小块读取或写入应使用 BufReader / BufWriter,减少系统调用。需要确认数据真的交给底层 writer 时显式 flush();析构时发生的 flush 错误无法可靠地交给调用方处理。
  • BufRead::lines() 每次分配一个 String 并去掉行尾换行符。需要复用缓冲区或精确保留字节时,改用 read_lineread_until 或底层 Read API。
  • FileTcpStreamVec<u8> 都可以实现 Read / Write。函数参数优先接受 impl Readimpl Write 或泛型 bound,只有真正需要文件元数据时才要求 File

环境、进程与时间:把平台状态当成不可信输入

模块常用 API关键注意事项
std::envargs_osvarvar_oscurrent_dir环境变量和工作目录都可被外部改变;var 只接受 Unicode 值
std::processCommandChildExitStatusCommand 默认不经过 shell;显式传参数并检查退出状态
std::timeDurationInstantSystemTime测量耗时用 Instant;显示或记录现实时间用 SystemTime

Command::new("git").args(["status", "--short"]) 会把两个字符串作为两个参数传给进程,不进行 shell 展开、通配符替换或管道解析。这既更可预测,也避免把不可信文本拼接进 shell 命令的风险。若确实需要 shell 语义,必须显式启动相应 shell,并承担其转义规则和注入边界。

Instant 只有“经过了多久”的意义,不能格式化成日期;SystemTime 表示墙上时间,时钟校正可能使 duration_since 返回错误。不要用 SystemTime 计算超时,也不要把 Instant 写进跨进程持久化数据。

线程与同步:先选消息传递,再讨论共享可变状态

std::thread 提供 OS 线程。thread::spawn 的闭包及其捕获值通常必须满足 Send + 'static,因为线程可能比创建它的栈帧活得更久。只需要在一个作用域内并行处理借用数据时,优先考虑 thread::scope,它能让编译器验证所有子线程会在离开作用域前结束。

需求标准库选择边界
独占堆分配Box<T>单一所有者,适合递归类型或大对象
单线程共享所有权Rc<T> + Weak<T>不可跨线程;Weak 用于打破引用环
单线程内部可变性Cell<T> / RefCell<T>RefCell 在运行时检查借用,违规会 panic
跨线程共享所有权Arc<T>只解决计数,不自动让内部数据可变或线程安全
互斥读写Mutex<T>等待时阻塞 OS 线程;锁 guard 离开作用域即释放
多读单写RwLock<T>仅在读多写少且测量证明收益时考虑
一次初始化OnceLock<T> / LazyLock<T>避免 static mut,用于线程安全全局初始化
工作结果回传sync::mpsc多生产者、单消费者的消息通道

消息通道能让 worker 保有自己的数据,把最终结果交回一个接收者,避免过早引入共享锁:

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
use std::sync::mpsc;
use std::thread;

fn sum_in_workers(numbers: &[u64]) -> u64 {
let (tx, rx) = mpsc::channel();
let mut workers = Vec::new();

for chunk in numbers.chunks(2) {
let tx = tx.clone();
let values = chunk.to_vec();

workers.push(thread::spawn(move || {
let subtotal: u64 = values.into_iter().sum();
tx.send(subtotal).expect("receiver remains alive");
}));
}

drop(tx);
let total: u64 = rx.into_iter().sum();

for worker in workers {
worker.join().expect("worker must not panic");
}

total
}

fn main() {
assert_eq!(sum_in_workers(&[1, 2, 3, 4, 5]), 15);
}

原始 tx 必须在接收迭代前 drop;否则 receiver 仍看到一个活着的发送端,会无限等待下一个值。示例里的两个 expect() 都是明确的内部不变量:接收端在 worker 发送前仍存活,worker 不应 panic。对于外部输入、I/O 或可预期的运行时故障,函数应改为返回 Result 并让调用方处理。

Mutex::lock()RwLock 的加锁方法还会报告中毒(poisoning):若持锁线程 panic,后续获取者必须决定内部状态能否继续使用。不要把锁 guard 持有到不必要的工作中;临界区应只做读写共享状态的最小操作。std 不提供异步运行时,因而 std::sync::Mutex 也不适合跨 .await 持有;异步程序应按所选运行时的并发模型设计。

网络与异步边界

std::net 提供基础的阻塞式网络能力:TcpListenerTcpStreamUdpSocketSocketAddrToSocketAddrsTcpStream 同时实现 Read / Write,所以文件和 TCP 协议处理可以共享一部分字节流代码。

它刻意不提供 HTTP、TLS、WebSocket、JSON 编解码或异步任务调度。set_nonblocking(true) 只会改变套接字的阻塞行为,不会凭空提供 reactor、计时器、取消传播或 .await 执行器。需要这些语义时,引入专门 crate 和运行时是正常设计,不应在 std 之上手搓半套异步框架。

网络代码必须明确消息边界:TCP 是连续字节流,不保留“某一次 write 对应某一次 read”的边界。长度前缀、分隔符或成熟协议解析器都可以;“每次 read 恰好拿到一个请求”不可以。

宏、断言与 panic:便利不等于错误处理

标准宏是 std 体验的一部分,但语义各不相同:

用途不该承担的责任
assert!assert_eq!测试和不可违反的不变量用户输入、网络或文件错误
debug_assert!仅调试构建的内部检查有副作用的业务逻辑
matches!简洁测试模式是否匹配替代需要绑定数据的完整 match
vec!构造 Vec不必要的临时集合
dbg!开发期快速诊断稳定日志或生产输出
todo!unimplemented!明确 panic 的占位已交付的可达业务路径
include_str!env!编译期嵌入文本 / 环境值运行时可变配置

panic! 表示当前任务无法安全继续,例如内存不变量被破坏。Result 表示调用方仍能做出有意义的恢复或报告。两者的选择是 API 合同,不是代码量的选择;“先 unwrap(),以后再补错误处理”会把边界不清的问题推迟到最难排查的时刻。

FFI、平台模块与 I/O 安全

跨语言或跨平台边界时,文本和资源句柄都不能再按普通 String / 整数处理:

  • OsStr / OsString 表示平台原生文本,可能不是 UTF-8;适合文件名、环境变量和命令参数。
  • CStr / CString 表示 NUL 结尾的 C 字符串;CString::new 会拒绝内部 NUL。
  • std::os 包含 Unix / Windows 专用扩展,应把它们限制在最薄的平台适配层,业务层继续使用 PathFileTcpStream 等可移植类型。
  • 文件描述符或 Windows handle 受 I/O 安全模型约束。安全 API 应接收拥有或借用的句柄类型,而不是把随意的整数当作可关闭、可读写的资源。

这类代码常常需要 unsafe,但不应让 unsafe 向上扩散。把原始指针、原始 handle、布局约束和 C ABI 封装在很小的模块内,公开 API 则用 Result、所有权和借用表达调用方需要遵守的规则。

何时停止只用 std

“标准库优先”是依赖选择的起点,不是禁止生态 crate 的信条。std 适合基础能力和小型工具;当问题本身含有更高层语义时,应选择维护良好的专用库。

需求std 是否足够原因
文件、路径、命令行参数、线程、TCP / UDP通常足够这些是标准库的明确边界
JSON / TOML / YAML不够数据格式及其兼容性策略不属于 std
HTTP、TLS、数据库协议不够需要协议实现、安全更新和生态互操作
异步 I/O、任务取消、定时器不够需要运行时与 reactor 模型
Unicode 分词、用户感知字符簇不够str 只保证 UTF-8 和 Unicode 标量值操作
日志、CLI 参数定义、日期时区通常不够配置、格式与平台策略远超基础抽象

先写清需求,再引入依赖。反过来,若需求只是读取文件、解析一行文本、遍历集合或启动一个子进程,先查 std 往往能减少构建时间、依赖树和安全更新面。

查文档的正确顺序

在线标准库文档的 crate 首页会列出模块、原生类型、标准宏和 prelude。阅读具体 API 时,按下面顺序过滤信息:

  1. 路径和签名:参数是借用、可变借用还是所有权转移;返回 OptionResult 还是直接值。
  2. Trait Implementations:一个类型实现了哪些能力;例如 TcpStreamRead / WriteVec<T>Deref<Target = [T]>
  3. Panics / Errors / Safety:panic 前提、错误种类和 unsafe 调用者义务比示例更重要。
  4. 稳定性标记:确认 API 是否稳定、何时稳定、是否受 feature gate 限制;稳定版文档也会展示实验性项目。
  5. Source 链接:需要理解性能、边界处理或实现约束时再读;先依赖公开合同,不要把实现细节当 API 保证。

本机工具链版本和线上 stable 文档可能不同。需要与项目 MSRV 严格一致时,优先使用:

1
2
3
4
rustup doc --std
rustup doc --core
rustup doc --alloc
cargo doc --open

前 3 条打开当前工具链自带的文档;cargo doc --open 则把项目依赖和本地类型一起生成,适合沿着实际调用关系跳转。

常见误区

  • std 当成“所有 Rust 功能”:原生类型由编译器提供,很多基础 trait 和类型来自 corestd 在其上增加完整宿主环境。
  • 以为 prelude 导入了所有常用名称HashMapPathReadDisplay 等仍要显式导入。
  • 把路径当 String 拼接:跨平台代码应使用 Path / PathBuf,并保留 OsStr 语义。
  • 假设一次 read 填满缓冲区:字节流 API 允许短读;固定长度协议必须使用 read_exact 或自行累计。
  • 依赖 HashMap 的遍历顺序:该顺序不是稳定合同;需要确定输出时排序或改用 BTree 容器。
  • Rc<RefCell<T>> 处理跨线程共享:它们是单线程工具;跨线程从 Arc 加消息传递或同步原语开始。
  • 认为 std::net 等于异步网络栈:它提供基础 socket,不提供 async executor、HTTP 或 TLS。
  • unwrap() 处理外部世界:文件、网络、环境变量和解析输入都会失败,应返回或匹配 Result
  • Path::canonicalize 当作唯一安全校验:它访问文件系统,仍需在真实打开、权限和竞态模型中整体设计。

官方入口

标准库的价值不在于“没有依赖”,而在于它给出了跨 crate、跨平台都能依赖的最小公共语义。先用这些语义把所有权、失败路径和资源边界写清楚;只有问题本身超出这一层时,再用生态 crate 补上协议、运行时或领域能力。