Rust 1.96:告别 Range 的 Copy 陷阱,断言模式匹配终于来了

2026-05-28 16 预计阅读时间:1 分钟
来源:blog.rust-lang.org AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:8 分钟

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..endCopy
core::range::RangeFrom start..Copy
core::range::RangeInclusive start..=endCopy,字段公开

后续版本还会补上 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 及其迭代器 RangeToInclusiveIter
  • core::range::RangeFromItercore::range::RangeIter

Cargo 修复了两个安全漏洞(仅影响第三方 registry 用户,crates.io 不受影响):

  • CVE-2026-5223(中危):crate tarball 中的符号链接提取问题
  • CVE-2026-5222(低危):规范化 URL 导致的认证问题

如果你只用 crates.io,无需额外操作。使用私有 registry 的团队建议尽快升级。

升级与采纳清单

rustup update stable

升级后建议检查的事项:

  1. Wasm 项目:跑一次完整构建,确认没有因 stricter linking 而报错。如果有未定义符号,用 #[link(wasm_import_module)] 精确标注,而非全局 --allow-undefined
  2. 区间相关代码:如果你有手动拆 start/end 来绕过 Copy 限制的结构体,可以改用 core::range::Range 重写,减少字段数量和出错概率。
  3. 断言模式匹配:把 assert!(matches!(...)) 替换为 assert_matches!,失败信息立刻变得有用。注意加 use 导入,避免与第三方 crate 冲突。
  4. 公开 API 中的 Range 类型:检查是否用了具体的旧 Range 类型作参数,考虑改为 impl RangeBounds 或新 core::range::Range,为未来 edition 切换做准备。

想提前体验后续特性的开发者可以切换到 beta 或 nightly:

rustup default beta    # 或
rustup default nightly

遇到问题记得提交 bug report,帮助稳定下一个版本。


相关推荐