Rust 1.96 正式发布:可复制 Range 类型、Cargo 双源依赖、Wasm 安全加固——系统编程语言的又一次实用主义进化
从 Range 可 Copy 到 Cargo 双源依赖,Rust 1.96 用三个关键更新,彻底解决开发者的日常痛点。这不是一次炫技式的版本升级,而是一场针对实际工程问题的精准打击。
写在前面:为什么这次更新值得关注?
2026年7月4日,Rust 团队发布了 1.96.0 稳定版。如果你习惯了 Rust 每6周一次的版本节奏,可能会觉得“又是一次小版本更新”。但这次不一样——它带来的三个核心特性,直接击中了 Rust 开发者的日常痛点:
- Range 类型终于可 Copy 了:困扰开发者多年的问题,终于有了优雅的解决方案
- Cargo 支持双源依赖:本地调试用 Git,发布用 registry,一套配置搞定
- Wasm 链接规则严格化:在编译期就拦截配置错误,而不是等到运行时才发现
更关键的是,这次更新体现了 Rust 团队的实用主义哲学:不追求大而全的特性堆砌,而是精准解决开发者在真实项目中遇到的具体问题。
一、Range 可 Copy:一个困扰 Rust 社区多年的设计困境
1.1 问题的根源:为什么旧版 Range 不能 Copy?
在 Rust 1.96 之前,我们日常写的 0..10 这样的语法,生成的是 std::ops::Range 类型。这个类型有一个让人头疼的限制:它不能实现 Copy trait。
为什么?因为在 Rust 的设计规范中,迭代器(Iterator)是独占、一次性消费的资源。标准库直接为 Range 实现了 Iterator trait,而 Rust 的规则是:实现 Iterator 的类型不能同时实现 Copy。
这个设计背后的逻辑是:
// 旧版 Range 的行为
let range = 0..10;
let copy = range; // 这实际上是 move,而不是 copy!
// 为什么?因为 range 本身就是一个迭代器
for i in range {
println!("{}", i);
}
// 此时 range 已经被消费,无法再次使用
迭代器代表了一个“正在进行的遍历过程”,它维护了当前遍历到的位置。如果允许 Copy,就会出现两个迭代器同时遍历同一个序列的情况——这违背了迭代器的语义。
1.2 实际痛点:当代码变得冗余
这个限制在实际开发中会带来很多不便。最典型的场景是:你想把一个切片访问器存储在结构体中。
假设你在开发一个文本处理工具,需要存储“从哪到哪”的切片范围:
// 旧版本(Rust 1.95 及之前)
pub struct Span {
start: usize,
end: usize, // 不得不拆分成两个字段
}
impl Span {
pub fn of(&self, s: &str) -> &str {
&s[self.start..self.end] // 使用时再拼接
}
}
为什么不能直接存 Range<usize>?因为 Range<usize> 没有实现 Copy,而你的 Span 结构体需要 Copy 语义(可能要放进 Cell、存在栈上、或者作为函数返回值)。
这就导致了一个非常不优雅的结果:明明是一个“区间”的概念,却被迫拆成两个字段来存储。
1.3 RFC 3550 的解决方案:新 Range 类型
Rust 1.96 正式稳定了 RFC 3550,提出了一套全新的方案:
核心思路:让 Range 类型“不再是迭代器本身”,而是“可以被转换为迭代器的数据容器”。
新版本引入了独立的 core::range 系列类型:
// 新的 Range 类型(Rust 1.96+)
use core::range::Range;
#[derive(Clone, Copy)] // 现在可以直接 Copy!
pub struct Span(Range<usize>);
impl Span {
pub fn of(self, s: &str) -> &str {
&s[self.0] // 直接使用,代码更简洁
}
}
为什么新 Range 可以 Copy?
因为它不再直接实现 Iterator,而是实现 IntoIterator:
// 新 Range 类型的设计
pub struct Range<Idx> {
pub start: Idx,
pub end: Idx,
}
// 实现 IntoIterator,而不是 Iterator
impl<Idx> IntoIterator for Range<Idx> {
type Item = Idx;
type IntoIter = RangeIter<Idx>; // 迭代器是一个独立的类型
fn into_iter(self) -> Self::IntoIter {
RangeIter::new(self.start, self.end)
}
}
// 因此可以实现 Copy
impl<Idx: Copy> Copy for Range<Idx> {}
关键区别:
| 特性 | 旧 Range (std::ops::Range) | 新 Range (core::range::Range) |
|---|---|---|
| 实现 Iterator | ✓ | ✗ |
| 实现 IntoIterator | ✓ | ✓ |
| 实现 Copy | ✗ | ✓ |
| 实现 Clone | ✗(部分情况) | ✓ |
| 字段可见性 | 私有 | 公开 |
1.4 实战示例:代码变得多么优雅
让我们看一个实际的例子。假设你在开发一个 JSON 解析器,需要存储 token 的位置信息:
// ====== 旧版本写法(Rust 1.95 及之前)======
use std::ops::Range as OldRange;
#[derive(Debug)]
pub struct Token {
kind: TokenKind,
start: usize, // 不得不拆分
end: usize, // 冗余的字段
}
impl Token {
pub fn new(kind: TokenKind, start: usize, end: usize) -> Self {
Self { kind, start, end }
}
pub fn span(&self) -> OldRange<usize> {
self.start..self.end // 每次都要拼接
}
}
// ====== 新版本写法(Rust 1.96+)======
use core::range::Range;
#[derive(Debug, Clone, Copy)] // 现在可以直接 Copy
pub struct Token {
kind: TokenKind,
span: Range<usize>, // 一个字段搞定
}
impl Token {
pub fn new(kind: TokenKind, start: usize, end: usize) -> Self {
Self {
kind,
span: Range { start, end },
}
}
pub fn text<'a>(&self, source: &'a str) -> &'a str {
&source[self.span] // 直接使用
}
}
1.5 迁移路径:旧代码怎么办?
Rust 团队非常注重向后兼容性。目前,0..10 这样的语法仍然产生旧版 std::ops::Range 类型,而不是新的 core::range::Range。
未来的计划是:
- 当前阶段:新旧类型并存,旧语法仍产生旧类型
- 下一阶段:引入
core::range::legacy::*作为旧类型的新命名空间 - Edition 升级:在未来某个 Edition(可能是 Rust 2027)中,
0..10语法将产生新的core::range::Range类型
迁移建议:
// 如果你想立即使用新类型
use core::range::Range;
let range = Range { start: 0, end: 10 };
// 如果你需要旧类型(临时兼容)
use std::ops::Range as LegacyRange;
let legacy = 0..10; // 当前仍然是旧类型
1.6 新 Range 的其他改进
除了支持 Copy,新的 Range 类型还有其他改进:
字段公开:
// 旧版 RangeInclusive 的字段是私有的
let range = 0..=10;
// range.start // 编译错误:字段私有
// 新版 RangeInclusive 的字段是公开的
use core::range::RangeInclusive;
let range = RangeInclusive { start: 0, end: 10 };
println!("start: {}, end: {}", range.start, range.end); // OK
更好的 API 设计:
use core::range::{Range, RangeFrom, RangeInclusive, RangeTo};
// 各种 Range 变体
let range = Range { start: 0, end: 10 }; // 0..10
let range_from = RangeFrom { start: 0 }; // 0..
let range_inclusive = RangeInclusive { start: 0, end: 10 }; // 0..=10
let range_to = RangeTo { end: 10 }; // ..10
// 所有这些类型都支持 Copy
let copy = range;
let another = range; // 编译通过!
二、Cargo 双源依赖:本地调试与发布配置的终极解决方案
2.1 痛点场景:开发与发布的依赖冲突
这是困扰 Rust 团队多年的一个实际问题。假设你的团队维护一个内部公共工具库 my-utils:
- 开发阶段:你需要依赖 Git 仓库的最新代码,修复 bug、测试新特性
- 发布阶段:必须使用私有 registry 或 crates.io 上的稳定版本
在 Rust 1.96 之前,你只能二选一:
# 方案 A:依赖 Git 源(适合开发,不适合发布)
[dependencies]
my-utils = { git = "https://github.com/xxx/my-utils.git", branch = "main" }
# 方案 B:依赖 registry(适合发布,不适合开发调试)
[dependencies]
my-utils = { version = "1.2.0", registry = "my-private-registry" }
过去的痛苦流程:
- 开发时,手动改成 Git 源
- 提交前,改回 registry 版本
- 忘记改回来 → CI 构建失败
- 团队协作,每个人都在重复这个过程...
2.2 Rust 1.96 的解决方案:单依赖双源配置
Rust 1.96 给出了优雅的解决方案:一个依赖项同时指定两个来源,根据构建场景自动切换。
# Cargo.toml
[dependencies]
my-utils = {
git = "https://github.com/xxx/my-utils.git",
registry = "my-private-registry"
}
切换规则:
| 场景 | 使用的源 |
|---|---|
本地开发 (cargo build) | Git 源 |
运行测试 (cargo test) | Git 源 |
发布构建 (cargo publish) | Registry 源 |
| CI 构建(非发布) | Git 源 |
--registry 参数指定 | Registry 源 |
2.3 实战示例:完整的配置流程
步骤 1:配置私有 registry
# .cargo/config.toml
[registries]
my-private-registry = { index = "https://github.com/xxx/crates-index" }
步骤 2:在 Cargo.toml 中配置双源依赖
[package]
name = "my-app"
version = "0.1.0"
edition = "2021"
[dependencies]
# 双源依赖:Git + Registry
my-utils = {
git = "https://github.com/company/my-utils.git",
branch = "develop", # 可选:指定分支
registry = "my-private-registry",
version = "1.2.0" # 可选:指定 registry 版本
}
# 也可以省略 version,自动使用 registry 上的最新版本
my-another-lib = {
git = "https://github.com/company/my-another-lib.git",
registry = "my-private-registry"
}
步骤 3:验证配置
# 本地开发:使用 Git 源
cargo build
# 输出:Downloading my-utils from git repository...
# 发布:使用 Registry 源
cargo publish --registry my-private-registry
# 输出:Using my-utils from registry my-private-registry...
2.4 高级用法:结合 patch 和 replace
Cargo 双源依赖还可以与其他功能结合使用:
场景:临时使用本地修改的库
# Cargo.toml
[dependencies]
my-utils = {
git = "https://github.com/company/my-utils.git",
registry = "my-private-registry"
}
# 临时 patch 到本地路径
[patch."https://github.com/company/my-utils.git"]
my-utils = { path = "../my-utils-local" }
这样,本地开发时会使用 ../my-utils-local,而发布时自动使用 registry 版本。
2.5 为什么这个功能如此重要?
这不是一个“锦上添花”的功能,而是解决真实工作流程痛点的实用性改进:
- 消除人工操作:不再需要手动切换依赖配置
- 防止发布事故:避免忘记改回 registry 版本导致的 CI 失败
- 团队协作友好:统一的配置,每个人都能遵循相同的流程
- CI/CD 集成:构建脚本不需要特殊处理,
cargo publish自动使用 registry
三、Wasm 链接规则严格化:从运行时错误到编译期错误
3.1 背景:WebAssembly 的未定义符号问题
在 Rust 1.96 之前,编译 WebAssembly 目标时,Rust 默认会向链接器传递 --allow-undefined 参数。这意味着:
如果有未定义的符号,链接器不会报错,而是将其自动转换为从 env 模块的导入。
这看起来很“宽容”,但实际上是一个隐患:
// 假设你写了一个 Wasm 函数
#[link(wasm_import_module = "env")]
extern "C" {
fn my_import_function() -> i32;
}
pub fn call_external() -> i32 {
unsafe { my_import_function() }
}
如果你不小心写错了函数名:
// 拼写错误:my_import_functon(少了一个 i)
extern "C" {
fn my_import_functon() -> i32;
}
旧版本的行为:
编译通过 ✓
链接通过 ✓
生成 .wasm 文件 ✓
运行时错误 ✗ ← 问题在这里!
你会得到一个运行时错误:“import env.my_import_functon not found”。
3.2 Rust 1.96 的改进:编译期拦截
Rust 1.96 彻底移除了 --allow-undefined 的默认行为。现在:
如果有未定义符号,直接在编译期报错。
error: linking with `rust-lld` failed
|
= note: rust-lld: error: undefined symbol: my_import_functon
这意味着你可以在开发阶段就发现问题,而不是等到部署到生产环境后才发现。
3.3 实战影响:迁移指南
如果你依赖旧行为(极少数情况):
# 显式启用 --allow-undefined
RUSTFLAGS="-C link-arg=--allow-undefined" cargo build --target wasm32-unknown-unknown
最佳实践:
// 正确的做法:明确声明所有导入
#[link(wasm_import_module = "env")]
extern "C" {
// 显式列出所有需要的导入
fn console_log(ptr: *const u8, len: usize);
fn get_timestamp() -> u64;
}
// 或者使用 wasm-bindgen
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
3.4 为什么这个改进很重要?
| 场景 | 旧版本 | 新版本(Rust 1.96+) |
|---|---|---|
| 拼写错误 | 运行时崩溃 | 编译期报错 |
| 符号重命名 | 运行时崩溃 | 编译期报错 |
| 配置遗漏 | 运行时崩溃 | 编译期报错 |
核心收益:将问题从“运行时发现”提前到“编译期发现”,大大降低调试成本。
四、新增便捷断言宏:assert_matches!
4.1 痛点:旧版本的繁琐写法
在 Rust 1.96 之前,如果你想断言一个值匹配某个模式,需要这样写:
enum Status {
Idle,
Working(u32),
Offline,
}
let status = Status::Working(10086);
// 旧写法:嵌套两层宏
assert!(matches!(status, Status::Working(_)));
虽然可行,但有几个问题:
- 代码冗余:
assert!包裹matches!,两层嵌套 - 错误信息不清晰:断言失败时,只知道 “assertion failed”,不知道实际值是什么
- 无法提取匹配值:如果想在断言的同时提取值,需要额外代码
4.2 新版本:assert_matches! 宏
Rust 1.96 新增了 assert_matches! 和 debug_assert_matches! 宏:
use std::assert_matches::assert_matches;
enum Status {
Idle,
Working(u32),
Offline,
}
let status = Status::Working(10086);
// 新写法:直接匹配模式,语义更清晰
assert_matches!(status, Status::Working(_));
// 如果断言失败,会输出 Debug 格式的实际值
// assert_matches!(status, Status::Idle);
// → assertion failed: `status` does not match `Status::Idle`
// actual: Working(10086)
4.3 高级用法:提取匹配值
use std::assert_matches::assert_matches;
let status = Status::Working(10086);
// 使用 @ 绑定语法提取值
assert_matches!(status, Status::Working(id @ 10086) => {
println!("Working on order: {}", id);
});
// 或者使用 if let 风格
if let Status::Working(id) = status {
assert_eq!(id, 10086);
}
4.4 debug_assert_matches!:调试版本专用
use std::assert_matches::debug_assert_matches;
// 只在 debug 模式下检查,release 模式下会被移除
debug_assert_matches!(status, Status::Working(_));
// 等价于
if cfg!(debug_assertions) {
assert_matches!(status, Status::Working(_));
}
五、安全漏洞修复:CVE-2025-15661、CVE-2026-55199、CVE-2026-55200
Rust 1.96.1 点版本修复了三个安全漏洞,建议所有用户尽快升级。
5.1 CVE-2026-5223(中危):第三方注册表 crate 压缩包软链接篡改
影响范围:仅影响使用第三方私有 registry 的用户,官方 crates.io 用户不受影响。
问题描述:第三方 registry 的 crate 压缩包中的软链接可被利用篡改本地文件。
修复措施:新版本将直接拒绝解析包内软链接。
缓解建议:
# 立即升级
rustup update stable
# 检查是否使用了第三方 registry
grep -r "registry" .cargo/config.toml
5.2 CVE-2026-5222(低危):Git 协议注册表 URL 标准化认证漏洞
影响范围:使用 Git 协议的私有 registry 用户。
问题描述:Git 协议注册表 URL 标准化存在认证绕过风险。
修复措施:优化源地址校验逻辑。
5.3 其他修复
- Cargo HTTP 客户端缺少重试与超时机制:已添加默认重试和超时配置
- MIR 优化中存在的错误编译问题:已修复
六、完整更新清单:还有什么值得关注?
6.1 标准库 API 稳定
Rust 1.96 稳定了 20+ 新 API,以下是几个实用的:
// slice::array_windows:编译期安全的滑动窗口
let arr = [1, 2, 3, 4, 5];
for window in arr.array_windows::<2>() {
println!("{:?}", window); // [1, 2], [2, 3], [3, 4], [4, 5]
}
// f32::fma:融合乘加运算(精度更高)
let a: f32 = 0.1;
let b: f32 = 0.2;
let c: f32 = 0.3;
let result = a.mul_add(b, c); // a * b + c,精度更高
6.2 编译器改进
- 增量编译优化:大型项目的增量编译速度提升约 15%
- 错误信息改进:生命周期错误提示更友好
- 类型推断增强:复杂泛型场景下的类型推断更准确
6.3 Cargo 改进
# 新增:cargo report future-incompatibilities
# 提前发现未来版本的不兼容变更
cargo report future-incompatibilities
# 新增:cargo tree --duplicates
# 查看依赖树中的重复版本
cargo tree --duplicates
七、总结:实用主义者的胜利
Rust 1.96 不是一次“革命性”的更新,但它是一次“实用性”的胜利。它解决的三个核心问题——Range 可 Copy、Cargo 双源依赖、Wasm 链接严格化——都是开发者在日常工作中反复遇到的真实痛点。
升级建议
# 立即升级
rustup update stable
# 查看当前版本
rustc --version
# rustc 1.96.0 (xxxxxxx 2026-05-28)
# 如果需要使用新 Range 类型
use core::range::{Range, RangeFrom, RangeInclusive};
迁移清单
- 升级到 Rust 1.96.1(包含安全修复)
- 检查是否有依赖第三方私有 registry(CVE-2026-5223)
- 更新 WebAssembly 构建脚本(移除
--allow-undefined依赖) - 尝试新的
assert_matches!宏替换旧的assert!(matches!(...)) - 评估是否需要迁移到新的
core::range类型
展望
Rust 的版本迭代节奏稳定,每 6 周一个新版本。从这次更新可以看出,Rust 团队非常注重实用性和向后兼容性——新特性不会破坏旧代码,而是提供更好的替代方案。
Rust 1.96 的哲学:不追求炫技,只解决实际问题。这正是 Rust 能够在系统编程领域持续增长的原因。
参考资料: