Rust 1.99 更新了什么?一个新手视角的升级前后对比
照着教程写 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.1 | Rust 1.99.0 |
|---|---|---|
遍历 Box<[T; N]> | 编译报错 | 直接 for |
把非法 UTF-8 字节转成 String | 能转,但要多一次拷贝 | 直接消费 Vec<u8> |
保留 VecDeque 尾部 N 个元素 | 只能循环 pop_front | retain_back(n) |
| 修改文件时间 | 只能调系统命令或第三方 crate | fs::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 = falseCargo 直接拒绝:
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}"); // 71.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 固定版本。确认测试和弃用警告都处理好后,再决定是否切换项目默认工具链。