Rust 1.96 深度实战:Range 终于可 Copy、Cargo 双源依赖、Wasm 严格链接——从设计哲学到生产级迁移的完全指南(2026)
2026 年 5 月 28 日,Rust 1.96.0 正式发布。这个版本没有惊天动地的新语法,却在几个基础组件深处修复了积年已久的 API 设计瑕疵,同时给出了清晰、渐进的迁移路径。作为 Rust 开发者,理解这些变化的"为什么"和"怎么用",远比单纯浏览更新列表更有价值。本文将从设计哲学出发,深入每一个特性的底层原理,配合大量实战代码,帮你真正掌握这次更新。
一、背景:为什么 Rust 需要一次"长尾改进"
Rust 的版本发布节奏是每 6 周一个稳定版。从 1.0 到 1.96,已经走过了近 12 年。在这个过程中,标准库中的某些类型设计逐渐暴露出了"历史债务"——当时的设计决策在后来的实践中被证明不够理想,但因为向后兼容性承诺,无法直接修改。
Rust 团队选择的方式是:引入新类型,提供迁移路径,在未来 Edition 中完成切换。1.96 就是这种"渐进式修复"哲学的集中体现。
这次更新的三大核心特性:
- core::range 新类型:让范围类型终于支持 Copy
- Cargo 双源依赖:一个依赖,两种来源,按场景自动切换
- Wasm 链接规则严格化:从"宽容默认"到"严格默认"
再加上 assert_matches! 宏的稳定、多个实用 API 的稳定化,以及两个 Cargo 安全漏洞修复。看起来不炸裂,但每一个都直击日常开发痛点。
二、core::range:让范围类型真正"值"起来
2.1 旧世界的尴尬:Range 的"双重身份"困境
这是 Rust 标准库中最让人困惑的设计之一。我们每天都在写 0..10,但你知道它生成的 std::ops::Range 有多别扭吗?
let r = 1..10; // Range<usize>
let first = r.next(); // 作为 Iterator 使用
let r2 = r; // ❌ 编译错误:r 已被移动
为什么?因为 std::ops::Range 直接实现了 Iterator。在 Rust 的设计规范中,迭代器是独占的、一次性消费的资源——调用 next() 会消耗自身。因此,实现了 Iterator 的类型不能实现 Copy。
这导致了两个极其常见的痛点:
痛点一:无法放进 Copy 容器
当你需要一个可拷贝的切片索引器时,不得不把 start 和 end 拆开存储:
// 旧版本:手动拆字段,丢失 Range 语义
#[derive(Clone, Copy)]
struct Span {
start: usize,
end: usize,
}
impl Span {
fn of(self, s: &str) -> &str {
&s[self.start..self.end] // 又拼回了 Range...
}
}
这种手工作法丢失了 Range 自带的方法和语义,本质上是"拆了再拼"。
痛点二:RangeInclusive 的字段私有化
为了保证"已迭代完成"这一状态的正确性,旧版 RangeInclusive 的字段是私有的:
let range = 1..=10; // RangeInclusive<usize>
// range.start // ❌ 字段是私有的!
// range.end // ❌ 同样无法访问
你无法直接构造或解构它,只能通过 ..= 语法。这意味着你无法在运行时动态构建一个包含边界的范围——除非用 new() 方法,但那个方法的存在本身就说明了设计的别扭。
痛点三:泛型约束的尴尬
如果你想写一个接受任意范围的函数,你不得不使用 RangeBounds trait:
use std::ops::RangeBounds;
fn process<R: RangeBounds<usize>>(range: R) {
// ...
}
process(1..10); // OK
process(1..=10); // OK
process(..10); // OK
这本身没问题,但如果你想存储这个范围呢?RangeBounds 是一个 trait,不是具体类型。你无法定义 struct Config { range: impl RangeBounds<usize> }——impl trait 不能用在 struct 字段上。
2.2 新设计的核心思想:分离迭代能力
RFC 3550 引入了一套全新的范围类型,位于 core::range 模块:
| 新类型 | 对应旧类型 | 关键变化 |
|---|---|---|
core::range::Range | std::ops::Range | 不再实现 Iterator,实现 IntoIterator |
core::range::RangeFrom | std::ops::RangeFrom | 同上 |
core::range::RangeInclusive | std::ops::RangeInclusive | 字段公开,不再实现 Iterator |
| (未来加入) | std::ops::RangeFull | - |
| (未来加入) | std::ops::RangeTo | - |
关键改变就一句话:这些类型不再实现 Iterator,而是实现 IntoIterator。
这意味着类型本身可以作为纯数据自由拷贝,只有当你显式调用 .into_iter() 时,才会转移所有权并开始迭代。
用代码对比再清楚不过:
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(); // 显式消费
for i in iter {
println!("{}", i);
}
// 对比旧 Range(来自 std::ops)
let old_range = 1..10; // std::ops::Range<usize>
let _old_iter = old_range.into_iter(); // 旧 Range 本身就是 Iterator
// 此时 old_range 已被移动,不能再使用
2.3 深入理解:为什么 IntoIterator 而不是 Iterator?
这个设计选择的背后是 Rust 核心团队对"所有权语义"的深层思考。
Iterator trait 的 next() 方法签名是 fn next(&mut self) -> Option<Self::Item>——它通过 &mut self 消费迭代器。这意味着每次调用 next() 都在改变迭代器的内部状态。
而 IntoIterator 的 into_iter(self) 方法签名是 fn into_iter(self) -> Self::IntoIter——它通过值消费自身,返回一个新的迭代器类型。关键区别在于:类型本身只是一个"区间描述",迭代器才是"消费状态"。
这就实现了关注点分离:
core::range::Range= 纯数据,描述一个区间[start, end)core::range::Iter(由into_iter()返回)= 迭代状态,负责逐个产出值
2.4 实战一:让 Span 既 Copy 又体面
有了 core::range::Range,开头那个 Span 的例子终于可以优雅起来了:
use core::range::Range;
#[derive(Clone, Copy, Debug)]
pub struct Span(Range<usize>);
impl Span {
pub fn new(start: usize, end: usize) -> Self {
Span(Range { start, end })
}
pub fn of(self, s: &str) -> &str {
&s[self.0] // 直接使用新 Range 作为切片索引
}
pub fn len(self) -> usize {
self.0.end.saturating_sub(self.0.start)
}
pub fn is_empty(self) -> bool {
self.0.start >= self.0.end
}
}
fn main() {
let span = Span::new(0, 5);
let text = "Hello, Rust 1.96!";
let slice = span.of(text);
assert_eq!(slice, "Hello");
// Span 是 Copy 的,可以多次使用
let span2 = span;
assert_eq!(span2.of(text), "Hello");
assert_eq!(span.len(), 5);
}
2.5 实战二:编译器诊断中的 Range 表示
在编译器、代码分析工具中,我们经常需要存储源码位置。旧的 Range 不能 Copy,导致大量冗余代码:
// 旧方案:手动拆字段
#[derive(Clone, Copy)]
struct SourceSpan {
file_id: usize,
start: usize,
end: usize,
}
// 新方案:直接用 Range
use core::range::Range;
#[derive(Clone, Copy)]
struct SourceSpan {
file_id: usize,
range: Range<usize>,
}
impl SourceSpan {
fn contains(self, offset: usize) -> bool {
self.range.start <= offset && offset < self.range.end
}
fn merge(self, other: Self) -> Self {
Self {
file_id: self.file_id,
range: Range {
start: self.range.start.min(other.range.start),
end: self.range.end.max(other.range.end),
},
}
}
}
注意 merge 方法——如果用旧方案,你需要写 self.start.min(other.start) 和 self.end.max(other.end),Range 的语义被拆散了。新方案保持了 Range 的整体性,代码意图更加清晰。
2.6 实战三:RangeInclusive 字段公开的威力
旧版 RangeInclusive 的字段是私有的,这导致了一个匪夷所思的问题:你无法在 const 上下文中构造一个包含边界的范围。1.96 修复了这个问题:
use core::range::RangeInclusive;
// ✅ 字段公开,可以自由构造
const FULL_BYTE_RANGE: RangeInclusive<u8> = RangeInclusive { start: 0, end: 255 };
// ✅ 也可以自由解构
let RangeInclusive { start, end } = FULL_BYTE_RANGE;
assert_eq!(start, 0);
assert_eq!(end, 255);
// 实际应用:IP 地址范围校验
fn is_private_ip(octets: [u8; 4]) -> bool {
// 10.0.0.0 - 10.255.255.255
if octets[0] == 10 {
return true;
}
// 172.16.0.0 - 172.31.255.255
if octets[0] == 172 {
let second_range = RangeInclusive { start: 16u8, end: 31 };
return second_range.contains(&octets[1]);
}
// 192.168.0.0 - 192.168.255.255
if octets[0] == 192 && octets[1] == 168 {
return true;
}
false
}
2.7 迁移策略:库作者现在该怎么做?
新旧范围类型将在未来一个 Edition 中完成切换(.. 语法届时会生成 core::range 类型)。在此之前,你的公开 API 应该遵循兼容之道:
// ✅ 推荐:使用 trait bound 接受所有范围类型
pub fn process_range(range: impl std::ops::RangeBounds<usize>) {
// ...
}
// ✅ 如果需要存储范围,可以开始使用新类型,同时提供旧类型的转换
pub fn normalize_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 }
}
// ✅ 暴露两种构造方式
pub struct Query {
range: core::range::Range<usize>,
}
impl Query {
// 接受新类型
pub fn from_range(range: core::range::Range<usize>) -> Self {
Self { range }
}
// 兼容旧 Range 语法
pub fn from_std_range(range: std::ops::Range<usize>) -> Self {
Self {
range: core::range::Range {
start: range.start,
end: range.end,
},
}
}
}
2.8 性能分析:Copy vs Clone 的零成本抽象
有人可能会问:新 Range 的 Copy 语义是否有运行时开销?
答案是:没有。Copy trait 的语义是按位复制,对于 Range<usize>(本质就是两个 usize),编译器生成的代码和手动拷贝两个字段完全一致。
// 旧方案
let span = Span { start: 1, end: 10 };
let span2 = span; // Clone,需要显式调用
// 新方案
let span = Span(core::range::Range { start: 1, end: 10 });
let span2 = span; // Copy,自动按位复制
在优化后的汇编层面,两者生成的指令完全相同——都是两次 mov。Copy vs Clone 的区别只在类型系统层面,不影响运行时性能。
三、assert_matches!:断言失败时,让错误开口说话
3.1 assert!(matches!(...)) 的致命缺陷
测试中我们经常用 matches! 宏检查模式:
fn get_status() -> Result<u32, String> {
Ok(42)
}
#[test]
fn test_status() {
let result = get_status();
// 旧写法:matches! 只返回 bool,assert! 不知道实际值是什么
assert!(matches!(result, Ok(_)), "Expected Ok, got {:?}", result);
}
问题在于:
- 你必须手动写格式化字符串
"Expected Ok, got {:?}",否则断言失败时只会显示assertion failed matches!不捕获绑定变量,你无法在格式化字符串中引用匹配到的值- 每次都要写
assert!(matches!(...))这个冗长的组合
3.2 assert_matches! 的智能之处
1.96.0 新增的 assert_matches! 宏解决了这个痛点:失败时自动以 Debug 格式打印被检查的值。
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 显示了期望的模式。这种"所见即所得"的诊断,在测试失败时能帮你节省大量时间。
3.3 深入用法:枚举模式匹配
这是 assert_matches! 最实用的场景——检查枚举变体:
use core::assert_matches;
#[derive(Debug)]
enum ApiResponse {
Success { data: Vec<String> },
Error { code: u16, message: String },
Timeout,
}
fn parse_response(raw: &str) -> ApiResponse {
if raw.is_empty() {
ApiResponse::Timeout
} else if raw.starts_with("ERR") {
ApiResponse::Error {
code: 500,
message: raw.to_string(),
}
} else {
ApiResponse::Success {
data: vec![raw.to_string()],
}
}
}
#[test]
fn test_success_response() {
let resp = parse_response("hello");
assert_matches!(resp, ApiResponse::Success { .. });
}
#[test]
fn test_error_code() {
let resp = parse_response("ERR: timeout");
// 使用守卫条件进一步检查
assert_matches!(
resp,
ApiResponse::Error { code, .. } if code >= 400 && code < 500
);
}
#[test]
fn test_not_timeout() {
let resp = parse_response("hello");
// 否定检查:确保不是 Timeout
assert_matches!(resp, ApiResponse::Success { .. } | ApiResponse::Error { .. });
}
3.4 实战:构建类型状态机的断言套件
在类型状态机(Typestate Pattern)中,assert_matches! 可以用来验证状态转换的正确性:
use core::assert_matches;
#[derive(Debug, Clone)]
enum ConnectionState {
Disconnected,
Connecting { endpoint: String },
Connected { latency_ms: u32 },
Error { retries: u32, last_error: String },
}
struct Connection {
state: ConnectionState,
}
impl Connection {
fn new() -> Self {
Self { state: ConnectionState::Disconnected }
}
fn connect(&mut self, endpoint: &str) {
self.state = ConnectionState::Connecting {
endpoint: endpoint.to_string(),
};
}
fn on_connected(&mut self, latency_ms: u32) {
self.state = ConnectionState::Connected { latency_ms };
}
fn on_error(&mut self, error: &str) {
let retries = match &self.state {
ConnectionState::Error { retries, .. } => retries + 1,
_ => 1,
};
self.state = ConnectionState::Error {
retries,
last_error: error.to_string(),
};
}
}
#[test]
fn test_connection_lifecycle() {
let mut conn = Connection::new();
assert_matches!(conn.state, ConnectionState::Disconnected);
conn.connect("api.example.com:443");
assert_matches!(conn.state, ConnectionState::Connecting { .. });
conn.on_connected(23);
assert_matches!(conn.state, ConnectionState::Connected { latency_ms: 23 });
conn.on_error("ECONNRESET");
assert_matches!(conn.state, ConnectionState::Error { retries: 1, .. });
}
3.5 debug_assert_matches!:Release 构建零开销
与 debug_assert! 系列一致,debug_assert_matches! 在 release 构建中会被完全移除:
use core::debug_assert_matches;
fn process(data: &[u8]) {
// 只在 debug 构建中检查,release 中零开销
debug_assert_matches!(data.len(), 0..=1024);
// 实际处理逻辑...
}
这在性能敏感的路径上特别有用——你既想确保开发阶段的正确性,又不想在发布版本中留下任何断言开销。
3.6 与第三方 crate 的兼容性注意
assert_matches! 不在 prelude 中(避免与第三方 crate 的同名宏冲突),使用时需要显式引入:
// 标准库路径
use std::assert_matches; // 或 use core::assert_matches;
// 如果你之前用了第三方 crate(如 static_assertions)
// 不会产生冲突,因为路径不同
use static_assertions::assert_eq_size; // 不冲突
use core::assert_matches; // 标准库
四、Cargo 双源依赖:终结开发与发布的依赖割裂
4.1 痛点场景
这是本次更新中最实用的特性之一。先看一个真实场景:
你的团队维护一个内部公共工具库 my-utils。日常开发时,你需要依赖 Git 仓库的最新代码(修复 bug、新增特性),所以 Cargo.toml 这样写:
[dependencies]
my-utils = { git = "https://github.com/company/my-utils.git", branch = "main" }
但项目正式打包、发布、CI 构建时,必须使用私有 registry 或 crates.io 上的稳定版本:
[dependencies]
my-utils = { version = "1.2.0", registry = "my-private-registry" }
问题来了:一个依赖只能指定一种来源。
过去的常见做法:
- 手动切换:本地开发时改 Git 源,发布前改回 registry 版本。完全依赖人工操作,容易出错。
- Git patch:用
[patch]段临时覆盖。但[patch]只能覆盖版本号依赖,不能覆盖 Git 依赖,且需要维护额外的配置。 - Cargo workspace hack:在 workspace 根目录维护多个
Cargo.toml变体。复杂且脆弱。 - CI 脚本:在 CI 中用
sed动态替换。不可靠,且难以本地调试。
4.2 双源依赖的规则
Rust 1.96 终于支持在同一个依赖声明中同时指定两种来源:
[dependencies]
my-utils = { git = "https://github.com/company/my-utils.git", registry = "my-private-registry" }
规则清晰:
- 本地开发、日常构建:优先使用 Git 源(始终获取最新代码)
- 项目发布、
cargo publish构建、正式打包:自动降级使用 registry 注册表稳定版本
4.3 深入理解:双源的工作机制
双源依赖的核心逻辑在 Cargo 的"发布感知"构建中:
cargo build → 使用 Git 源(本地开发)
cargo build --release → 使用 Git 源(仍是本地构建)
cargo publish → 使用 registry 版本(发布构建)
关键区别在于 cargo publish 命令。当执行 cargo publish 时,Cargo 会检查所有依赖的来源:
- 如果依赖只声明了
git,Cargo 会报错——不能发布依赖 Git 源的 crate - 如果依赖同时声明了
git和registry/version,Cargo 在发布时会自动忽略git源,只使用registry/version - 如果依赖只声明了
version/registry,Cargo 正常使用
4.4 实战配置:多种场景
场景一:Git + 私有 Registry
[dependencies]
# 日常开发用 Git 最新代码,发布时用私有 registry 稳定版本
auth-lib = { git = "https://git.company.com/auth-lib.git", registry = "company-registry" }
场景二:Git + crates.io
[dependencies]
# 日常开发用 Git main 分支,发布时用 crates.io 版本
serde_json = { git = "https://github.com/serde-rs/json.git", version = "1.0" }
场景三:Git + version 双指定(最常见)
[dependencies]
# Git 源码的 version 字段仅用于发布时确定 registry 依赖
my-core = { git = "https://github.com/company/core.git", branch = "develop", version = "2.1" }
场景四:完整 Cargo.toml 示例
[package]
name = "my-service"
version = "0.5.0"
edition = "2021"
publish = ["company-registry"]
[dependencies]
# 公共依赖:只用 crates.io
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
# 内部依赖:双源
my-proto = { git = "https://git.company.com/proto.git", version = "0.3" }
my-auth = { git = "https://git.company.com/auth.git", registry = "company-registry" }
my-logger = { git = "https://git.company.com/logger.git", branch = "feat/structured-log", version = "1.0" }
[registries.company-registry]
index = "https://cargo.company.com/index"
4.5 团队工作流最佳实践
双源依赖解决了技术问题,但团队协作还需要一些流程配合:
Git 依赖的分支策略:
# ❌ 不推荐:依赖 main 分支,代码可能随时变动
my-lib = { git = "https://github.com/company/my-lib.git", version = "1.0" }
# ✅ 推荐:依赖特定分支或 tag
my-lib = { git = "https://github.com/company/my-lib.git", branch = "release/1.0", version = "1.0" }
# ✅ 更好:依赖 Git tag
my-lib = { git = "https://github.com/company/my-lib.git", tag = "v1.0.3", version = "1.0" }
版本号对齐检查:
Git 源码的 Cargo.toml 中的版本号应该与双源声明中的 version 字段保持兼容。建议在 CI 中加入检查:
#!/bin/bash
# check_dual_source_versions.sh
grep -E '^\w.*=.*\{.*git.*version' Cargo.toml | while read line; do
dep_name=$(echo "$line" | sed 's/^\([^ =]*\).*/\1/')
declared_version=$(echo "$line" | grep -oP 'version\s*=\s*"([^"]*)"' | head -1 | grep -oP '"[^"]*"')
echo "Dependency: $dep_name, Declared version: $declared_version"
done
4.6 常见陷阱与排障
陷阱一:双源的 version 不是 Git 源的版本
# ⚠️ version = "1.0" 指的是 registry 上的版本,不是 Git 仓库中的版本
my-lib = { git = "https://github.com/company/my-lib.git", version = "1.0" }
version 字段仅在发布时生效,用于从 registry 拉取对应版本。本地开发时,Cargo 完全忽略 version,只使用 git。
陷阱二:Git 依赖的 lock 文件行为
双源依赖中,Cargo.lock 记录的是 Git 源的具体 commit hash。当 Git 仓库有新提交时,需要手动 cargo update 来更新:
# 更新特定依赖到 Git 仓库最新提交
cargo update my-lib
# 或更新所有依赖
cargo update
陷阱三:私有 registry 认证配置
如果双源声明中使用了私有 registry,确保 .cargo/config.toml 中正确配置了认证:
# .cargo/config.toml
[registry]
global-credential = "cargo.company.com"
[net]
git-fetch-with-cli = true # 使用系统 Git,支持 SSH 认证
五、WebAssembly 链接规则严格化:从"宽容"到"严格"
5.1 变更内容
升级到 1.96 后,为 Wasm 目标编译时,链接器不再默认传递 --allow-undefined。这意味着任何未定义的链接符号将直接导致链接错误,而不再是默默地变成从 env 模块导入的 stub。
5.2 旧行为为什么危险
旧链接器会"好心"地将未定义符号变成来自 env 模块的导入。典型场景:
#[link(wasm_import_module = "my_host")]
extern "C" {
fn host_func();
}
fn main() {
unsafe { host_func(); }
}
如果你写错了函数名(比如 host_func 实际是 host_function),旧链接器不会报错,而是:
- 忽略
#[link(wasm_import_module = "my_host")] - 将
host_func变成一个来自env模块的未定义导入 - 你的 Wasm 模块在运行时可能静默失败或表现出怪异行为
这种"宽容"掩盖了配置错误,让你以为链接成功,实际上运行时会出各种诡异问题。更糟糕的是,由于 Wasm 的运行时错误通常不如原生平台那么直观,排查这种问题可能耗费数小时。
5.3 新行为:严格链接
1.96 的行为变更非常直接:未定义符号 → 链接报错,不再默默创建 stub。
error: ld.lld: error: undefined symbol: host_func
>>> referenced by main
>>> /path/to/main.o:(host_func)
这是一个典型的"提前失败"(fail fast)设计——在构建阶段暴露问题,而不是让问题溜到运行时。
5.4 如何恢复旧行为
如果你的项目确实需要允许未定义符号(比如动态加载场景),有两种方法恢复旧行为:
方法一:环境变量(全局)
RUSTFLAGS="-Clink-arg=--allow-undefined" cargo build --target wasm32-unknown-unknown
方法二:源码级显式注解(推荐)
在声明外部块的 extern 上添加 link(wasm_import_module = "env"),明确表达你的意图:
#[link(wasm_import_module = "env")] // 显式指出导入自 env 模块
extern "C" {
fn some_dynamic_import();
}
方法二更好,因为它是源码级的,不会影响项目中其他 Wasm 外部函数的严格检查。
5.5 实战:Wasm 项目迁移检查清单
// ❌ 升级后会链接错误的代码
#[link(wasm_import_module = "my_host")]
extern "C" {
fn host_compute(input: u32) -> u32; // 如果宿主环境没有这个函数名
}
// ✅ 确保函数名与宿主环境完全一致
#[link(wasm_import_module = "my_host")]
extern "C" {
fn host_compute(input: u32) -> u32; // 宿主环境必须导出 host_compute
}
// ✅ 如果确实需要动态导入
#[link(wasm_import_module = "env")]
extern "C" {
fn dynamic_callback() -> u32;
}
迁移步骤:
- 升级到 Rust 1.96
- 运行
cargo build --target wasm32-unknown-unknown - 如果出现链接错误,检查每个
extern "C"块的函数名拼写和#[link(...)]属性 - 如果使用了
wasm-bindgen,通常不需要修改(绑定生成器会自动处理符号) - 如果确实需要允许未定义符号,使用上述方法一或方法二
六、其他值得关注的稳定化 API
6.1 pointer::is_aligned
检查指针是否满足给定对齐,无需 unsafe 手动计算:
let ptr: *const u32 = &42u32;
assert!(ptr.is_aligned());
// 对齐检查在自定义分配器中特别有用
struct AlignedBuffer<const ALIGN: usize> {
ptr: *mut u8,
len: usize,
}
impl<const ALIGN: usize> AlignedBuffer<ALIGN> {
fn new(len: usize) -> Self {
let layout = std::alloc::Layout::from_size_align(len, ALIGN).unwrap();
let ptr = unsafe { std::alloc::alloc(layout) };
debug_assert!(ptr.is_aligned());
Self { ptr, len }
}
}
6.2 NonNull::is_aligned
同上,适用于非空指针:
use std::ptr::NonNull;
fn check_alignment(ptr: NonNull<u64>) -> bool {
ptr.is_aligned() // 检查是否满足 8 字节对齐
}
6.3 {slice, array}::as_flattened_mut
将 &mut [[T; N]] 重新解释为 &mut [T],便于对二维数组进行线性操作:
let mut matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
// 旧方式:需要 unsafe 或逐个元素处理
// 新方式:一行搞定
let flat: &mut [i32] = matrix.as_flattened_mut();
flat[4] = 0; // 修改 matrix[1][1]
assert_eq!(matrix[1][1], 0);
这在图像处理、矩阵运算等场景中特别有用——你可以安全地在行优先和列优先之间切换,而无需手动计算偏移量。
6.4 Option::take_if
条件性地取出值,失败时返回 None,类似于 filter 但获取所有权:
let mut x = Some(42);
let taken = x.take_if(|v| *v > 10);
// x 变为 None,taken 为 Some(42)
assert_eq!(x, None);
assert_eq!(taken, Some(42));
let mut y = Some(5);
let not_taken = y.take_if(|v| *v > 10);
// y 保持 Some(5),not_taken 为 None
assert_eq!(y, Some(5));
assert_eq!(not_taken, None);
实际应用——从任务队列中取出特定类型的任务:
#[derive(Debug)]
enum Task {
Compute(u32),
Download(String),
Shutdown,
}
fn process_shutdown(queue: &mut Vec<Option<Task>>) -> bool {
for slot in queue.iter_mut() {
if let Some(Task::Shutdown) = slot.take_if(|t| matches!(t, Task::Shutdown)) {
return true; // 找到并取出 Shutdown 任务
}
}
false
}
七、Cargo 安全漏洞修复
7.1 CVE-2026-5223(中危):软链接篡改
影响范围:仅第三方私有 registry 用户,crates.io 用户不受影响。
漏洞描述:第三方注册表的 crate 压缩包中如果包含符号链接(symlink),解压时可能通过软链接篡改本地文件。攻击者可以在 crate 包中放置指向敏感路径的软链接,当 Cargo 解压时会跟随链接写入攻击者控制的内容。
1.96 的修复方式:直接拒绝解析包内软链接。如果 crate 包中包含软链接,Cargo 会报错并拒绝安装。
7.2 CVE-2026-5222(低危):URL 标准化认证漏洞
影响范围:仅使用 Git 协议注册表的用户。
漏洞描述:Git 协议注册表的 URL 标准化过程中存在认证缺陷,可能导致认证信息被错误地应用到不同的源地址。
1.96 优化了源地址校验逻辑,确保 URL 标准化过程中认证信息不会被误用。
7.3 你需要做什么?
- 如果你只使用 crates.io:什么都不用做,这两个漏洞不影响你
- 如果你使用第三方私有 registry:立即升级到 1.96,修复已在工具链中
- 如果你维护私有 registry:建议在服务端也加入软链接检测,作为纵深防御
八、升级指南与最佳实践
8.1 升级命令
# 使用 rustup 升级
rustup update stable
# 验证版本
rustc --version
# 应输出:rustc 1.96.0 (...)
# 如果需要指定版本
rustup install 1.96.0
rustup default 1.96.0
8.2 升级后的立即收益
Wasm 项目:运行
cargo build --target wasm32-unknown-unknown,如果链接通过,说明你的符号声明正确。如果出现链接错误,恭喜你——1.96 帮你提前发现了一个潜在的运行时 bug。测试套件:将
assert!(matches!(...))替换为assert_matches!(...),失败信息会更有用。可以批量替换:
# 查找所有可替换的断言
rg 'assert!\(matches!' --type rust
# 替换模板
# assert!(matches!(expr, pattern)) → assert_matches!(expr, pattern)
# assert!(matches!(expr, pattern), msg) → assert_matches!(expr, pattern) // 错误信息现在是自动的
- 库作者:开始在公开 API 中使用
impl RangeBounds而不是具体的std::ops::Range,为未来的 Edition 切换做好准备。
8.3 长期迁移路线图
| 时间线 | 建议动作 |
|---|---|
| 现在(1.96) | 升级工具链,使用 assert_matches!,评估 core::range 新类型 |
| 下一个 Edition | .. 语法将生成 core::range 类型,旧代码可能需要适配 |
| 长期 | std::ops::Range 可能被弃用,全面迁移到 core::range |
九、总结与展望
Rust 1.96.0 是一次典型的"长尾改进"式发布。它没有引入惊天动地的新语法,却在几个基础组件的深处修复了积年已久的 API 设计瑕疵。
三大核心变更回顾:
core::range:将"区间描述"和"迭代状态"分离,让 Range 类型终于支持 Copy。这是一个教科书级的 API 重设计——不破坏旧代码,引入新类型提供渐进迁移路径,在未来 Edition 中完成切换。
Cargo 双源依赖:一个声明搞定开发与发布两种场景,终结了长期以来的手动切换之痛。这个特性对团队协作的影响尤其深远——再也不用担心"本地能跑,CI 挂了"的尴尬。
Wasm 严格链接:从"宽容默认"到"严格默认",这是一个安全哲学的转变——让错误在编译时暴露,而不是在运行时爆炸。
Rust 的成熟体现在哪里?
不在于每隔几个版本就搞出个惊天动地的新特性,而在于这种持续打磨、渐进修复的耐心。每一个版本都在让标准库更一致、工具链更安全、开发体验更顺畅。1.96 正是这样一枚"精益求精"的补丁。
作为开发者,我的建议是:立即升级。这些改进看似细微,但日积月累,它们会让你的 Rust 代码更简洁、更安全、更易维护。而 Rust 正是通过这种持续迭代,一步步兑现"零成本抽象"和"安全默认"的承诺。
参考链接: