编程 Rust 1.96 完全实战指南:core::range 可 Copy 化、assert_matches! 与 Cargo 双源依赖的工程全貌

2026-07-17 12:47:30 +0800 CST views 7

Rust 1.96 深度拆解:当 Range 终于能「Copy」——RFC 3550 如何用 IntoIterator 解耦数据与迭代,顺带修好 Wasm 链接与 Cargo 双源依赖

2026 年 5 月 28 日,Rust 1.96.0 稳定版悄然发布。它没有一个像「async/await」那样惊天动地的语法糖,也没有像「const generics」那样改变整个生态的能力跃迁。它做的是另一种更难、也更能体现 Rust 工程哲学的事:在几个最基础、最被日常使用的 API 的深处,修掉那些积攒了数年、每一个老 Rustacean 都曾绊过一跤的设计瑕疵。

一、背景:六周一次,但每一次都有它的「长尾」

Rust 以每六周一个稳定版的节奏前进。这种高频发布的好处是:新能力可以快速落地,而不必等三年一次的大版本。但代价是——任何一个破坏性的改动都会被无限放大。所以 Rust 团队对「稳定 API 的修改」极其保守:一旦某个 API 进入 stable,它就几乎被永久锁定。

这带来一个经典困境:早期的设计决策,哪怕后来被证明是错的,也很难再改。Range 类型正是这样一个例子。

1.1 一个你每天都写、却从没真正拥有过的类型

let r = 0..10;      // 这行代码你写过无数次
let s = &text[r];   // 用 Range 去切片字符串
for i in 0..10 { }  // 或者作为 for 循环的范围

0..10 这个语法糖,生成的是 std::ops::Range<usize>。它是 Rust 里最高频使用的类型之一,几乎出现在每一段代码里。然而,绝大多数 Rust 开发者并不知道——这个类型,你不能 Copy 它,也不能轻易把它塞进一个需要 Clone/Copy 的结构体里

为什么?答案藏在「迭代器」这个词里。这是本文要解开的第一个核心谜题。

1.2 为什么这件事拖了九年才修?

如果你好奇「既然这么别扭,当年为什么不一次做对」,答案要回到 2014—2015 年 Rust 1.0 之前的设计现场。

那时候,for i in 0..n 这种「范围循环」必须工作——它是所有教学示例的第一行。要让 for 循环跑起来,范围类型就得是某种「可迭代的东西」。在 Rust 早期,最直接、最少样板的选择,就是让 Range 直接实现 Iterator。这样 for i in 0..n 一行就能用,不需要任何额外的 into_iter() 胶水。

这是一个典型的「当时正确、后来反噬」的取舍:为了让入门语法优雅,把「迭代能力」焊死在了「区间数据」上。等到 Rust 生态壮大、大家开始写解析器、编译器、文本处理库,需要在结构体里存「一个区间」时,这个焊接点的代价才真正显现。而此时 std::ops::Range 早已稳定,动它就意味着潜在的破坏性变更——于是这一等,就是九年。

1.96 的解法之所以优雅,正是因为它绕开了「改旧类型」这个雷区,转而在 core 里放一个全新、干净的替代物。

二、核心痛点:为什么旧的 Range 不能 Copy?

2.1 Iterator 与 Copy 的天然互斥

Rust 的 std::ops::Range 直接实现了 Iterator trait:

impl Iterator for Range<usize> {
    type Item = usize;
    fn next(&mut self) -> Option<usize> {
        // 每次调用都会推进内部的 start 指针
    }
}

Iterator::next 的签名是 fn next(&mut self) -> Option<Self::Item>——它会修改自身状态来推进迭代。一个会被修改内部状态、且被设计为「一次性消费」的类型,天然不可能实现 Copy

Copy 的语义是「按位复制后,原件和副本完全等价、可独立使用」。但 Range 一旦开始迭代,它的 start 字段就被改写了——你复制的到底是「迭代到一半的状态」还是「原始区间」?这正是 Copy 所不允许的歧义。所以编译器干脆禁止 Range: Copy

