Rust 1.99 更新了什么?一个新手视角的升级前后对比

2,848 字#Rust #学习笔记 #版本更新

照着教程写 Rust,最容易卡住的地方不是语法,是这一行:

error[E0658]: use of unstable library feature `fs_set_times`

代码抄得没错,教程也没错,只是那个 API 在当时的稳定版里还没开放。新手很难判断这是自己写错了,还是版本不对,只能一遍遍对着教程检查,最后才发现问题不在自己身上。

Rust 1.99.0 在 2026 年 10 月 1 日发布。下面挑几个真会碰到的变化,把升级前后的代码放在一起跑,顺便说清楚哪些是实打实的省事,哪些只是换了个写法。

版本和对比方式

本文以 Rust 1.97.1 作为升级前版本,以 Rust 1.99.0 作为升级后版本。示例只保留和版本变化直接相关的代码。

先看总表

想做的事Rust 1.97.1Rust 1.99.0
遍历 Box<[T; N]>编译报错直接 for
把非法 UTF-8 字节转成 String能转,但要多一次拷贝直接消费 Vec<u8>
保留 VecDeque 尾部 N 个元素只能循环 pop_frontretain_back(n)
修改文件时间只能调系统命令或第三方 cratefs::set_times
配置 debug profile名字被保留,拒绝[profile.debug] 可用
在 Rust 里定义 C 可变参数函数只能调用,不能定义可以用 ... 定义
拆开 / 拼回 Vec需要自己管理裸指针into_parts / from_parts

前两条和普通集合、字符串处理有关,比较容易遇到;中间三条属于特定工具或配置场景;最后两条主要面向 FFI 和底层内存操作。

一、Box<[T; N]> 终于能直接遍历

从函数返回一个固定长度的数组,经常会被包成 Box<[T; N]>。拿到之后想遍历:

let b: Box<[i32; 3]> = Box::new([1, 2, 3]);
for x in b {
    println!("{x}");
}

1.97.1 上直接拒绝:

error[E0277]: `[i32; 3]` is not an iterator
   --> t1.rs:3:14
    |
  3 |     for x in b {
    |              ^ `[i32; 3]` is not an iterator
    |
    = help: the trait `Iterator` is not implemented for `[i32; 3]`

编译器顺手告诉你,Box<[I]> 实现了 IntoIterator,但 Box<[I; N]> 没有。翻译一下:不定长数组可以,定长数组不行。这种"就差一点"的规则最难猜,因为你不知道该往哪个方向试。

升级后同样一段代码输出:

1
2
3

借用和可变借用也一起补上了:

let b: Box<[i32; 3]> = Box::new([1, 2, 3]);
for x in &b {
    println!("ref {x}");
}

let mut b2: Box<[i32; 3]> = Box::new([4, 5, 6]);
for x in &mut b2 {
    *x += 1;
}
println!("{b2:?}"); // [5, 6, 7]

以前 &Box<[i32; 3]> 和 &mut Box<[i32; 3]> 报的是同一个错。现在三种形式都能用,for 的写法和别的集合一致了。

二、非法 UTF-8:省掉的是一次拷贝

从文件、网络或数据库读来的字节,不一定是合法 UTF-8。想转成字符串并把非法字节换成 �,1.99 之前其实也能做:

let bytes: Vec<u8> = vec![0xE4, 0xBD, 0xA0, 0xE5, 0xA5, 0xBD, 0xFF];
let s: String = String::from_utf8_lossy(&bytes).into_owned();
println!("{s:?}"); // "你好�"

这段在 1.97.1 上跑得通,结果和新 API 一模一样。区别在于 from_utf8_lossy 返回的是 Cow<str>,你要拿到真正拥有数据的 String,就得再 .into_owned(),多一次分配和拷贝。

1.99 之后可以直接把 Vec<u8> 交出去:

let bytes: Vec<u8> = vec![0xE4, 0xBD, 0xA0, 0xE5, 0xA5, 0xBD, 0xFF];
let s = String::from_utf8_lossy_owned(bytes);
println!("{s:?}"); // "你好�"

1.97.1 上会报:

error[E0658]: use of unstable library feature `string_from_utf8_lossy_owned`
  |
3 |     let s = String::from_utf8_lossy_owned(bytes);
  |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: see issue #129436 for more information

配套还稳定了 FromUtf8Error::into_utf8_lossy,用在"转换确实失败了,但我想要一个尽量能用的字符串"这种场合:

let bad: Result<String, std::string::FromUtf8Error> =
    String::from_utf8(vec![0xFF, 0xFE]);
match bad {
    Ok(s) => println!("ok {s}"),
    Err(e) => println!("lossy: {:?}", e.into_utf8_lossy()),
}

所以这条别理解成"以前做不到"。以前做得到,只是要多写一步、多一次拷贝。数据量大的时候这个差别才有意义。

三、retain_back:名字容易看错

第一次看到 retain_back,很容易以为它是 retain 的兄弟,接受一个闭包。实际上它接受一个 usize,语义是"保留最后 N 个元素":

use std::collections::VecDeque;

let mut dq: VecDeque<i32> = VecDeque::from([1, 2, 3, 4, 5]);
dq.retain_back(2);
println!("{dq:?}"); // [4, 5]

我按闭包传参试了一次,编译器提示 expected usize, found closure,才回头确认签名是 retain_back(&mut self, len: usize)。它带一个 doc(alias = "truncate_front"),也可以理解成"从前面截断"。

1.97.1 上的报错很直白:

error[E0599]: no method named `retain_back` found for struct `VecDeque<T, A>`

以前要实现同样效果,得循环 pop_front() 或者自己算索引。日志缓冲区只留最近 N 条这种需求,现在一行够了。

四、改文件时间:不用再调系统命令

想改文件的修改时间,以前 Rust 里没有稳定 API,常见做法是 std::process::Command 去执行系统命令,或者引第三方 crate。

1.99 稳定了 std::fs::set_times:

use std::fs;
use std::time::{Duration, SystemTime};

fn main() -> std::io::Result<()> {
    std::fs::write("target.txt", "demo")?;

    let t = SystemTime::now() - Duration::from_secs(3600);

    fs::set_times(
        "target.txt",
        fs::FileTimes::new().set_modified(t),
    )?;

    Ok(())
}

1.97.1 上:

error[E0658]: use of unstable library feature `fs_set_times`

顺带稳定了 set_times_nofollow,作用于符号链接本身而不是它指向的文件。写构建工具、同步脚本或者做测试时挺常见,现在不用为了这件事去调外部命令。

五、Cargo:两个和日常开发有关的改动

第一个是 profile 名字。以前在 Cargo.toml 里写:

[profile.debug]
inherits = "dev"
debug = false

Cargo 直接拒绝:

error: profile name `debug` is reserved
       To configure the default development profile, use the name `dev` as in [profile.dev]

debug 这个名字以前是保留的。1.99 新增了内置的 debug profile,目前它和 dev 没有区别,官方说法是"为将来把 dev 从调试定位里挪开做准备"。也就是说,现在写它不会报错,但暂时也看不出差别。

第二个更实用:在 CI 环境里(通过 CI 环境变量判断),Cargo 默认关闭增量编译。增量编译在 CI 上收益不大,还会让缓存目录膨胀,这个默认值改得合理。

六、写 FFI 或底层内存操作才会用到的两条

这两条不是普通业务代码的日常工具。先知道它们解决什么问题即可,除非你正在写 C 绑定、分配器或底层库,否则不用为了追新 API 特意使用它们。

定义 C 可变参数函数。 Rust 一直能调用 printf 这类函数,1.99 之后也能定义:

unsafe extern "C" fn sum(mut args: ...) -> i32 {
    let a = unsafe { args.next_arg::<i32>() };
    let b = unsafe { args.next_arg::<i32>() };
    a + b
}

fn main() {
    let s = unsafe { sum(3i32, 4i32) };
    println!("sum={s}"); // 7
}

1.97.1 上:

error[E0658]: C-variadic functions are unstable
error[E0658]: use of unstable library feature `c_variadic`: the `c_variadic` feature has not been properly tested on all supported platforms

... 的类型是 core::ffi::VaList。这段代码只适合说明稳定化后的能力;它依然是 unsafe,调用方必须按约定传够参数,读错类型编译器拦不住。普通 CLI、Web 服务和业务代码可以直接跳过这一节。

拆开和拼回 Vec。 以前没有稳定版本,只能自己 mem::forget 掉 Vec 再取裸指针,或者用 nightly 上的 into_raw_parts。现在:

let v: Vec<i32> = vec![1, 2, 3, 4];
let (ptr, len, cap) = v.into_parts();
let v2 = unsafe { Vec::from_parts(ptr, len, cap) };
println!("{v2:?}"); // [1, 2, 3, 4]

Box 也补了非空指针版本:

let b: Box<i32> = Box::new(7);
let nn = Box::into_non_null(b);
let b2 = unsafe { Box::from_non_null(nn) };
println!("{b2}"); // 7

1.97.1 上这两组都报 use of unstable library feature 'box_vec_non_null'。

两个函数都是 unsafe,因为指针、长度、容量是否配套要你自己保证。新版本给的是稳定入口,不是安全网。只是想把所有权传出去的话,直接用 Vec 本身就行,没必要绕到这里。

七、收紧的部分:几个旧写法开始报警

新功能之外,1.99 也让一些旧写法浮出水面。最容易碰到的是这个:

println!("{}", std::i32::MAX);

1.97.1 上能编译,1.99 上给弃用警告:

warning: use of deprecated constant `std::i32::MAX`: replaced by the `MAX` associated constant on this type
  |
2 |     println!("{}", std::i32::MAX);
  |                              ^^^

正确写法是 i32::MAX。这类旧模块路径在 1.99 里被完全弃用,早晚要改,现在顺手改掉比以后被迫改轻松。

其他几条新手可能遇到的:

  • no_mangle_generic_items 从警告升级为硬错误。给泛型函数加 #[no_mangle] 会直接编译失败。
  • Pin::new_unchecked 的安全不变量有细微调整,涉及 Drop 的实现。
  • 文档注释代码块里,"没有应用到任何东西的属性"现在会报错。
  • 宏展开出多余分号时,来自其他 crate 的宏也会给警告,以前只警告本 crate 的。

还有一条新增的 lint,默认只提示不拦截:

fn main() {
    let v = vec![1, 2, 3];

    let q: *const i32 = (&v[1]) as *const i32;
    println!("{}", unsafe { *q });
}

开启 -W raw_borrows_via_references 后:

warning: creating an intermediate reference implies aliasing requirements even when immediately cast to a raw pointers
help: consider using `&raw const` for a safer and more explicit raw pointer

(&x) as *const T 会先创建一个引用,再转成裸指针,引用本身的别名规则依然生效。推荐写 &raw const x。这个坑以前不容易意识到,现在编译器会主动指出来。

这些变化说明什么

这几个例子放在一起,变化其实很朴素:以前需要手动绕路的操作,现在标准库给了一个稳定的方法名。

但不同 API 的收益并不一样:

  • Box<[T; N]> 遍历、retain_back、fs::set_times,解决的是以前缺少稳定入口的问题;
  • from_utf8_lossy_owned 和 into_parts,解决的是以前能绕路完成,但代码和内存处理更麻烦的问题。

第二类变化不一定会让每个项目变快。它的价值主要是减少重复代码、避免依赖 nightly,并让代码更容易按照标准库文档维护。into_parts 和 extern "C" 可变参数仍然是 unsafe,稳定化不等于使用风险消失。

对新手来说,升级后最实用的判断方法是:如果项目没有用到这些 API,升级不会带来明显的业务功能变化;如果你因为版本过旧遇到 E0658,或者正在写文件工具、FFI 和底层库,1.99 才会真正减少绕路。

怎么升级

如果要严格使用 1.99.0,可以安装并指定这个工具链:

rustup toolchain install 1.99.0
cargo +1.99.0 check
cargo +1.99.0 clippy

如果项目里锁了旧工具链,可以用 rust-toolchain.toml 固定版本。确认测试和弃用警告都处理好后,再决定是否切换项目默认工具链。

参考资料