觉学社 04月03日
Rust 2024 年度报告(一)| Rust 2024 Edition 变更特性详解
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入探讨了Rust在2024 Edition中的主要更新,包括语言特性的改进、生态工具的变化,以及对Rust未来发展方向的展望。文章聚焦于Rust的核心理念——安全性与高性能,以及开放的社区治理和多领域扩张。通过对新版特性的详细分析,帮助读者了解Rust的最新进展,并对Rust的技术惯性、发展趋势有更全面的理解。

💡 Rust 2024 Edition 引入了 `if-let chains`,允许在单个 `if let` 表达式中串联多个条件,简化了代码结构,减少嵌套,提升可读性。

✨ 2024 Edition 改进了尾表达式临时值的作用域,使其在块结束时立即释放,提升了内存效率,但也可能导致部分依赖临时值生命周期的代码编译失败。

✅ 针对模式匹配,Rust 2024 Edition 规范了绑定模式,明确要求在非 move 绑定模式下不能使用 `mut`,并显式匹配引用类型,使代码行为更可预测,降低了理解难度。

🛡️ 在 Unsafe Rust 方面,新版通过在 `unsafe` 函数内部使用 `unsafe {}` 块来明确不安全操作的范围,防止意外的不安全操作,并允许使用 `safe` 关键字声明安全的外部函数,提升了代码安全性。

原创 张汉东 2025-02-23 20:04 美国

本报告旨在回顾 Rust 在 2024 年的主要进展、行业应用、社区建设,以及对未来发展的展望。本文为第一篇,为大家深度分析 Rust 2024 Edition 中变更特性。

2024 年对于 Rust 社区而言是意义非凡的一年。

从 2015 年正式发布 1.0 版本算起,Rust 语言已经走过了近十年的发展历程。在过去的一年里,Rust 官方团队与全球社区成员紧密协作,进一步丰富语言特性,完善工具链和生态系统,同时在各大开源项目和商业领域中都取得了令人瞩目的成果。

本报告旨在回顾 Rust 在 2024 年的主要进展、行业应用、社区建设,以及对未来发展的展望。

本文为第一篇,为大家深度分析 Rust 2024 Edition 中变更特性。


Rust 之道

某项技术一旦选择了某条发展路径,便会形成惯性,使其沿着这条道路延续下去。 —— Saylor, MicroStrategy CEO

最近在《What is Money ? The Saylor Series》 这本书中看到这句话时,我深表认同。

转而我又思考了 Rust 这项技术的“惯性”是什么。

在讨论 Rust 的“惯性”时,我们可以从它最核心、最具标识度的特性和发展轨迹入手。所谓“技术的惯性”,即一项技术在初步确立其核心理念和特性之后,所形成的内在驱动力与外部生态推动,使其在未来相当长一段时间内都沿着同样或相似的路径演进

对 Rust 来说,这种惯性主要体现在以下方面。


安全性(Safety)与高性能(Performance)的核心定位

安全与高性能形成了 Rust 的两大支柱,也是它在后续版本中不断打磨、提升的目标。这种方向一旦确立,语言的设计团队和社区就会在长期内保持对这两个特性的高度重视,难以在根本上偏离。


安全性

Rust 最为突出的特点便是其“内存安全”和“数据竞争安全”。它通过所有权(ownership)、借用(borrowing)与生命周期(lifetime)等机制,把绝大部分常见的内存错误在编译期就扼杀在摇篮里。

惯性表现:一旦确立了“安全至上”的基调,整个语言的演进都会围绕着如何保持并强化这种安全性进行。例如,编译器不断进化,社区也会非常谨慎地评估任何可能破坏安全模型的特性提案。


高性能

Rust 追求“接近 C/C++ 的性能”并保持“零成本抽象(zero-cost abstraction)”的理念。这决定了它会优先确保编译后的机器码在运行时的效率和资源占用上不输给传统系统级语言。

惯性表现:遇到任何语言特性的调整或新功能的添加,都会评估其对性能开销的影响,并尽量做到“在保持安全的同时不牺牲性能”。


生态系统和社区协作的开放治理

Rust 社区开放协作的特点和成体系的工具链是其核心竞争力之一。随着更多企业与个人的涌入,以及越来越多的教程、第三方库、培训资源的出现,Rust 的生态将继续沿着协作、包容的道路扩张。


开放的社区治理模式

Rust 社区以公开、透明、协作的模式闻名。通过 RFC(Request For Comments)流程、工作组(WG)等形式,汇集来自世界各地开发者的意见,共同商讨并推动语言和生态的发展。

惯性表现:当一个技术生态已经培养出一批热心且优质的贡献者,并形成了相对完善的治理流程,那么即便管理层或维护团队发生更迭,这种协作治理的传统也会延续下去。Rust 社区的多元与包容在很大程度上保证了发展的稳定性。


工具链与包管理

Cargo、Rustup、Rust Analyzer 等工具对 Rust 生态的成长贡献巨大。加上 crates.io(Rust crate 注册中心)的不断发展,让 Rust 项目在依赖管理、编译、测试、文档生成等环节都相对成熟、易用。

惯性表现:开发者对这些工具及其工作流已经形成了依赖和认可。因此在未来,Rust 社区会继续在工具链的易用性、性能、兼容性等方面发力,而不会轻易颠覆现有的使用习惯。


零成本抽象与现代语言特性的兼容

Rust 的抽象与现代化特性为开发者提供了相对愉悦的编程体验。这种惯性意味着在语言设计和实现上,Rust 仍然会不断引入新颖、安全且高效的特性,进一步巩固其“既安全又现代、既高效又优雅”的形象。


零成本抽象(Zero-Cost Abstraction)

Rust 一直以来都努力在抽象层级与性能之间取得平衡。它鼓励开发者使用高层抽象来写出可维护的代码,但编译器会在底层尽力消除抽象带来的额外开销。

惯性表现:语言特性、标准库和第三方库的开发都遵循这一思路,这会延伸到元编程、宏系统、异步编程等方向上。任何新提案若无法保证“零成本”或至少“可控成本”,都很难被社区所接受。


现代语言特性

Rust 拥抱了许多现代语言趋势,如强大的泛型系统、模式匹配、trait 机制、异步/并发支持等,并且在保证安全的前提下积极引入新的高级特性。

惯性表现:Rust 已经显示出对创新特性和最佳实践的热衷。社区保持着一定的前瞻性:当出现新的编程理念或学术研究时,Rust 也会尝试以 RFC 或试验性特性来进行探索。在不破坏核心安全/性能模型的前提下,这种尝试会持续下去。


多领域扩张与应用落地

Rust 已经成功在多领域站稳脚跟。只要这些场景的需求稳定增长,Rust 就会不断积累成功经验和实践案例,而这种正反馈将进一步增强 Rust 在更多行业的接受度。


系统编程领域的巩固

Rust 最初定位就是“系统编程语言”,在操作系统内核、驱动、编译器、网络底层等场景的应用愈发普及。

惯性表现:由于在系统编程领域已有成功实践和案例,更多企业、组织会将 Rust 用于底层开发和高并发服务,逐渐替代或补充 C/C++。社区针对底层和并发的优化与库支持也会持续推进。

向上层应用场景延伸

随着 AI 应用、 WebAssembly(Wasm)、服务器端后端开发、区块链、嵌入式和游戏引擎等领域的蓬勃发展,Rust 也在其中扮演越来越重要的角色。

惯性表现:一旦有一定数量的成功案例示范,更多项目和企业会跟进。在这个过程中,Rust 会相应地在标准库、第三方库、开发者工具上进一步投入,提供更完备的支持。

与其他语言及生态融合

Rust 提供了良好的 FFI(外部函数接口)能力,可以与 C/C++、Python、Node.js 等语言良好配合,同时借助 WebAssembly 进入 JS/浏览器环境。Rust 的 FFi 也许是第一个在语言层面考虑 Safe FFi 的语言(Unsafe 机制)。

惯性表现:为了适应与其他语言共存的场景,Rust 会继续完善跨语言绑定和互操作的工具,形成“跟传统语言互为补充”的局面。这一点对于 Rust 的长期发展至关重要。


小结

综上,Rust 走上了一条“安全 + 高性能 + 开放治理 + 多领域扩张”的发展道路,这些要素彼此紧密交织,构成了它的技术惯性。对于任何一种技术而言,若想在软件工业的洪流中保持持续增长和影响力,就必须在已有的核心竞争力之上不断演进,却不能轻易背离其根基。

Rust 的惯性使得它会在未来相当长的一段时间里都持续深耕安全性和高性能,加之完善的社区治理和活跃的工具生态,Rust 将在更多的应用场景中得到认可与青睐。这既是它在过去数年中形成的“惯性”,也是它不断前行、继续吸引开发者与企业的关键所在。


Rust 2024 Edition 特性全面解析

Rust 2024 Edition[1] 的名称虽然是“2024”,但由于开发节奏与社区协调的关系,在 2025 年 2 月 20 号已经正式稳定发布,对应 Rust 语义版本为 1.85 。

本节内容将为大家汇总这次 Edition 更新最值得关注的语言改进、新增特性以及生态工具变化,帮助你一窥 Rust 未来发展的方向。

详细资料 The Rust Edition Guide [2]  和 Rust Project Goals[3]


Rust Edition 机制及发布时间

何为 2024 Edition

Rust 使用 “Edition” 来标记大的语言里程碑版本,每隔三年发布一次(2018、2021、2024、2027 …)。

Rust Edition 的设计原则是:Edition 之间的代码互操作必须保持兼容,但同一个 Edition 内部的语法或语义调整可以是破坏性的。这意味着:

• 新 Edition 可以引入破坏性变更,但这些变更不会影响旧 Edition 的代码。 • 不同 Edition 的 crate 仍然可以互操作,不会因为 Edition 变更而导致 ABI 或类型系统不兼容。 • 迁移时有自动化工具(cargo fix)辅助,尽可能让代码从旧 Edition 平滑升级到新 Edition。

Rust 语言保证在大多数情况下,不同 Edition 的 crate 可以互操作,不会出现“一刀切”式的割裂

关于 2024 Edition 的时间安排的澄清

• 虽然名字叫“2024 Edition”,但它预计会在 2025 年 2 月 20 随 stable 版本发行。这是因为原本计划在 2024 年底完成,但开发过程出现了一些延期。 • Edition 与具体发布月份并非严格对应。 Rust 社区已确定三年一个 Edition 的节奏,但并不会为了“赶日期”而牺牲语言稳定性或让特性仓促上线。因此,Edition 名字只代表周期年份,与实际发布日期可能略有出入。


Rust 2024 Edition 语言层面特性

Rust 2024 Edition 包含的 Rust 语义版本范围为 1.56 ~ 1.85 。

Rust 2024 Edition 的使命是让 Rust 更简单易用,该 Edition 一共包括了三大旗舰目标和其他 23 个小目标。



1. if-let chains / let_chains

• 特性介绍

if-let chains 能让你在同一个 if let 表达式里进行多次模式匹配,从而减少层层嵌套的 if let 或 match。用法类似:


if let Some(x) = opt1 && let Ok(y) = some_func(x) && y > 10 {
    // 在单个 if-let 表达式中串联多个条件
}

为什么要等 2024 版本

• 该特性依赖一个被称作 “if_let_rescope” 的底层语言变更,涉及到作用域和解析规则,需要 Edition 级别的“破坏性改动”才能引入。

对比 Rust 2021 和 2024 edition 中 if_let_rescope 的主要差异:

特性
Rust 2021 Edition
Rust 2024 Edition (if_let_rescope)
临时值生命周期
临时值存活直到整个 if let 语句结束
临时值在进入分支前就被释放
编译器错误提示
只提示临时值在语句结束时被释放
提供更详细的错误信息,包括两种修复建议:使用 let 绑定或改写为 match
内存效率
临时值在内存中存活时间较长
临时值更早释放,内存使用更高效
语法一致性
与 match 表达式行为一致
与 let...else 语法行为一致
代码迁移方案
不适用
1. 使用显式 let 绑定
2. 改写为 match 表达式
3. 重构依赖临时值生命周期的代码
对现有代码影响
基准行为
可能导致依赖临时值生命周期的代码编译失败
借用检查行为
允许临时值的引用存活较长时间
对临时值引用的生命周期检查更严格

为什么这仍然符合 Rust Edition 的设计?

Rust Edition 允许在新 Edition 内部引入破坏性变更,但始终保证:

    不会影响旧 Edition 的代码:如果你不升级到 2024 Edition,你的代码仍然按照 Rust 2021 Edition 的规则解析,行为不会变。
    Edition 之间的 crate 可以互相调用:Rust 2021 和 Rust 2024 的 crate 仍然可以相互依赖,并不会因为 if_let_rescope 影响链接或交互。
    提供自动迁移工具:Rust 提供 cargo fix,如果你的代码受影响,可以自动转换为等价的新写法,保证升级尽量平滑。


2. 尾表达式临时值作用域

这又是一个与临时值作用域有关的变更。


// Before 2024
fn f() -> usize {
    let c = RefCell::new("..");
    c.borrow().len() // error[E0597]: `c` does not live long enough
}

在 2024 Edition 之前,上述代码 c.borrow() 会返回一个 Ref<T> 类型的临时值(未明确用 let 绑定的变量)。 这个借用值在计算完 len() 之后,应该被正常释放,而 c 变量在这之后被释放,这样的顺序才符合直觉。

但编译器会报错:c 在函数调用结束后仍然还被借用。

所以 2024 Edition 解决了这个反常的问题,上述代码编译正常。现在块中尾部表达式如何有临时值,就会被马上释放,而 c 在最后释放的时候,也不会被借用了。

但是这个规则的修改,会影响到「临时值作用域扩展」的某些情况。

比如下面代码:


// This example works in 2021, but fails to compile in 2024.
fn main() {
    let x = { &String::from("1234") }.len();
}

// error[E0716]: temporary value dropped while borrowed
//  --> src/main.rs:3:16
//   |
// 3 |     let x = { &String::from("1234") }.len();
//   |               -^^^^^^^^^^^^^^^^^^^-
//   |               ||                  |
//   |               ||                  temporary value is freed at the end of this statement
//   |               |creates a temporary value which is freed while still in use
//   |               borrow later used here

这段代码在 Rust 2021 Edition 编译正常,因为 &String::from("1234") 这个临时值作用域会被延伸到块表达式之外,从而正常求值 len()。但是 Rust 2024 Edition 规则修改之后,因为它是块中尾表达式,所以会释放,作用域不会被延伸到块外。

关于作用域延伸规则,我总结了一份代码示例:temporary_lifetime_extension.rs[4] 。


3. 改进 match 匹配

当我们进行模式匹配时,Rust 维护一个"默认绑定模式"状态,它决定了如何绑定变量:


let points = &[(12), (34)];

// 匹配引用时,默认绑定模式从 move 变为 ref
let [(x, y), ..] = points;  // x 和 y 都是 &i32

这个状态会根据我们遇到的模式动态改变。最常见的变化是当我们匹配一个引用时,默认绑定模式会从 move 变为 ref (match 匹配和所有权机制对应)。

2021 Edition 的规则存在一些令人困惑的行为:


let nums = &[12];

// 在同一个模式中得到不同的类型
let [x, mut y] = nums;
// x: &i32
// y: i32  // mut 神奇地改变了类型!

// 这种不一致性使代码难以理解和维护

这种行为违反了直觉,因为 mut 关键字不应该影响类型推导。

Rust 2024 Edition 中新的规则要求更明确的语法:


// 旧代码(Rust 2021)
let [x, mut y] = &[12];     // 允许但不推荐
let [ref x] = &[()];          // 允许但冗余
let [&x, y] = &[&1, &2];      // 允许但令人困惑

// 新代码(Rust 2024)
let &[ref x, mut y] = &[12];  // 清晰地表明引用关系
let [x] = &[()];                // 简洁且清晰
let &[&x, ref y] = &[&1, &2];   // 明确的引用处理

主要变化是:

    不允许在非 move 绑定模式下使用 mut
    不允许冗余的 ref
    引用模式必须显式匹配引用类型

这些改变使得代码的行为更加可预测,并且更容易理解每个变量的确切类型。你可以使用 cargo fix --edition 来自动迁移代码到新的语法。也可以使用 #![warn(rust_2024_incompatible_pat)] lint 来识别代码里的需要修改的地方。


4. Unsafe Rust 的四个主要变化


A. unsafe 函数中 unsafe 操作的重要变化

Rust 2021 Edition 中,unsafe 函数扮演了两个角色:

    告诉调用者:"调用这个函数需要 unsafe"
    允许函数体内直接使用不安全操作

// Rust 2021 Edition
unsafe fn old_get_unchecked<T>(slice: &[T], index: usize) -> &T {
    // 危险:不安全操作没有明确的边界
    slice.get_unchecked(index)
}

Rust 2024 Edition 中


unsafe fn new_get_unchecked<T>(slice: &[T], index: usize) -> &T {
    // 安全:明确标记了不安全操作的范围
    unsafe { slice.get_unchecked(index) }
}

这个改变解决了几个关键的安全问题:

明确的不安全边界


unsafe fn process_data(data: &[u8]) -> u8 {
    // 一些安全的操作
    let len = data.len();
    
    // 清晰地标记出不安全的部分
    unsafe {
        *data.get_unchecked(len - 1)
    }
    
    // 继续安全的操作
}

防止意外的不安全操作


unsafe fn complex_operation(ptr: *mut i32) {
    // 编译器会警告这里缺少 unsafe 块
    *ptr = 42;  // 警告!
    
    // 正确的写法
    unsafe {
        *ptr = 42;
    }
}

最佳实践


unsafe fn update_value(ptr: *mut i32) {
    // 安全检查
    debug_assert!(!ptr.is_null());
    
    // SAFETY: ptr 的有效性在函数签名中保证
    unsafe {
        *ptr += 1;
    }
}

B.  Unsafe extern blocks

对应 RFC 3484 。

Rust 2021 Edition 中, 所有的 extern 声明的函数默认都被认为是 unsafe 的。这意味着调用任何外部函数都需要使用 unsafe 块:


// Rust 2021 及之前
extern "C" {
    fn external_function(x: i32);
}

fn main() {
    // 必须使用 unsafe 块
    unsafe { external_function(42); }
}

在 Rust 2024 中,我们可以使用 safe 关键字明确声明某个外部函数是安全的:


unsafe extern"C" {
    // sqrt 对任何 f64 值都是安全的
    safe fn sqrt(x: f64) -> f64;
    
    // strlen 需要有效的指针,所以必须是 unsafe
    unsafefn strlen(p: *consti8) -> usize;
}

fn main() {
    // 可以直接调用,不需要 unsafe 块
    sqrt(42.);
    
    // 仍然需要 unsafe 块
    unsafe { strlen(ptr); }
}

使用 safe 的责任

当我们标记一个外部函数为 safe 时,我们在向编译器和其他开发者承诺:

    这个函数对于任何合法的 Rust 值都是安全的
    这个函数不会违反 Rust 的安全保证
    这个函数的签名完全捕获了所有的不变性要求

这个新特性提供了几个重要的好处:

    更好的安全性声明:现在我们可以明确地表达哪些外部函数是真正安全的。
    更好的 API 设计:库作者可以更清晰地表达他们的 API 安全保证。
    减少不必要的 unsafe 块:对于确实安全的外部函数,我们不再需要使用 unsafe 块。

这个变化体现了 Rust 一直以来的理念:尽可能在类型系统中表达安全性保证,让编译器帮助我们确保安全。


C. Unsafe Attributes

在 Rust 2024 中,三个关键属性必须被标记为 unsafe:

    export_name
    link_section
    no_mangle

理解符号碰撞问题

符号名称在链接的库之间形成了一个全局命名空间。通常,Rust 的名称重整(name mangling)机制会确保每个符号名称的唯一性。但是这些属性可能会打破这种保证:


// 这段代码在大多数类 Unix 系统上会导致崩溃
fn main() {
    println!("Hello, world!");
}

#[export_name = "malloc"]
fn foo() -> usize { 1 }

这段代码看起来是安全的,但实际上它覆盖了系统的 malloc 函数,这可能导致灾难性的后果。

在 Rust 2024 中,我们必须明确承认这种危险:


// SAFETY: 我们确保这个自定义的 malloc 实现满足所有必要的要求
#[unsafe(export_name = "malloc")]
fn custom_malloc() -> usize { 1 }

当使用这些 Unsafe 属性时,我建议遵循以下原则

    总是提供详细的安全性文档,解释为什么这个符号名称的使用是安全的
    建立一个项目级别的符号命名约定
    使用版本控制系统的工具来检测潜在的符号冲突
    在代码评审时特别关注这些属性的使用

D. 标准库中某些 Safe 函数变为 Unsafe 函数

在 Rust 2024 中,以下标准库函数被标记为 unsafe:


// 环境变量操作
std::env::set_var
std::env::remove_var

// Unix 特定的进程控制
std::os::unix::process::CommandExt::before_exec

为什么呢?


// 在 Rust 2021 中是安全的
fn configure_app() {
    std::env::set_var("APP_MODE""production");
}

// 在 Rust 2024 中必须显式声明不安全性
fn configure_app() {
    // SAFETY: 我们确保这段代码只在程序启动时的单线程环境中执行
    unsafe { std::env::set_var("APP_MODE""production"); }
}

拿环境变量来说,环境变量操作在多线程环境中可能导致未定义行为。


use std::thread;

fn problematic_code() {
    // 线程1修改环境变量
    thread::spawn(|| {
        std::env::set_var("SHARED_CONFIG""value1");
    });

    // 线程2同时修改同一个环境变量
    thread::spawn(|| {
        std::env::set_var("SHARED_CONFIG""value2");
    });
    
    // 这种并发访问可能导致未定义行为!
}

before_exec 函数的安全问题更加微妙

std::os::unix::process::CommandExt::before_exec 函数是一个特定于 Unix 的函数,它提供了一种在调用 exec 之前运行闭包的方法。该函数在 1.37 版本中被弃用,并被 pre_exec 替代,后者执行相同的操作,但标记为 unsafe 。

尽管 before_exec 已被弃用,但从 2024 版开始,它现在被正确标记为 unsafe 。这应该有助于确保任何尚未迁移到 pre_exec 的遗留代码需要一个 unsafe 块。

理解 pre_exec 和 before_exec 为什么设计为 unsafe 函数,有助于理解 Rust 的安全性与系统领域复杂性之间的关系

在早期,Rust 团队可能认为这些函数主要是进程控制的工具,看起来似乎不涉及内存安全问题。但随着对系统编程领域更深入的理解,团队发现了更多的安全隐患。

在 Unix 系统中,创建新进程通常需要两个步骤:fork 和 exec。这两个操作各自完成不同的任务:


use std::process::Command;

// 这行代码背后实际上包含了 fork 和 exec 两个操作
Command::new("ls").spawn()?;

fork 之后:

    子进程获得父进程的完整副本
    两个进程同时运行
    子进程从 fork 返回的地方开始执行
    所有资源(内存、文件描述符等)都被复制

exec 会:

    完全替换当前进程的程序
    保持进程 ID 不变
    清除大部分进程状态,但保留一些重要的属性(如文件描述符)
    从新程序的入口点开始执行

pre_exec 的作用


let mut cmd = Command::new("program");
unsafe {
    cmd.pre_exec(|| {
        // 这个闭包在 fork 之后,exec 之前执行
        // 这是设置子进程特定属性的最后机会
        
        // 例如:设置进程组
        libc::setpgid(00);
        
        // 或者关闭不需要的文件描述符
        libc::close(fd);
        
        Ok(())
    });
}

这就是为什么 pre_exec 的安全性要求如此严格。

它运行在一个非常特殊的时刻,任何错误都可能导致严重的问题。比如,如果在 fork 和 exec 之间发生崩溃,可能会导致进程处于不一致的状态。

在这个环境中,只有**异步信号安全 (async-signal-safety)**的操作是允许的。这是一个很严格的要求:


use std::os::unix::process::CommandExt;
use libc;

letmut cmd = Command::new("program");
unsafe {
    cmd.pre_exec(|| {
        // 安全:使用 libc 的异步信号安全函数
        libc::setpgid(00);  // 设置进程组 ID
        
        // 危险:println! 不是异步信号安全的
        // println!("Child process starting");  // 不要这样做!
        
        Ok(())
    });
}

异步信号安全 的意思是,在处理信号时的代码是“安全的”,即它能在任何时候被异步地调用,而不会导致未定义的行为、数据竞争或死锁。这一点尤其重要,因为信号处理器的执行与正常程序的执行是并行的,所以信号处理器中的代码需要确保不会干扰正常的程序运行。