2.2 真实世界里的三处痛点

这听起来是语言洁癖,但落到工程里就是实实在在的麻烦:

痛点一:无法成为 Copy 结构体的字段

#[derive(Clone, Copy)]   // ❌ 编译错误
struct Span {
    range: std::ops::Range<usize>,  // Range 不是 Copy,Span 也不可能是 Copy
}

如果你写一个做源码解析、文本切片的库,想用一个 Span 来标记「第 3 到第 7 个字符」并让它随值自由传递,你被迫退化成手动存两个字段:

#[derive(Clone, Copy)]   // ✅ 但很丑
struct Span {
    start: usize,
    end: usize,
}
impl Span {
    fn slice<'a>(&self, s: &'a str) -> &'a str { &s[self.start..self.end] }
}

你丢失了 Range 自带的一切语义和方法,还要自己小心别把 start/end 写反。

痛点二:迭代即消费

let r = 1..10;
let first = r.next();   // r 的内部状态被推进
let r2 = r;             // ❌ 错误:r 在 next() 调用后已部分移动/借用

你想先看看区间的第一个元素,再把它传给别的函数?对不起,做不到。因为 next() 借走了 &mut self,之后 r 处于一个「半迭代」的模糊状态,编译器拒绝让你再把它当完整值使用。

痛点三:RangeInclusive 的私有化字段

std::ops::RangeInclusive(也就是 a..=b)更尴尬:为了保证「已迭代完成」这个内部状态正确,它的 start/end 字段是私有的。你既不能 RangeInclusive { start: 0, end: 10 } 直接构造,也不能解构它,只能依赖 ..= 语法糖。一旦你想在泛型代码里统一处理各种 Range,私有字段就成了绊脚石。

这三个痛点,每一个单独看都不致命,但叠加起来,就是无数 Rust 开发者心里的「那点别扭」。

三、架构分析:RFC 3550 的解法 —— 把「数据」和「迭代」拆开

Rust 团队的解法干净利落,体现了这门语言最核心的设计直觉:如果一个类型承担了两种职责,就把它们拆开

3.1 两套世界:std::ops::Range(旧) vs core::range::Range(新)

RFC 3550 引入了一套全新的范围类型,位于 core::range 模块:

旧类型(已实现 Iterator)新类型(仅实现 IntoIterator)
std::ops::Range<T>core::range::Range<T>
std::ops::RangeFrom<T>core::range::RangeFrom<T>
std::ops::RangeInclusive<T>core::range::RangeInclusive<T>

关键区别只有一句话:core::range::* 类型不再实现 Iterator,而是实现 IntoIterator

  • Iterator:类型本身就是迭代器,调用 next() 会消费/改写自身 → 不能是 Copy。
  • IntoIterator:类型只是数据,当你显式调用 .into_iter() 时,才「生成」一个独立的迭代器 → 数据本身可以安然地 Copy

用一句更形象的话说:旧 Range 是「正在播放的 CD」,你没法复印它,因为你不知道复印的是歌单还是现在的播放进度;新 Range 是「一张纯歌单」,想听就 .into_iter() 生成一个播放器,歌单本身想复印几份都行。

3.2 为什么 IntoIterator 足够了?

你可能会担心:如果新 Range 不是 Iterator,那我还能写 for i in range { } 吗?

能。因为 Rust 的 for 循环底层就是 IntoIterator

for i in range { }   // 编译器自动展开为:let mut it = range.into_iter(); ...

IntoIteratorfor 循环的真实契约,而 Iterator 只是迭代器的能力。旧设计让类型既是数据又是迭代器,新设计让类型只做数据,把迭代交给 into_iter() 的产物。职责清晰,鱼与熊掌兼得。

3.3 迁移路径:绝不破坏现有代码

这是 Rust 风格的精髓。注意:

  • 你今天写的 0..10 语法糖,暂时仍然生成旧版的 std::ops::Range。这是为了 100% 向后兼容——你的几十万行存量代码一行都不用改。
  • 新类型需要你显式 opt-inuse core::range::Range;
  • 未来某个 Rust Edition(版本)里,语法糖才可能默认指向新类型;在那之前,两套世界长期共存。

这种「加新不改旧、靠 Edition 缓慢迁移」的策略,正是 Rust 能在九年里保持 API 稳定的根本原因。

3.4 反直觉的细节:为什么不直接「让旧 Range 也 Copy」?

有人会问:既然问题是「旧 Range 实现了 Iterator」,那把 Iterator 实现挪走、让它 Copy 不就行了?问题在于——一旦 std::ops::Range 不再是 Iterator,那么所有 for i in 0..n 的代码、所有依赖 Range: Iterator 的泛型约束都会编译失败。这是破坏性的。

更麻烦的是类型推断:很多地方靠「0..n 是 Iterator」来反推出 i 的类型。直接改旧类型会牵一发动全身。RFC 3550 的「另起炉灶、新旧并存」方案,本质上是在承认「修复稳定 API 的代价太大」之后,选择的代价最小、风险最低的路径。这也是为什么 Rust 的演进很少走「原地推翻」,而是走「平行世界 + 长期迁移」。

3.5 一个思维实验:如果 Range 从第一天起就是纯数据会怎样?

如果 2015 年的 Rust 就采用了今天的方案——Range 只做数据、迭代交给 IntoIterator——那么 for i in 0..n 这一行背后其实会发生一次隐式的 into_iter() 调用。对当时的语言设计者来说,这个「隐式调用」并不显然,而且早期 Rust 的 trait 推导还不如今天成熟,很容易写出让初学者困惑的错误信息。所以当年选择「让 Range 直接是 Iterator」,是用一个今天的代价,换来了当年的「零门槛入门」。这段历史提醒我们:API 设计从来不是追求纸面上的完美,而是在当时的编译器能力、用户认知、生态阶段之间做权衡。九年后,当 trait 系统、错误信息、Edition 机制都成熟了,RFC 3550 才成为「时机已到」的那一步。

四、代码实战:core::range 的六种打开方式

4.1 实战一:让 Span 真正 Copy 起来

这是最经典的受益场景——文本/字节区间标记:

use core::range::Range;

#[derive(Clone, Copy)]
pub struct Span(Range<usize>);

impl Span {
    /// 从一个字符串里切出本 Span 覆盖的子串
    pub fn of(self, s: &str) -> &str {
        &s[self.0]
    }

    pub fn start(&self) -> usize { self.0.start }
    pub fn end(&self) -> usize { self.0.end }
}

fn main() {
    let src = "hello, rust";
    let span = Span(Range { start: 7, end: 11 });  // "rust"
    println!("{}", span.of(src));  // 输出: rust

    // 因为是 Copy,span 可以随意传递、拷贝、存入数组
    let spans = [span, span, span];
    for sp in spans {
        println!("{}", sp.of(src));
    }
}

注意 &s[self.0] 这个索引操作:新的 core::range::Range<T> 实现了 SliceIndex,所以它能直接用于切片,和旧 Range 的用法完全一致。但区别在于——现在 Span 可以是 Copy,你可以把一堆 Span 塞进数组、当作函数返回值自由传递,而无需任何 clone()Rc 包装。

4.2 实战二:在 no_std / 嵌入式里用 core::range

core::range 属于 core 而非 std,意味着在 #![no_std] 环境、裸机嵌入式、WASM 核心里同样可用:

#![no_std]
use core::range::Range;

/// 在固定大小的显存 buffer 上标记一帧的绘制区间
#[derive(Clone, Copy)]
struct DrawRegion(Range<usize>);

impl DrawRegion {
    fn len(self) -> usize { self.0.end - self.0.start }
}

对嵌入式开发者来说,能在一个零成本、无堆分配、可 Copy 的区间类型上构建抽象,是这类场景里非常实在的红利。

