Rust 1.96.0 是一次"长尾改进"式的版本发布------它没有引入惊天动地的新语法,却在几个基础组件的深处,修复了积年已久的 API 设计瑕疵,同时给出了清晰、渐进的迁移路径。作为 Rust 开发者,理解这些变化的"为什么"和"怎么用",会比单纯浏览更新列表更有价值。
本文将对三个核心特性进行深度解析,并附上可运行的代码示例。文末还会简要梳理其他值得关注的稳定化 API 与安全加固。
一、core::range:让范围类型真正"值"起来
1.1 旧世界的尴尬
长久以来,标准库中的范围类型(std::ops::Range, RangeInclusive 等)处于一种微妙的"双重身份"困境:
rust
let r = 1..10; // Range<usize>
let first = r.next(); // 作为 Iterator 使用
let r2 = r; // 编译错误:r 已被移动
因为它们直接实现了 Iterator,调用 next() 会消耗自身,所以无法实现 Copy。这导致了两个常见痛点:
-
无法放进
Copy容器当你需要一个可拷贝的切片索引器时,不得不把
start和end拆开存储:rust#[derive(Clone, Copy)] struct Span { start: usize, end: usize, }这种手工作法丢失了
Range自带的方法和语意。 -
RangeInclusive的字段私有化为了保证"已迭代完成"这一状态的正确性,旧版
RangeInclusive的字段是私有的,你无法直接构造或解构它,只能通过..=语法。
1.2 新设计的核心思想:分离迭代能力
RFC 3550 引入了一套全新的范围类型,位于 core::range 模块:
core::range::Rangecore::range::RangeFromcore::range::RangeInclusive- 以及未来会加入的
RangeFull,RangeTo等
关键改变就一句话:这些类型不再实现 Iterator,而是实现 IntoIterator 。这意味着类型本身可以作为纯数据自由拷贝,只有当你显式调用 .into_iter() 时,才会转移所有权并开始迭代。
用代码对比再清楚不过:
rust
use core::range::Range;
// 新 Range 实现了 Copy
let range: Range<usize> = Range { start: 1, end: 10 };
let copy = range; // 普通拷贝,不消耗
assert_eq!(copy.start, 1); // 可以继续访问字段
// 要迭代,必须显式转换
let iter = range.into_iter(); // range 被消耗(但因为它是Copy,这里只是拷贝了一份)
for i in iter { /* ... */ }
// 对比旧 Range(来自 std::ops)
let old_range = 1..10;
let old_iter = old_range.into_iter(); // 旧 Range 本身就是 Iterator,into_iter 返回自身
// 此时 old_range 已被移动
1.3 实战:让 Span 既 Copy 又体面
有了 core::range::Range,开头那个 Span 的例子终于可以优雅起来了:
rust
use core::range::Range;
#[derive(Clone, Copy)]
struct Span(Range<usize>);
impl Span {
pub fn of(self, s: &str) -> &str {
// 直接使用新 Range 作为切片索引
&s[self.0]
}
}
fn main() {
let span = Span(Range { start: 0, end: 5 });
let text = "Hello, Rust!";
let slice = span.of(text);
assert_eq!(slice, "Hello");
}
RangeInclusive 也有了同样的改进,并且字段变为公开:
rust
use core::range::RangeInclusive;
let inclusive = RangeInclusive { start: 1, end: 10 };
// 字段可访问
assert_eq!(inclusive.end, 10);
1.4 迁移策略:库作者现在该怎么做?
新旧范围类型将在未来一个 Edition 中完成切换(.. 语法届时会生成 core::range 类型)。在此之前,你的公开 API 应该遵循兼容之道:
rust
// 推荐:使用 trait bound 接受所有范围类型
pub fn process_range(range: impl std::ops::RangeBounds<usize>) {
// ...
}
// 如果需要存储范围,可以开始使用新类型,同时提供旧类型的转换
pub fn store_range(range: impl std::ops::RangeBounds<usize>) -> core::range::Range<usize> {
use std::ops::Bound;
let start = match range.start_bound() {
Bound::Included(&s) => s,
Bound::Excluded(&s) => s + 1,
Bound::Unbounded => 0,
};
let end = match range.end_bound() {
Bound::Included(&e) => e + 1,
Bound::Excluded(&e) => e,
Bound::Unbounded => usize::MAX, // 按需处理
};
core::range::Range { start, end }
}
这样,你的库就同时服务了还停留在旧范围的世界,以及率先拥抱新世界的用户。
二、assert_matches!:断言失败时,让错误开口说话
2.1 assert!(matches!(...)) 的致命缺陷
测试时我们常用 matches! 宏检查模式:
rust
fn get_number() -> u32 { 42 }
#[test]
fn test_number_range() {
let n = get_number();
assert!(matches!(n, 1..=6), "number should be in range 1..=6, got {}", n);
}
当断言失败,你会看到类似这样的输出:
thread 'test_number_range' panicked at 'number should be in range 1..=6, got 42', src/main.rs:5:5
虽然我们手动加了 got {},但每次都要额外处理格式,麻烦且容易忘。如果直接写 assert!(matches!(n, 1..=6)),失败信息只会干巴巴地告诉你 assertion failed,而不会打印实际值------调试体验很差。
2.2 assert_matches! 的智能之处
1.96.0 新增的 assert_matches! 宏解决了这个痛点:失败时自动以 Debug 格式打印被检查的值。
rust
use core::assert_matches;
fn get_number() -> u32 { 42 }
fn main() {
assert_matches!(get_number(), 1..=6);
}
输出会变成:
thread 'main' panicked at 'assertion failed: `(left matches right)`
left: `42`,
right: `1..=6`', src/main.rs:5:5
left 直接给出了实际值 42,right 显示了期望的模式。这种"所见即所得"的诊断,在测试失败时能帮你节省大量时间,尤其是当值复杂(如嵌套枚举、大型结构体)时。
2.3 深入用法:更复杂模式匹配
这两个宏支持所有 matches! 能用的模式,包括守卫:
rust
#[derive(Debug)]
enum Response {
Data(Vec<u8>),
Error { code: u16, message: String },
}
fn handle(response: Response) {
use core::assert_matches;
// 检查是否是错误,且状态码为 404
assert_matches!(
response,
Response::Error { code: 404, .. }
);
}
由于 assert_matches! 不在 prelude 中(避免与第三方 crate 的同名宏冲突),使用时记得 use std::assert_matches; 或 use core::assert_matches;。
三、WebAssembly 链接器:从"宽容"到"严格"
3.1 变更内容
升级到 1.96 后,为 Wasm 目标编译时,链接器不再默认传递 --allow-undefined。这意味着任何未定义的链接符号将直接导致链接错误 ,而不再是默默地变成从 "env" 模块导入的 stub。
3.2 为什么这样改
旧行为很容易掩盖配置错误。典型场景:
rust
#[link(wasm_import_module = "my_host")]
extern "C" {
fn host_func();
}
fn main() {
unsafe { host_func(); }
}
如果你写错了函数名(比如 host_func 实际是 host_function),旧链接器会"好心"地将 host_func 变成一个来自 "env" 模块的未定义导入,你的 Wasm 模块在运行时可能会静默失败或表现出怪异行为。现在,你会直接得到一个链接错误,指出 host_func 未定义,迫使你立即修复。
3.3 如果你的确需要这种"宽容"
如果你的项目故意依赖这种自动 stub(例如某些动态加载场景),有两种方法恢复行为:
方法一:环境变量(全局)
bash
RUSTFLAGS="-Clink-arg=--allow-undefined" cargo build --target wasm32-unknown-unknown
方法二:源码级显式注解(推荐)
在声明外部块的 extern 上添加 link(wasm_import_module = "env"),明确表达你的意图:
rust
#[link(wasm_import_module = "env")] // 显式指出导入自 env 模块
extern "C" {
fn some_dynamic_import();
}
这样既维持了严格检查,又保留了必要的灵活性。
3.4 实战检查清单
- 如果使用了
wasm-bindgen或其他绑定生成器,通常不会受影响,因为它们会自动处理符号。 - 若你手写了
extern "C"块,请确认所有函数名与宿主环境的实际导出完全一致。 - 升级后立即运行
cargo build --target wasm32-unknown-unknown,如果出现链接错误,仔细检查函数名拼写和#[link(...)]属性。
四、其他值得关注的稳定化与安全更新
4.1 新稳定的 API 精选
这次还稳定了一批实用的 API,几个值得注意的例子:
-
pointer::is_aligned检查指针是否满足给定对齐,无需
unsafe手动计算。rustlet ptr: *const u32 = &42u32; assert!(ptr.is_aligned()); -
NonNull::is_aligned:同上,适用于非空指针。 -
{slice, array}::as_flattened_mut可以将
&mut [[T; N]]重新解释为&mut [T],便于对二维数组进行线性操作。 -
Option::take_if条件性地取出值,失败时返回
None,类似于filter但获取所有权:rustlet mut x = Some(42); let taken = x.take_if(|v| *v > 10); // x 变为 None,taken 为 Some(42)
这些 API 虽然零散,却在日常编码中能减少不少 unsafe 和样板代码。
4.2 Cargo 安全加固
1.96 修复了两个影响第三方 registry 用户的漏洞:
- CVE-2026-5223:涉及 crate 包中符号链接的安全提取问题(中等严重性)。
- CVE-2026-5222:涉及使用规范化 URL 进行身份验证时的缺陷(低严重性)。
如果你仅使用 crates.io,则不受影响。但无论是否受影响,保持工具链最新总是明智之举。
总结:一次为未来铺路的"体验性"更新
Rust 1.96.0 没有激动人心的语法糖,却是在 API 设计的一致性、调试的人性化、构建的安全默认值三个维度上的扎实进步。它再次展示了 Rust 团队的成熟风格:发现问题 → 深思熟虑 → 给出平滑的迁移方案 → 分阶段落地。
作为开发者,你可以这么做:
- 立即升级,享受更优的断言诊断和更安全的 Wasm 链接。
- 在测试中用上
assert_matches!,让你的失败信息不再沉默。 - 在库代码中开始使用
impl RangeBounds并评估core::range新类型,为未来的 Edition 切换做好准备。
每一次版本迭代,都是让代码库变得更健壮、更易维护的契机。Rust 1.96.0 正是这样一枚"精益求精"的补丁,值得我们细细消化并应用到实际工作中。