Rust 1.96.0 正式发布。这一版的核心亮点是标准库引入了全新的 Range 类型族,解决了长期以来 Range 不能 Copy 的痛点;同时 assert_matches! 宏进入稳定 API,让模式断言的调试信息不再是一片空白。此外,WebAssembly 目标的链接行为也做了更严格的收紧。下面逐项拆解。
Range 为什么不能 Copy——旧类型的坑
日常写 0..n 拿到的 core::ops::Range 直接实现了 Iterator,这意味着它持有迭代状态,一旦开始迭代就会消耗自身。把一个会"变"的类型标记为 Copy 是典型的 footgun——复制一份看似安全,但两份副本共享起始状态,迭代一次后行为就不可预期了。所以旧 Range 类型刻意不实现 Copy。
问题在于:很多时候你只是想存一个"区间"作为切片访问器,并不打算迭代它。比如编译器里常见的 Span(start, end),为了存进 Copy 结构体只能把起止拆成两个 usize 字段,啰嗦且容易出错。
新 Range 类型:IntoIterator 而非 Iterator
RFC 3550 的方案很简单——让区间类型实现 IntoIterator 而非 Iterator。这样类型本身只是静态的起止范围,可以安心 Copy;需要迭代时调用 .into_iter() 产生一个独立的迭代器,状态隔离,互不干扰。
1.96 稳定了以下新类型:
| 新类型 | 说明 |
|---|---|
core::range::Range |
start..end,Copy |
core::range::RangeFrom |
start..,Copy |
core::range::RangeInclusive |
start..=end,Copy,字段公开 |
后续版本还会补上 RangeFull(..)和 RangeTo(..end)的 re-export,以及 core::range::legacy::* 作为旧类型的归档位置。
一个直接可用的例子——把区间存进 Copy 结构体:
use core::range::Range;
#[derive(Clone, Copy)]
pub struct Span(Range<usize>);
impl Span {
pub fn new(start: usize, end: usize) -> Self {
Span(Range::new(start, end))
}
pub fn of(self, s: &str) -> &str {
&s[self.0]
}
}
fn main() {
let text = "hello, rust 1.96!";
let span = Span::new(7, 11);
// Span 是 Copy,可以随意复制使用
let another = span;
assert_eq!(span.of(text), "rust");
assert_eq!(another.of(text), "rust");
}
注意:目前
0..1语法仍然产生旧类型。在新 edition 切换之前,需要显式使用core::range::Range::new(start, end)或从core::range导入新类型来构造。
库作者的建议
如果你在公开 API 中接收区间参数,优先写 impl RangeBounds——它同时兼容新旧类型。如果必须用具体类型,倾向新类型,因为未来 edition 会把语法默认切换到新类型。
assert_matches!:断言失败时能看到值
以前写 assert!(matches!(val, Some(_))),失败时只能看到断言表达式本身,看不到 val 到底是什么。新稳定的 assert_matches! 宏在 panic 时会打印值的 Debug 表示,诊断效率直接提升。
use core::assert_matches;
fn get_random_number() -> u32 {
4 // chosen by a fair dice roll, guaranteed to be random
}
fn main() {
// 如果值不在 1..=6 范围内,panic 信息会包含实际值
assert_matches!(get_random_number(), 1..=6);
}
运行这段代码会 panic,输出类似:
assertion failed: `get_random_number()` matches `1..=6`
value: 4
对比旧写法 assert!(matches!(get_random_number(), 1..=6)),你只能看到断言表达式,看不到 4。
这个宏没有加入 prelude,因为不少第三方 crate(比如 assert_matches crate)已经提供了同名宏,直接加入会冲突。使用前需要手动 use core::assert_matches; 或 use std::assert_matches;。debug_assert_matches! 是它的 debug-only 版本,release 构建中不检查。
WebAssembly 链接:未定义符号不再静默变成导入
1.96 起,Wasm 目标的链接器不再默认传 --allow-undefined。以前未定义的符号会被自动转成从 env 模块的 Wasm 导入,链接阶段不报错,运行时才暴露问题——这掩盖了构建配置错误和符号命名失误。
现在未定义符号直接触发链接错误,问题在编译期就被抓住。
如果你确实需要旧行为(比如构建一个故意依赖外部导入的 Wasm 模块),两种恢复方式:
# 方式一:通过 RUSTFLAGS 恢复全局旧行为
RUSTFLAGS="-Clink-arg=--allow-undefined" cargo build --target wasm32-unknown-unknown
# 方式二:在源码中精确标注导入模块
# 对需要从 env 导入的 extern 块加属性
#[link(wasm_import_module = "env")]
extern "C" {
fn external_func();
}
方式二更精确,只对确实需要外部导入的符号放行,推荐优先使用。
其他稳定 API 与安全修复
稳定 API 清单(部分):
From<T> for AssertUnwindSafe<T>—catch_unwind场景更方便From<T> for LazyCell<T, F>/From<T> for LazyLock<T, F>— 延迟初始化类型可直接从值构造core::range::RangeToInclusive及其迭代器RangeToInclusiveItercore::range::RangeFromIter、core::range::RangeIter
Cargo 修复了两个安全漏洞(仅影响第三方 registry 用户,crates.io 不受影响):
- CVE-2026-5223(中危):crate tarball 中的符号链接提取问题
- CVE-2026-5222(低危):规范化 URL 导致的认证问题
如果你只用 crates.io,无需额外操作。使用私有 registry 的团队建议尽快升级。
升级与采纳清单
rustup update stable
升级后建议检查的事项:
- Wasm 项目:跑一次完整构建,确认没有因 stricter linking 而报错。如果有未定义符号,用
#[link(wasm_import_module)]精确标注,而非全局--allow-undefined。 - 区间相关代码:如果你有手动拆
start/end来绕过Copy限制的结构体,可以改用core::range::Range重写,减少字段数量和出错概率。 - 断言模式匹配:把
assert!(matches!(...))替换为assert_matches!,失败信息立刻变得有用。注意加use导入,避免与第三方 crate 冲突。 - 公开 API 中的 Range 类型:检查是否用了具体的旧
Range类型作参数,考虑改为impl RangeBounds或新core::range::Range,为未来 edition 切换做准备。
想提前体验后续特性的开发者可以切换到 beta 或 nightly:
rustup default beta # 或
rustup default nightly
遇到问题记得提交 bug report,帮助稳定下一个版本。