4.3 实战三:泛型 + trait 的统一处理

因为新 Range 是普通结构体、字段公开,你可以轻松写出跨区间类型的泛型逻辑:

use core::range::{Range, RangeInclusive};
use core::ops::RangeBounds;

/// 任意可 Copy 的 usize 区间都能算长度
fn length_of(r: impl RangeBounds<usize> + Copy) -> usize {
    // RangeBounds 让 std::ops 与 core::range 的各类 Range 被统一对待
    match (r.start_bound(), r.end_bound()) {
        (core::ops::Bound::Included(s), core::ops::Bound::Excluded(e)) => e - s,
        (core::ops::Bound::Included(s), core::ops::Bound::Included(e)) => e - s + 1,
        _ => 0,
    }
}

fn main() {
    let a = core::range::Range { start: 0, end: 10 };
    let b = core::range::RangeInclusive { start: 0, end: 9 };
    println!("{} {}", length_of(a), length_of(b));  // 10 10
}

RangeBounds 是把 std::opscore::range 两大系列 Range 统一起来的公共 trait,因此你可以在同一个泛型函数里混用新旧类型。

4.4 实战四:RangeInclusive 终于能直接构造了

use core::range::RangeInclusive;

// 旧世界:字段私有,只能写 0..=10 语法糖
// 新世界:普通结构体,字段公开
let inc = RangeInclusive { start: 0, end: 10 };
println!("{}..={}", inc.start, inc.end);  // 0..=10

对写解析器、状态机、需要在泛型里动态构造闭区间的库作者来说,公开字段意味着不再依赖语法糖,代码可以纯粹地「构造——传递——解构」。

4.5 实战五:用 core::range 写一个迷你词法分析器

最能体现「区间即数据」价值的,是词法分析 / 源码高亮这类场景。它们需要大量「位置区间」在函数中自由流动:

use core::range::Range;

#[derive(Clone, Copy)]
struct Token {
    kind: TokenKind,
    span: Range<usize>,  // 直接存区间,无需手写 start/end
}

#[derive(Clone, Copy)]
enum TokenKind { Ident, Number, LParen, RParen }

/// 极简 tokenizer:把 "(abc 123)" 切成 token,并记录每个 token 的源码位置
fn tokenize(src: &str) -> Vec<Token> {
    let bytes = src.as_bytes();
    let mut tokens = Vec::new();
    let mut i = 0;
    while i < bytes.len() {
        let c = bytes[i] as char;
        match c {
            '(' => tokens.push(Token { kind: TokenKind::LParen, span: Range { start: i, end: i + 1 } }),
            ')' => tokens.push(Token { kind: TokenKind::RParen, span: Range { start: i, end: i + 1 } }),
            '0'..='9' => {
                let start = i;
                while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
                tokens.push(Token { kind: TokenKind::Number, span: Range { start, end: i } });
                continue;
            }
            'a'..='z' | 'A'..='Z' => {
                let start = i;
                while i < bytes.len() && bytes[i].is_ascii_alphanumeric() { i += 1; }
                tokens.push(Token { kind: TokenKind::Ident, span: Range { start, end: i } });
                continue;
            }
            _ => {} // 跳过空白
        }
        i += 1;
    }
    tokens
}

fn main() {
    let src = "(abc 123)";
    for tok in tokenize(src) {
        let s = &src[tok.span];   // 直接用区间切片回原文
        println!("token = {:?}, text = {}", tok.kind, s);
    }
}

注意这里 Token 直接把 Range<usize> 当字段——因为 RangeCopyToken 也天然 Copy,整个 tokenizer 全程零堆分配、零 clone(),且每个 token 都能「凭区间切片回原文」。换成旧 std::ops::RangeToken 就必须是 Clone(带运行时开销)或退化成手写 start/end

4.6 实战六:迭代组合子一个都不少

有人担心「不是 Iterator 是不是就不能用 .rev().step_by() 了」?完全不用担心——IntoIterator 的产物依然是个完整的迭代器,所有你熟悉的组合子照常工作:

use core::range::Range;

fn main() {
    let r = Range { start: 0, end: 10 };

    // 倒序、步长、带索引,全部可用
    for i in r.rev().step_by(2) {
        print!("{i} ");   // 8 6 4 2 0
    }
    println!();

    // r 依然是 Copy,上面迭代后毫发无损
    for (idx, v) in r.into_iter().enumerate() {
        print!("({idx}:{v}) ");  // (0:0) (1:1) ... (9:9)
    }
}

迭代能力的完整度,新旧 Range 完全一致;区别仅仅在于「数据本体」现在可以独立存活、自由拷贝。

五、断言宏:assert_matches! 与 debug_assert_matches!

1.96 还稳定了两个被期待已久的宏,专门解决「模式匹配校验」的样板问题。

5.1 旧世界的啰嗦写法

let val: Result<i32, &str> = Ok(42);

// 你不得不手写这段啰嗦的样板
if let Ok(n) = &val {
    if *n <= 0 {
        panic!("expected positive, got {}", n);
    }
} else {
    panic!("expected Ok, got {:?}", val);
}

5.2 新世界的清爽写法

use core::assert_matches::assert_matches;

let val: Result<i32, &str> = Ok(42);

// 一行搞定,且失败时自动以 Debug 格式打印出实际值
assert_matches!(val, Ok(n) if n > 0);

assert_matches! 在断言失败时,会像 assert_eq! 一样打印出实际值的结构化 Debug 输出,而不是一句干巴巴的 called 'Result::unwrap()' on an 'Err' value。这在调试复杂的枚举、嵌套 Result、或者测试解析器输出时,体验提升是质变的。

配套的 debug_assert_matches! 则只在开发模式(--cfg debug_assertions)下编译进代码,在 release 构建里被完全剔除——零运行时成本,却能在开发期帮你挡住一大类「形状不对」的 bug。

use core::assert_matches::debug_assert_matches;

fn process(input: Option<u32>) -> u32 {
    debug_assert_matches!(input, Some(_));  // release 下这行直接消失
    input.unwrap()
}

5.3 横向对比:别的语言怎么处理「区间」?

为了看清 Rust 这次改动的分量,不妨看看其它语言:

  • Pythonrange(0, 10) 返回的是一个「惰性序列对象」,本身既不是迭代器也不是简单数据——它可重复迭代,但你没法把它当「一个区间值」切片、拷贝、塞进结构体。它的设计目标是「for 循环好用」,而非「作为数据传递」。
  • Go:根本没有内建区间类型,for i := 0; i < n; i++ 是裸循环;要做「区间值」得自己定义 type Span struct{Start, End int},谈不上语言级支持。
  • C++std::views::iota(0, 10) 是视图(view),同样强调的是「可迭代」而非「可拷贝的数据」。

Rust 的独特之处在于:它硬要在「可迭代」和「可拷贝的数据」之间找到平衡点,并且用类型系统把这个平衡编码进编译器。新 core::range 既是零成本的数据,又保留完整的迭代能力——这是只有 Rust 的类型哲学才能逼出来的设计。

六、Cargo 双源依赖:git 与 registry 终于能共存

这可能是 1.96 里对企业/库作者最实用的特性。

6.1 旧世界的死结

此前,一个依赖项要么来自 git,要么来自 registry(如 crates.io),二者不可兼得。这导致一个常见场景无解:

你本地开发时,想用一个打了补丁的 fork(通过 git = "...");但当你把自己的库发布到 crates.io 时,下游用户不应该去拉你的私有 git,而应该用 crates.io 上的正式版。

旧 Cargo 做不到「本地用 git、发布用 registry」这种分流。

6.2 新世界的写法

1.96 允许一个依赖同时声明 git 和 registry

