Rust 1.92.0 于 2025 年 12 月 11 日发布。这个版本没有惊天动地的新特性,但带来了一批等了很久的实用 API。作为一个写了几年 Rust 的开发者,看到这些 API 终于稳定,感觉就像是"终于不用自己造轮子了"。
本文将深入分析这些新 API 的实战价值、生态影响和设计哲学。
总览:这个版本稳定了什么?
并发相关
RwLockWriteGuard::downgrade
智能指针零初始化
Box::new_zeroed/new_zeroed_sliceRc::new_zeroed/new_zeroed_sliceArc::new_zeroed/new_zeroed_slice
数值计算
NonZero<u{N}>::div_ceil
集合操作
btree_map::Entry::insert_entrybtree_map::VacantEntry::insert_entry
FFI / 调试
Location::file_as_c_str
过程宏
TokenStream: Extend<Group/Literal/Punct/Ident>
常量化
<[_]>::rotate_left/rotate_right(const)
让我们逐个深入。
一、RwLockWriteGuard::downgrade:等了九年的 API
这是什么?
impl<T: ?Sized> RwLockWriteGuard<'_, T> {
pub fn downgrade(self) -> RwLockReadGuard<'_, T>;
}
简单说:把写锁降级为读锁,而不释放锁。
使用场景
downgrade 解决的核心问题:修改数据后,需要继续持有锁读取,同时希望其他线程也能并发读。
use std::sync::{RwLock, RwLockWriteGuard};
use std::thread;
use std::time::Duration;
fn update_and_compute(data: &RwLock<Vec<i32>>) -> i64 {
// 1. 获取写锁,修改数据
let mut write = data.write().unwrap();
write.push(42);
// 2. 降级为读锁:原子操作,没有时间窗口
let read = RwLockWriteGuard::downgrade(write);
// 3. 耗时的只读计算(此时其他线程可以并发读!)
thread::sleep(Duration::from_millis(100));
read.iter().map(|&x| x as i64).sum()
}
fn main() {
let data = RwLock::new(vec![1, 2, 3]);
let result = update_and_compute(&data);
println!("结果: {}", result); // 输出: 结果: 48
}
为什么不能先释放写锁再获取读锁? 因为存在时间窗口:
use std::sync::RwLock;
fn update_and_compute_bad(data: &RwLock<Vec<i32>>) -> i64 {
{
let mut write = data.write().unwrap();
write.push(42);
} // 写锁释放
// 时间窗口:其他线程可能在这里修改数据!
let read = data.read().unwrap();
read.iter().map(|&x| x as i64).sum() // 读到的可能不是我们刚修改的数据
}
fn main() {
let data = RwLock::new(vec![1, 2, 3]);
let result = update_and_compute_bad(&data);
println!("结果: {}", result); // 单线程下输出 48,但多线程下可能不一致
}
downgrade 是原子操作,锁从未被释放,保证读到的一定是自己刚修改的数据。
生态影响
这个 API 对以下场景特别有价值:
- 配置热更新:更新配置后,基于新配置执行长时间的只读操作,同时让其他线程可以读取新配置
- COW(Copy-on-Write)数据结构:修改后需要长时间读取,降级让其他读者不必等待
- 缓存预热:写入缓存后,立即基于缓存数据进行计算,同时允许其他线程读取
一些第三方库(如 parking_lot)早就提供了这个功能,现在标准库终于跟上了。
设计哲学
为什么这个 API 等了这么久?因为 Rust 标准库对 API 的稳定非常谨慎。downgrade 涉及:
- 语义清晰性:降级后原来的写锁失效,这通过
self所有权转移来保证 - 平台兼容性:不同操作系统的 RwLock 实现不同,需要确保所有平台都能正确实现
- 与
upgrade的对称性:Rust 故意没有提供upgrade(读锁升级为写锁),因为这会导致死锁
// 为什么没有 upgrade?假设两个线程都持有读锁并尝试升级:
// 线程 A: 持有读锁,等待所有读锁释放以升级为写锁
// 线程 B: 持有读锁,等待所有读锁释放以升级为写锁
// 结果:死锁!双方都在等对方释放读锁
downgrade 没有这个问题,因为写锁本身就是独占的。
二、零初始化智能指针:性能优化的新选择
新增的 API
// Box
impl<T> Box<T> {
pub fn new_zeroed() -> Box<MaybeUninit<T>>;
}
impl<T> Box<[T]> {
pub fn new_zeroed_slice(len: usize) -> Box<[MaybeUninit<T>]>;
}
// Rc 和 Arc 同理
impl<T> Rc<T> {
pub fn new_zeroed() -> Rc<MaybeUninit<T>>;
}
impl<T> Arc<T> {
pub fn new_zeroed() -> Arc<MaybeUninit<T>>;
}
这解决什么问题?
传统的 Box::new(value) 流程是:
- 在栈上创建
value - 在堆上分配内存
- 把
value从栈复制到堆
对于大型数据结构,第 3 步的复制开销可能很大:
// 问题:语义上,这个数组先在栈上创建,再复制到堆上
let big_array = Box::new([0u8; 1024 * 1024]); // 1MB
// 更糟:如果栈空间不够,直接栈溢出
// Debug 模式 + 默认栈大小(通常 8MB)下,10MB 数组必定溢出
let huge_array = Box::new([0u8; 10 * 1024 * 1024]);
等等,Release 模式好像不会栈溢出?
上面的代码在 --release 模式下并不会栈溢出。这是因为 LLVM 优化器足够聪明,能够识别这种模式并直接在堆上分配:
Debug 模式(opt-level = 0):
语义上的流程:栈上创建 → 堆上分配 → 复制
实际结果:大数组超出默认栈大小,直接栈溢出
Release 模式(opt-level = 3,rustc 1.92.0 测试):
零初始化 [0u8; N]:直接调用 __rust_alloc_zeroed
常量填充 [val; N]:__rust_alloc + memset
→ 完全跳过栈分配,不会栈溢出
我们可以通过汇编验证这一点。以下是 Release 模式下 Box::new([0u8; 1024 * 1024]) 在 ARM64 macOS 上的汇编:
; 直接调用 alloc_zeroed,没有栈分配!
mov w0, #1048576 ; 大小 = 1MB
mov w1, #1 ; 对齐 = 1
bl __rust_alloc_zeroed ; 直接堆分配 + 零初始化
但这不意味着可以依赖这个优化! 原因如下:
- 不是语言保证:这是 LLVM 的优化行为,不同编译器版本、不同优化级别、不同代码结构可能有不同结果
- Debug 构建仍会失败:开发阶段的测试可能因此遗漏问题
- 复杂场景可能失效:如果数组初始化逻辑更复杂,优化器可能无法识别
以下场景即使在 Release 模式下也会栈溢出(rustc 1.92.0 验证):
const SIZE: usize = 10 * 1024 * 1024; // 10MB
// ❌ 场景 1:std::array::from_fn 初始化
// from_fn 需要逐元素调用闭包,无法优化为单次堆分配
let arr1: Box<[u8; SIZE]> = Box::new(std::array::from_fn(|i| (i % 256) as u8));
// ❌ 场景 2:array.map() 转换
// map 返回新数组,整个转换过程在栈上完成后才能装箱
let source = [0u8; SIZE];
let arr2: Box<[u8; SIZE]> = Box::new(source.map(|x| x.wrapping_add(1)));
// ❌ 场景 3:通过 #[inline(never)] 函数包装
// 禁止内联后,数组必须在调用方构造完成后传递给函数
#[inline(never)]
fn wrap_box<T>(value: T) -> Box<T> {
Box::new(value)
}
fn main() {
let arr = wrap_box([0u8; 10 * 1024 * 1024]); // 栈溢出
}
这些场景的共同点:编译器无法将"构造数组"和"堆分配"合并为一步操作,必须先在栈上完成数组构造。
Box::new_zeroed() 是 API 保证——无论什么优化级别,都会直接在堆上分配。这才是生产代码应该使用的方式。
零初始化的优势
fn main() {
// 固定大小数组:直接在堆上分配并零初始化
let big_array: Box<[u8; 1024]> = unsafe {
Box::new_zeroed().assume_init()
};
println!("数组长度: {}", big_array.len());
// 动态大小切片
let dynamic_array: Box<[u8]> = unsafe {
Box::new_zeroed_slice(1024).assume_init()
};
println!("切片长度: {}", dynamic_array.len());
}
为什么返回 MaybeUninit? 对于 new_zeroed() 的典型用例(数值数组),全零总是有效的。但 Rust 类型系统无法证明"全零对于任意 T 是有效的",所以 API 采用保守设计:返回 MaybeUninit<T>,让程序员通过 unsafe { assume_init() } 显式承诺类型有效性。
实战用法
use std::mem::MaybeUninit;
use std::sync::Arc;
fn create_shared_buffer(size: usize) -> Arc<[u8]> {
let uninit: Arc<[MaybeUninit<u8>]> = Arc::new_zeroed_slice(size);
// MaybeUninit<u8> 和 u8 有相同的内存布局
unsafe { Arc::from_raw(Arc::into_raw(uninit) as *const [u8]) }
}
fn main() {
let buffer = create_shared_buffer(1024);
println!("缓冲区大小: {} 字节", buffer.len());
println!("全为零: {}", buffer.iter().all(|&x| x == 0));
}
性能对比
场景:分配 10MB 缓冲区(Debug 模式或优化失效时)
Box::new([0u8; 10 * 1024 * 1024])
1. 栈上分配 10MB(可能栈溢出)
2. 堆上分配 10MB
3. 复制 10MB
总计:20MB 内存操作 + 复制开销
Box::new_zeroed() + assume_init()
1. 堆上分配 10MB
2. 操作系统零初始化(利用零页映射,接近零成本)
总计:10MB 内存操作,无复制,无栈溢出风险
设计哲学
这组 API 体现了 Rust 的一个核心理念:零成本抽象。
MaybeUninit在编译后完全消失,没有运行时开销- 通过类型系统强制程序员处理"未初始化"状态
- 提供
unsafe逃生舱口,但把责任明确交给程序员
这也是为什么 API 返回 MaybeUninit——虽然对于典型用例全零总是有效的,但类型系统无法表达这个约束,所以用 unsafe 让程序员显式承诺。
三、NonZero::div_ceil:小而美的补全
新增的 API
impl NonZero<u8> { // 以及 u16, u32, u64, u128, usize
pub const fn div_ceil(self, rhs: Self) -> Self;
}
除法向上取整,且保证结果也是 NonZero。
为什么需要这个?
div_ceil(向上取整除法)是一个常见操作:
// 场景:计算需要多少个页面来存储 n 字节
fn pages_needed(bytes: usize, page_size: usize) -> usize {
(bytes + page_size - 1) / page_size // 传统写法,容易溢出
// 或
bytes.div_ceil(page_size) // 标准库方法,1.73.0 稳定
}
但对于 NonZero 类型,之前没有这个方法:
use std::num::NonZero;
fn main() {
let items = NonZero::new(100usize).unwrap();
let batch_size = NonZero::new(30usize).unwrap();
// 之前:需要转换
let batches_old = NonZero::new(items.get().div_ceil(batch_size.get())).unwrap();
// 现在:直接调用
let batches = items.div_ceil(batch_size);
println!("100 / 30 向上取整 = {}", batches.get()); // 输出: 4
}
为什么返回值也是 NonZero?
数学保证:如果被除数和除数都是正整数,向上取整的结果至少是 1。
1 / 1 = 1 (向上取整)
1 / 100 = 1 (向上取整)
99 / 100 = 1 (向上取整)
100 / 100 = 1
所以返回 NonZero 是类型安全的——不可能返回零。
生态影响
NonZero 类型在以下场景广泛使用:
- 内存布局优化:
Option<NonZero<u32>>和u32大小相同 - 类型安全:避免除零错误
- FFI:很多 C API 用非零值表示有效句柄
这个 API 让 NonZero 在数值计算场景更加实用。
四、BTreeMap::Entry::insert_entry:补齐最后一块拼图
新增的 API
impl<'a, K: Ord, V> Entry<'a, K, V> {
pub fn insert_entry(self, value: V) -> OccupiedEntry<'a, K, V>;
}
impl<'a, K: Ord, V> VacantEntry<'a, K, V> {
pub fn insert_entry(self, value: V) -> OccupiedEntry<'a, K, V>;
}
与现有 API 的区别
use std::collections::BTreeMap;
fn main() {
let mut map: BTreeMap<&str, i32> = BTreeMap::new();
// 现有的 or_insert 方法:返回 &mut V
let value_ref: &mut i32 = map.entry("key").or_insert(42);
println!("value_ref = {}", value_ref);
// 新的 insert_entry 方法:返回 OccupiedEntry
let entry = map.entry("key2").insert_entry(100);
println!("key = {}, value = {}", entry.key(), entry.get());
}
使用场景
use std::collections::BTreeMap;
fn insert_if_valid<K: Ord, V>(
map: &mut BTreeMap<K, V>,
key: K,
value: V,
is_valid: impl FnOnce(&K, &V) -> bool,
) -> Option<V> {
let entry = map.entry(key).insert_entry(value);
if !is_valid(entry.key(), entry.get()) {
Some(entry.remove())
} else {
None
}
}
fn main() {
let mut map: BTreeMap<i32, i32> = BTreeMap::new();
// 100 >= 50,不满足条件,被移除
let removed = insert_if_valid(&mut map, 1, 100, |_k, v| *v < 50);
println!("removed = {:?}", removed); // 输出: Some(100)
}
设计哲学
这个 API 体现了 Rust 集合 API 的一个原则:Entry API 应该是完备的。
Entry 模式是 Rust 集合的标志性设计,它解决了"查找后插入"的效率问题:
// 低效:两次查找
if !map.contains_key(&key) {
map.insert(key, compute_value());
}
// 高效:一次查找
map.entry(key).or_insert_with(|| compute_value());
insert_entry 补齐了 Entry API 的最后一块:插入后继续操作条目。
五、Location::file_as_c_str:FFI 友好的调试信息
新增的 API
impl<'a> Location<'a> {
pub fn file_as_c_str(&self) -> &'a CStr;
}
这是什么?
std::panic::Location 用于获取代码位置信息(文件名、行号、列号),常用于 #[track_caller]:
#[track_caller]
fn my_unwrap<T>(opt: Option<T>) -> T {
match opt {
Some(v) => v,
None => {
let loc = std::panic::Location::caller();
panic!("Called unwrap on None at {}:{}:{}",
loc.file(), loc.line(), loc.column());
}
}
}
为什么需要 C 字符串版本?
当你需要把位置信息传给 C 代码时:
use std::ffi::{CString, c_char};
// FFI 场景:调用 C 的日志库
extern "C" {
fn c_log(file: *const c_char, line: u32, message: *const c_char);
}
#[track_caller]
fn log_from_rust(message: &str) {
let loc = std::panic::Location::caller();
let message_cstr = CString::new(message).unwrap();
// 之前:需要额外分配
// let file_cstr = CString::new(loc.file()).unwrap();
// 现在:零分配
let file_cstr = loc.file_as_c_str();
unsafe {
c_log(file_cstr.as_ptr(), loc.line(), message_cstr.as_ptr());
}
}
设计哲学
这个 API 很小,但体现了 Rust 的零成本互操作理念:
- 文件名字符串本来就以 null 结尾存储在二进制中
file_as_c_str只是暴露了这个事实,没有任何运行时开销- 对于 FFI 密集型代码,这些小优化累积起来很可观
六、TokenStream 扩展:过程宏的人体工程学改进
新增的 API
impl Extend<Group> for TokenStream { ... }
impl Extend<Ident> for TokenStream { ... }
impl Extend<Literal> for TokenStream { ... }
impl Extend<Punct> for TokenStream { ... }
之前怎么做?
use proc_macro::{TokenStream, TokenTree, Ident, Literal, Punct, Spacing, Span};
fn build_token_stream() -> TokenStream {
let mut tokens = TokenStream::new();
// 之前:必须先转换为 TokenTree
tokens.extend(std::iter::once(TokenTree::Ident(
Ident::new("hello", Span::call_site())
)));
tokens.extend(std::iter::once(TokenTree::Punct(
Punct::new(',', Spacing::Alone)
)));
tokens.extend(std::iter::once(TokenTree::Literal(
Literal::string("world")
)));
tokens
}
现在:更直接
use proc_macro::{TokenStream, Ident, Literal, Punct, Spacing, Span};
fn build_token_stream() -> TokenStream {
let mut tokens = TokenStream::new();
// 现在:直接 extend
tokens.extend([Ident::new("hello", Span::call_site())]);
tokens.extend([Punct::new(',', Spacing::Alone)]);
tokens.extend([Literal::string("world")]);
tokens
}
生态影响
过程宏是 Rust 生态的重要组成部分(serde、tokio、diesel 等都依赖它)。这个改进让手写过程宏更加愉快。
当然,大多数人会使用 quote! 宏,但对于需要动态构建 token 的场景,这个改进很有价值。
七、切片旋转的常量化
新增的能力
const ROTATED: [i32; 5] = {
let mut arr = [1, 2, 3, 4, 5];
arr.rotate_left(2);
arr
};
fn main() {
println!("{:?}", ROTATED); // 输出: [3, 4, 5, 1, 2]
}
使用场景
const fn rotate_bytes(s: &[u8; 5], n: usize) -> [u8; 5] {
let mut result = *s;
result.rotate_left(n);
result
}
const HELLO: [u8; 5] = *b"hello";
const LLOHE: [u8; 5] = rotate_bytes(&HELLO, 2);
fn main() {
println!("{}", std::str::from_utf8(&LLOHE).unwrap()); // 输出: llohe
}
设计哲学
Rust 正在逐步扩展 const fn 的能力边界。每个版本都会有一些函数被"常量化",这是一个渐进的过程:
- 编译期计算减少运行时开销
- 更多的编译期验证
- 支持更复杂的 const 泛型
总结:这个版本值得升级吗?
值得。 虽然没有革命性的新特性,但这些 API 都是实打实解决痛点的:
| API | 解决的痛点 | 受益场景 |
|---|---|---|
RwLockWriteGuard::downgrade | 锁降级的时间窗口 | 缓存、配置热更新 |
Box/Rc/Arc::new_zeroed | 大数据结构的栈溢出和复制开销 | 内存密集型应用 |
NonZero::div_ceil | NonZero 数值计算不便 | 分页、批处理计算 |
Entry::insert_entry | 插入后无法操作条目 | 复杂的 Map 操作 |
Location::file_as_c_str | FFI 场景的额外分配 | C 互操作 |
TokenStream::extend | 过程宏编写繁琐 | 宏开发 |
rotate_* const 化 | 编译期数组操作受限 | 查找表、编译期计算 |
升级命令:
rustup update stable
Rust 的演进哲学:不追求大版本的惊艳,而是每六周稳定地补齐一些 API、修复一些痛点。这种"无聊"的演进方式,恰恰是生产级语言应该有的样子。
每一个新稳定的 API 背后,都是社区多年的讨论、RFC、实现、测试。RwLockWriteGuard::downgrade 从提出到稳定用了九年——这就是 Rust 对 API 稳定性的承诺。