异步信号安全比线程安全更加严格。因为 信号处理函数可以在程序的任何位置被中断执行,包括在多线程或异步代码执行期间。信号处理器应该避免使用不安全的操作,例如调用可能引发阻塞或竞争条件的非线程安全的库函数。

在某些情况下,信号处理器如果调用了不具备异步安全性的函数(比如标准库中的某些函数),可能会导致程序崩溃、死锁或者其他难以调试的问题。

异步信号安全的操作一般具有以下特征

    不涉及动态内存分配
    不访问或修改全局状态
    不调用非重入函数
    只执行最基本的系统调用

这种限制看似严格,但它实际上反映了一个深层的系统编程真理在某些特殊的系统状态下,我们需要极其谨慎,只能执行最基本、最可靠的操作。这就是为什么 pre_exec/before_exec 必须被标记为 unsafe,因为它要求开发者完全理解这些复杂的系统编程概念,并严格遵守这些安全限制。


5. 永无类型回退类型变更

永无类型(即,never type, !)的默认回退类型 从 () 改为 ! 。

• 之前的行为存在困惑:在 2021 Edition 及之前的 Rust 中,! 类型有时会被自动转换为 (),这在一些情况下非常令人困惑。特别是当类型推导失败时,! 会被转换成(),即使原始类型就是!,这可能导致意外的行为。

• 现在的行为更直观:通过将 ! 保持为 !,而不自动转换为 (),现在的行为变得更加符合直觉,尤其是对于不可达代码(如 panic!() 或 loop)的处理。


// 在 2024 之前可以工作
fn generic<T: Default>() -> Result<T, ()> {
    Ok(T::default())
}

fn main() -> Result<(), ()>{
    generic()?;  
    Ok(())
}

这段代码在 Rust 2021 Edition 中有警告:


warning: this function depends on never type fallback being `()`
 --> src/lib.rs:8:1
  |
8 | fn main() -> Result<(), ()>{
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in Rust 2024 and in a future release in all editions!
  = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/never-type-fallback.html>
  = help: specify the types explicitly
note: in edition 2024, the requirement `!: Default` will fail
 --> src/lib.rs:9:5
  |
9 |     generic()?;  
  |     ^^^^^^^^^
  = note: `#[warn(dependency_on_unit_never_type_fallback)]` on by default
help: use `()` annotations to avoid fallback changes
  |
9 |     generic::<()>()?;  
  |            ++++++

还有闭包中的 panic


trait MyTrait {}
impl MyTrait for () {}

// 2024 之前
fn run<R: MyTrait>(f: impl FnOnce() -> R) {
    f();
}
run(|| panic!());  // 可以工作,! 会转换为 ()

在 Rust 2024 Edition 中报错:


error[E0277]: the trait bound `!: MyTrait` is not satisfied
  --> src/lib.rs:9:5
   |
  |     run(|| panic!());  // 可以工作,! 会转换为 ()
   |     ^^^^^^^^^^^^^^^^ the trait `MyTrait` is not implemented for `!`
   |
   = help: the trait `MyTrait` is implemented for `()`
   = note: this error might have been caused by changes to Rust's type-inference algorithm (see issue #48950 <https://github.com/rust-lang/rust/issues/48950> for more information)
   = help: did you intend to use the type `()` here instead?

所以 2024 Edition 中需要指定转换类型 run(|| -> () { panic!() });

这个改变有几个重要的意义

    类型推断的一致性:让 Never 类型的行为更加直观和可预测。
    !类型的稳定化:移除了阻止 Never 类型稳定化的一个主要障碍。
    安全性提升:通过 never_type_fallback_flowing_into_unsafe lint 的提升,可以捕获潜在的未定义行为。


6. 宏片段说明符(Macro Fragment Specifiers)的变更

在 2021 Edition 中,expr 片段指定符不会匹配 const 表达式,而会匹配常规的表达式。然而,在 2024 Edition 中,expr 片段指定符现在也支持匹配 const 和 _ 表达式。

看下面代码的输出:

Rust 2021 Edition :


macro_rules! example {
    ($e:expr_2021) => { println!("first rule"); };
    ($e:expr) => { println!("second rule"); };
    (const $e:expr) => { println!("const rule"); };
}

fn main() {
    example!( 1+1 );  // first rule
    example!(const { 1 + 1 }); // const rule
}

Rust 2021 Edition :


macro_rules! example {
    // ($e:expr_2021) => { println!("first rule"); };
    ($e:expr) => { println!("second rule"); };
    (const $e:expr) => { println!("const rule"); };
}

fn main() {
    example!( 1+1 );  // second rule
    example!(const { 1 + 1 }); // const rule
}

Rust 2024 Edition:


macro_rules! example {
    ($e:expr_2021) => { println!("first rule"); };
    ($e:expr) => { println!("second rule"); };
    (const $e:expr) => { println!("const rule"); };
}

fn main() {
    example!( 1+1 ); // first rule
    example!(_);     // second rule
    example!(const { 1 + 1 }); // second rule
}

Rust 2024 Edition:


macro_rules! example {
    ($e:expr_2021) => { println!("first rule"); };
    (const $e:expr) => { println!("const rule"); };
}

fn main() {
    example!( 1+1 ); // first rule
    example!(const { 1 + 1 }); // const rule
}

关于宏片段说明符还有第二个变更: 缺失的宏片段说明符。


// 在 Rust 2021 中这会产生警告
macro_rules! incorrect {
    () => {};
    ($name) => {};  // 警告:缺少片段说明符
}

// 正确的写法应该是
macro_rules! correct {
    () => {};
    ($name:ident) => {};  // 指定了片段说明符
}

在 Rust 2024 中,这个警告变成了错误。


7.  gen 关键字引入 与 生成器

2024 版本的 Rust 引入了 gen 关键字,这是基于 RFC #3513 的提案,旨在通过 gen 块提供一种更简洁的方式来定义迭代器,减少手动实现Iterator trait 的复杂性。

生成器使得编写迭代器变得更加直观和简洁,它允许你以 命令式(imperative)方式编写代码,并使用 yield 关键字逐步生成值。

从 RFC 的例子来看,gen 块允许开发者写类似于以下的代码:


gen {
    for i in 0..10 {
        yield i * 2;
    }
}

这将生成一个迭代器,产生 0, 2, 4, ..., 18 的序列,类似于 Python 的生成器(generator)。

注意,这里需要明确一个重要变更:**Rust Async/Await 底层机制之前也叫做「生成器(Generator)」,但是现在已经改为了「协程(Coroutine)」**[5]


8. 深化 async 支持

Rust 的异步(async)生态系统目前正处于积极发展阶段,部分核心功能已经成熟并接近完成,但整体仍未达到全面成熟的状态。异步编程在 Rust 中已经可以通过异步函数和基本运行时支持实现,但多个关键领域的进展缓慢或暂停,导致生态系统在功能完整性和开发者体验上仍有较大提升空间。


Async Fn

在旧版本中,trait 里定义异步函数较为繁琐,需要借助第三方宏(如 async_trait)或手动编写 Pin<Box<dyn Future<...>>> 的返回值。

直接在 trait 中支持 async fn 可以显著减少样板代码、提高可读性。

• 实现效果

Rust 2024 Edition 允许直接编写:


trait MyAsyncTrait {
    async fn do_something(&self);
}

虽然仍然还需要在动态兼容(原为 object safety,此术语改为了 dyn compatibility )等方面进行特殊处理,但总体使用体验会更自然。


Async closure

在 Rust 2024 之前,如果我们想写一个接受异步回调的函数,我们需要这样做:


async fn for_each_city<F, Fut>(mut f: F)
where
    F: for<'cFnMut(&'c str) -> Fut,  // 需要分开声明函数类型和 Future 类型
    Fut: Future<Output = ()>,
{
    for x in ["New York""London""Tokyo"] {
        f(x).await;
    }
}