[dependencies]
serde = { version = "1.0", git = "https://github.com/serde-rs/serde", registry = "crates-io" }

语义是:

  • 本地构建时:使用 git 指定的仓库(你本地的 fork / 补丁版)。
  • 当你的 crate 被发布、下游用户拉取时:Cargo 会自动把依赖改写为 registry 版本,用户走 crates.io 正常解析。

这意味着库作者可以「用 fork 做本地开发和 CI 验证」,同时「对全世界暴露干净的标准依赖」。对维护大型 monorepo、需要对上游做临时 patch 又不想 fork 整个生态的团队,这是个实打实的效率提升。

6.3 它和 [patch] 有什么区别?

熟悉 Cargo 的人会想到 [patch] 区段——它也能把某个依赖重定向到 git。区别在于:

  • [patch]全局、显式的重定向,写在根 Cargo.toml 里,对所有依赖生效;它更像是「开发期把所有下游都换成我的 fork」。
  • 双源依赖是局部、声明在单个依赖上的,且携带了「发布时回退到 registry」的语义。它表达的是「这一个依赖,本地用 git、发布用 registry」。

换句话说,[patch] 适合「整个工作区临时换源」;双源依赖适合「单个 crate 既想本地用 fork、又想对外部发布成标准依赖」这种精细场景。两者互补,不冲突。

七、Wasm 链接收紧:--allow-undefined 的退场

wasm32 系列目标(wasm32-unknown-unknown 等)在 1.96 里做了一项看似微小、实则重要的链接行为变更

链接器不再默认传递 --allow-undefined

7.1 这意味着什么?

在旧的 Wasm 工具链里,未定义的符号会被链接器静默放过。因为 wasm 的很多符号确实是在运行时通过 JS 导入(env 命名空间)解析的,所以 --allow-undefined 曾是默认行为。但副作用是:如果你的 Rust 代码里少写了一个 #[export_name],或者 import 声明和实际 JS 端对不上,链接器不会报错,问题会一直潜伏到运行时才以「undefined symbol」的形式炸出来,而且往往很难定位。

1.96 收紧后,链接阶段就会直接报出未定义符号,把 bug 提前到编译/链接期——这正是 Rust「fail fast、把错误尽量推到编译期」哲学在 Wasm 上的延伸。

7.2 你需要做什么?

绝大多数纯 Rust 的 Wasm 项目无需改动,因为正常的 #[wasm_bindgen] / #[export_name] 符号都是明确定义的。如果你之前依赖「链接器兜底放行未定义符号」来绕过某些声明,现在需要补全对应的导入/导出声明。建议升级后跑一遍 wasm-pack buildcargo build --target wasm32-unknown-unknown,确认没有新的链接错误。

八、性能与工程影响:零成本抽象的真实含义

Rust 有一句口头禅:「零成本抽象」(zero-cost abstractions)——你用上的高级抽象,不该比手写底层代码慢或胖。

core::range 就是这句话的范本:

  1. 纯数据结构,零堆分配Range<T> 就是 { start: T, end: T } 两个字段,没有任何 vtable、没有任何间接层。它和手写 struct { start, end } 在内存布局上完全一致。
  2. 完全内联:因为只是普通字段 + IntoIterator 的简单实现,into_iter() 生成的迭代器在编译期被完全内联,和旧的 for i in 0..n 生成的汇编几乎逐字节相同。你用上了更灵活的类型,却没有付出任何运行时代价。
  3. 更好的缓存局部性:当一个 Span 能直接 Copy 嵌入到你的结构体里,你就少了一层指针间接、少了一个可能的堆分配,数据更紧凑,缓存命中率更高。在解析器、编译器、序列化库这类「海量小对象」场景里,这种差异会累积成可观的吞吐提升。
  4. 更清晰的 API 边界:把「数据」和「迭代」拆开后,编译器能更精准地做借用检查——迭代是迭代、数据是数据,二者不再互相污染借用状态。这对静态分析的友好度,是隐形的长期红利。