这种方式存在问题,因为我们不能正确处理生命周期。

使用旧的语法 || async {} 时,异步块不能引用闭包捕获的值:


async fn main() {
    let mut results = vec![];
    
    // 这段代码会失败
    for_each_city(|city| async {
        results.push(process_city(city).await);
    })
}

为了解决上述问题,Rust 2024 Edition 引入了完整的异步闭包支持:


// 新的异步闭包语法
let closure = async || {
    // 可以使用 await
    // 可以访问捕获的值
};

// 新的 trait 
trait AsyncFn<Args> {
    type CallRefFuture<'a>: Future<Output = Self::Output>;
    fn async_call(&self, args: Args) -> Self::CallRefFuture<'_>;
}

// 更简洁的回调表达
asyncfn for_each_city<F>(f: implasyncFn(&str)) {
    for city in ["New York""London"] {
        f(city).await;
    }
}

异步闭包允许返回的 Future 借用闭包捕获的值。这是通过特殊的编译器支持实现的。异步闭包 trait 也对应同步闭包的三种 trait : async FnOnce() / async Fn() / async FnMut() 。


use<'a>  新语法

在 Rust 的异步编程中,尤其是在处理生命周期和泛型参数时,返回位置的 impl Trait(RPIT)在不同场景下的生命周期捕获规则存在不一致性。

在 Rust 中,异步编程依赖于 Future trait 和 async/await 语法。async 块和 async fn 会生成一个 Future 类型,这个类型的生命周期与其捕获的变量密切相关。然而,在 Rust 2021 及之前的版本中,生命周期捕获规则存在以下不一致性:

    普通函数中的 RPIT:返回位置的 impl Trait(如 fn foo() -> impl Trait)不会自动捕获作用域内的生命周期参数,除非这些生命周期显式出现在函数签名的边界中。
    trait 实现和 async fn 中的 RPIT:在 trait 实现中的 RPIT、trait 定义中的 RPIT(RPITIT)以及 async fn 生成的 Future 类型中,所有作用域内的生命周期参数都会被隐式捕获。

这种不一致性导致开发者在编写异步代码时,尤其是涉及 trait 和 impl 块时,需要使用一些技巧(如 Captures trick 或 outlives trick)来手动管理生命周期的捕获。这不仅增加了代码的复杂性,还容易出错。

为了解决这些问题并提供更细粒度的控制,Rust 引入了 use<..> 语法。

在异步编程中,Future 类型的生命周期管理至关重要。use 语法通过允许开发者精确控制捕获的生命周期,确保异步任务的 Future 类型具有正确的生命周期约束。

以下是具体作用:

    明确捕获生命周期:
      在异步函数中,Future 类型需要正确捕获相关的生命周期,以确保引用的数据在异步任务中可用。
      use<..> 语法允许开发者显式指定捕获的生命周期,避免隐式捕获导致的行为不一致。

示例:异步闭包中的生命周期管理:


async fn process_data<'a>(data: &'a [u8]) -> impl Future<Output = ()> + use<'a> {
 async move {
  // 处理 data
 }
}

在这个例子中,use<'a> 确保返回的 Future 捕获了 'a 生命周期,使得 data 可以在异步块中安全使用。如果不使用 use<'a>,在 Rust 2024 中,'a 会被隐式捕获,但在 Rust 2021 中可能不会,导致行为不一致。


展望: 异步取消(Async Cancellation)和异步 drop(Async Drop)

在 Rust 的异步编程中,异步取消和异步 drop 是两个关键主题,尤其是在任务终止和资源管理方面。

异步取消是指在异步任务执行过程中,能够安全地终止任务并清理资源的机制。这通常用于需要提前结束任务的场景,例如用户取消操作或超时处理。当前,Rust 目前没有一个标准化的、跨运行时的异步取消机制。

当前,开发者通常通过以下方式实现异步取消:

    使用 Future 的 drop 方法手动丢弃任务。
    使用运行时提供的工具,例如 tokio::select! 宏来选择性地终止任务。

然而,这些方法在不同异步运行时(如 Tokio 和 async-std)之间可能存在不一致的行为,增加了代码移植和维护的难度。

异步 drop 允许在异步上下文中执行资源的清理操作。例如,当一个异步任务结束时,需要异步地释放资源(如异步文件句柄、数据库连接等)。

Rust 目前不支持异步 drop,资源的清理操作仍然是同步的。开发者通常通过在 drop 函数中启动新任务来实现异步清理,但这可能导致程序退出前任务未完成的问题。但目前标准库已经引入一些实验性的特性,比如 async_drop in std::future[6] 。

未来,为了更可靠地进行异步清理,Rust 没准会引入线性类型(Linear Types) 。

线性类型是一种类型系统特性,确保某些值在使用后必须被显式处理或销毁,不能简单丢弃。


8. GATs(Generic Associated Types)未得到深化

• 什么是 GATs

在 Rust 2024 Edition 中,泛型关联类型(Generic Associated Types,GATs)是一个重要的特性。它允许在 trait 中定义与类型参数相关的关联类型。这使得 trait 的灵活性和表达能力得到了显著提升。

泛型关联类型的主要优势包括:

    更好的类型表达:可以在 trait 中定义与类型参数相关的关联类型,从而使得 trait 的实现更加灵活。
    简化代码:可以减少类型的重复定义,简化代码结构。
    增强的抽象能力:GATs 允许开发者创建更复杂的抽象,能够更好地适应不同的使用场景。

trait StreamingIterator {
    type Item<'a>;
    fn next(&mut self) -> Option<Self::Item<'_>>;
}
    泛型关联类型稳定化(GATs)

    trait Iterator {
        type Item<'awhere Self'a;
        fn next(&mut self) -> Option<Self::Item<'_>>;
    }
    允许在trait中定义泛型关联类型

GATs 自 Rust 1.65(2022 年 11 月)起稳定,允许特质中的关联类型带有泛型参数,增强了类型系统的表达能力。鉴于 Rust 2024 Edition 是基于后续版本,且相关文档(如 Rust 2024 Edition Guide[7])未明确提及 GATs 新变化,确认 GATs 在 2024 Edition 中为延续现有功能。


小结

本文为大家深入分析了 Rust 2024 Edition 的最值得关注的语言特性变更。下一篇我将为大家带来 《Rust 2024 年度报告(二)| 深入剖析 Rust 在 AI 领域的应用与革新》。

感谢阅读。

参考资料
[1] 

Rust 2024 Edition: https://github.com/rust-lang/rust/pull/133349

[2] 

The Rust Edition Guide : https://doc.rust-lang.org/edition-guide/rust-2024/index.html

[3] 

Rust Project Goals: https://rust-lang.github.io/rust-project-goals/index.html

[4] 

temporary_lifetime_extension.rs: https://gist.github.com/ZhangHanDong/33eaa28a501fecb364c61421fb5223d5

[5] 

「协程(Coroutine)」**: https://doc.rust-lang.org/beta/unstable-book/language-features/coroutines.html?search=

[6] 

async_drop in std::future: https://doc.rust-lang.org/stable/std/future/fn.async_drop.html

[7] 

Rust 2024 Edition Guide: https://doc.rust-lang.org/edition-guide/rust-2024/index.html


阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Rust 2024 Edition 编程语言 安全性 高性能
相关文章