8.5 一个可量化的直觉:Span 的内存布局

size_of 直接验证「零成本」不是空话:

use core::range::Range;
use std::ops::Range as OldRange;

#[derive(Clone, Copy)]
struct NewSpan(Range<usize>);

#[derive(Clone, Copy)]
struct OldSpan(OldRange<usize>);  // ❌ 实际上 OldRange 不是 Copy,这里仅为对比布局

fn main() {
    // Range<usize> 就是两个 usize,布局等同于手写 struct
    assert_eq!(core::mem::size_of::<Range<usize>>(), 16);  // 64 位下 2×8 字节
}

core::range::Range<usize> 的占用就是两个 usize,没有任何隐藏字段或指针。所谓「更灵活的抽象没有额外成本」,在这里是可以用 size_of 量出来的硬事实。

8.6 真实收益场景:解析器与编译器的世界里,Span 是骨架

如果你写过编译器、格式化工具、或者任何「需要把输出映射回源码位置」的程序,你会立刻明白 core::range 的价值。这类程序的核心数据结构几乎都是「带位置的信息」:一个 AST 节点要记住它从哪来到哪去,一个诊断信息要能高亮出报错的原文区间。在旧世界里,这些位置往往被迫退化成两个 usize 字段,或者引入 Span 类型但承担 Clone 的运行时成本。当 Span 可以零成本地 Copy 时,你的递归下降解析器在每次返回节点时都能顺手带上位置,而无需在调用栈里到处传递、克隆。对 LSP、静态分析、代码生成这类「位置即一切」的工具来说,这简直是命中注定的契合。

九、升级指南与总结展望

9.1 升级只需一行

rustup update stable

1.96 是增量更新,没有破坏性改动,cargo build 大概率直接通过。建议顺手跑一遍测试套件和 Wasm 构建。

9.2 什么时候该用新 Range?

  • 如果你在写需要 Copy/Clone 的区间相关类型(解析器、Span、文本处理库)→ 立刻 use core::range::Range;,收益最大。
  • 如果你只是写 for i in 0..n 循环 → 暂时不用改,旧语法糖依旧工作,等未来 Edition 自然迁移。
  • 如果在 no_std / 嵌入式里需要轻量区间类型 → core::range 是比 std::ops 更合适的选择。

9.3 这版更新到底说明了什么?

Rust 1.96 没有 headline feature,但它恰好展示了 Rust 这门语言最被低估的一种能力:在保持绝对兼容的前提下,悄悄把地基垫高一块

Range 的问题存在了接近十年。换作很多语言,要么「将错就错」永远不改,要么「下个大版本破坏性重写」。Rust 选了第三条路:在 core 里放一个更好的原语,默认不破坏任何人,靠 Edition 做跨越数年的平滑迁移。

这种克制,才是 Rust 能在系统编程、嵌入式、Wasm、服务端如此多领域被信任的根本原因。当你下次写 0..10 时,不妨想一想:你踩在的,是一块被精心打磨了九年、如今终于能 Copy 的地基上。

9.4 给团队的技术选型建议

如果你在评估是否要在生产代码里采用 core::range,我的建议分两层:对于应用层业务代码,除非你已经明显感到「存区间要 clone」的痛,否则不必急着迁移,for i in 0..n 依旧是最好读的写法;对于库 / 基础设施代码(解析器、序列化、编译器、文本处理),只要你的类型需要把区间当作值来传递,core::range 就是当下的首选,它同时给了你 Copy 能力与更干净的字段。升级 Rust 1.96 本身几乎没有风险——它没有破坏性改动,真正的收益是「当你下次需要区间即数据时,手边刚好有一个对的工具」。


本文基于 Rust 1.96.0 稳定版(2026-05-28 发布)官方 Release Notes 与 RFC 3550 整理。代码示例以「可读、可理解」为优先,实际迭代行为请以你本地 rustc --version 的文档为准。

推荐文章

程序员茄子在线接单