news 2026/10/21 23:25:09

Rust 全局声明体系重构:RFC 0246 对 `const` 与 `static` 的划分及其在现代 Rust 中的演进

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Rust 全局声明体系重构:RFC 0246 对 `const` 与 `static` 的划分及其在现代 Rust 中的演进
  • 文档

【免费下载链接】rfcs

RFCs for changes to Rust

项目地址:https://gitcode.com/gh_mirrors/rf/rfcs
点击查看免费下载

本文以 Rust RFC 仓库中的 text/0246-const-vs-static.md 为绝对主线,系统梳理 2014 年该 RFC 提出"将全局声明拆分为const常量与static静态变量两大类"的完整设计:从设计动机、语法与语义规则、全局项相互引用约束、模式匹配限制,到它对后续const fn、'static生命周期默认值、rvalue 提升等机制的影响。读完本文,你将能准确回答"什么时候该用const、什么时候该用static、二者为何不可互相替代",并理解现代 Rust 中static的Sync约束、只读内存放置与static mut的 unsafe 语义究竟从何而来。

背景:为什么一个static关键字不够用

在 RFC 0246 提出之前(2014 年 8 月),Rust 只有唯一的关键字static来声明全局变量。默认情况下,全局变量"不拥有显著地址(significant address)",可以被内联进程序;若想让它拥有显著地址,需要显式标注#[inline(never)];可变全局则使用static mut,而对它的所有访问都被视为 unsafe。此外,为了让static可以放进只读内存,其类型内部不允许出现UnsafeCell(即不允许内部可变性)。

该 RFC 指出这套单一static设计存在若干具体问题:

  • 无法安全地构建全局计数器或锁:这类需求必须借助static mut,而访问它属于非法(unsafe)操作。
  • #[inline(never)]的语义不直观:用"是否内联"来间接表达"是否有显著地址",对使用者极不友好。
  • 无法声明泛型常量:单一static机制无法表达带类型参数的常量。
  • static与let的类比对用户造成误导:二者表面相似,行为却截然不同——static大多声明的是可内联、无固定地址的"值"而非"变量";static不能有内部可变性而let可以;static因可出现在模式中而禁止被遮蔽,let则相反。
  • 语言中其他看似全局数据的构造(如无参枚举变体、无字段结构体)实际上是"无地址的值",被归类为 rvalue,与static的行为并不一致。

这些痛点共同指向一个结论:需要一个更清晰的类型系统划分来承载"常量值"与"全局变量"两种截然不同的语义。

核心提案:把全局声明拆成两类

RFC 0246 的 Summary 给出了整篇设计的总纲:将全局声明划分为两大类别——

  • const(常量)声明的是"常量值":它表示一个值(value),而不是一个内存地址。这是绝大多数场景下开发者会优先选择的机制,几乎可以取代当时所有对static的常规使用。
  • static(静态变量)声明的是"全局变量":它表示一个内存地址(memory address)。这类需求很少见,主要使用场景是:全局锁、全局原子计数器,以及与遗留 C 库交互。

这一划分确立了此后 Rust 全局声明体系的基本骨架,也是现代 Rust 中const与static语义差异的源头。

常量(const):声明的是值

RFC 重新引入const声明,语法为:

const name: type = value;

规则要点如下:

  • 可在任意作用域声明,且不能被遮蔽(cannot be shadowed)。
  • 常量被视为 rvalue:对常量取地址时,实际是在局部栈上创建一个位置——常量"按定义"没有显著地址。
  • 常量被设计为与无参枚举变体(nullary enum variants)行为完全一致:它们是真正的值,不占据固定的内存位置。

这一"常量即值、无地址"的定位,为编译器的优化敞开了大门:常量值可以被直接内联进程序,编译器也可以按需自由复制它们,而不必担心别名问题。正如 Motivation 中所说,如果常量被内联进下游 crate,那么一旦该常量发生变化,下游 crate 就必须重新编译——这是内联语义的固有代价。

可能的扩展:泛型常量

RFC 指出,允许常量携带泛型参数是完全合理的。例如:

struct WrappedOption<T> { value: Option<T> } const NONE<T> = WrappedOption { value: None }

这个扩展对static而言毫无意义——静态变量表示一个内存位置,其类型必须是具体的(concrete type)。泛型常量这一设想后来以"泛型常量表达式"等形式在语言中长期演进(相关讨论散见于 text/2000-const-generics.md 等 RFC 中),而"static 必须类型具体"这一约束则始终成立。

可能的扩展:常量函数(const fn的雏形)

RFC 0246 还设想了一种"常量函数"(constant functions),用来解决初始化逻辑封装的问题。为避免界定"哪些代码可以在常量函数中执行"的困难,可以将其从语法上限定为单个可在编译期展开的常量表达式(禁止递归)。原文档给出的示例是:

struct LockedData<T:Send> { lock: Lock, value: T } const LOCKED<T:Send>(t: T) -> LockedData<T> { LockedData { lock: INIT_LOCK, value: t } }

RFC 认为这能让我们把UnsafeCell的value字段设为私有(因为它提供了在常量上下文中初始化含锁/原子类型的安全途径)。

这个"可能扩展"后来被 text/0911-const-fn.md 正式落实:2015 年 2 月的 RFC 0911 允许将自由函数与固有方法标记为const,使其能在常量上下文中以常量实参被调用,从而解决UnsafeCell字段被迫公开的稳定化与安全性隐患(例如AtomicUsize、Cell<T>等在常量上下文中无法初始化的问题)。RFC 0911 的详细设计中明确列出了常量值表达式的允许集合(原始字面量、ADT、基础类型上的运算、类型转换、字段访问、无捕获闭包、引用与块表达式等),并排除了副作用与一般 CTFE 机制。从仓库中的文档脉络可以清晰看到:RFC 0246 提出"常量函数"设想 → RFC 0911 将其落地为const fn→ 如今const fn已成为现代 Rust 的核心特性,被AtomicBool::new、Cell::new等大量标准库 API 使用(text/0911-const-fn.md 中的示例即包含static FLAG: AtomicBool = AtomicBool::new(true)等现代写法)。

静态变量(static):声明的是内存位置

RFC 将static声明重新定位为只声明静态变量,规则如下:

  • 静态变量总是拥有唯一的地址(single address)。
  • 可以可选地声明为mut(static mut)。
  • 其生命周期恒为'static。
  • 不允许从 static 中 move 值。
  • 对 static 的访问会产生真实的内存读写:其值不会被内联(但见文末"未决问题")。

进一步的安全性规则是本 RFC 对现代 Rust 影响最深的部分:

  • 非mut的 static,其类型必须满足Syncbound。对该 static 的一切访问(读取变量、取其地址)都被视为安全操作。如果 static 的类型内部不包含UnsafeCell,编译器可将其放入只读内存;否则必须放在可变内存中。
  • mutstatic 可以是任意类型,但所有访问均被视为 unsafe,且不允许放入只读内存。

这条"非 mut static 必须Sync、内部无UnsafeCell才可进只读内存"的规则,直接呼应了 Motivation 中"我们希望把大型常量放进只读内存,但这要求数据必须真正不可变,否则将导致段错误"的诉求。它也与 text/0458-send-improvements.md 中"在安全代码中,所有 static 项都要求Syncbound"的描述相互印证——Sync是&T可跨线程共享的前提,而static恰恰是对&'static T的最直接利用。

全局项之间的相互引用规则

RFC 0246 用一整节细致规定了const与static互相引用时的合法性与编译器行为,这是最容易写错、也最值得精读的部分。原文档以SomeStruct为例逐一对四种组合做了规定。

const => const:允许,但存在内部可变性限制

可以创建一个const或static通过地址引用另一个const或static:

struct SomeStruct { x: uint } const FOO: SomeStruct = SomeStruct { x: 1 }; const BAR: &'static SomeStruct = &FOO;

常量通常被内联进引用它的栈帧,但在 static 上下文中没有栈帧。此时编译器会将其**改写(reinterpret)**为:

struct SomeStruct { x: uint } const FOO: SomeStruct = SomeStruct { x: 1 }; const BAR: &'static SomeStruct = { static TMP: SomeStruct = FOO; &TMP };

即:编译器引入一个static,以便给该常量一个真正具有'static生命周期的指针。由于这一改写,编译器将禁止SomeStruct内部包含UnsafeCell(内部可变性)。一般性结论:若常量 B 内部含有UnsafeCell,则常量 A 不能引用 B 的地址。

const => static:非法

常量不允许引用另一个 static。原因在于:常量表示"常量值",而 static 表示"内存位置",在二者的定义下这种引用难以调和。这正体现了"值语义"与"位置语义"的根本冲突。

static => const:允许,但只有 Sync 限制

如果static引用某个const的地址,会发生类似的改写,但没有内部可变性限制——只有Sync限制。也就是说,被引用的常量类型只需满足Sync,即可被静态变量以地址方式引用。

static => static:按值引用被禁止

静态变量禁止按值引用另一个 static,所有引用都必须是借用(borrow),而且只允许显式取另一个 static 的地址;字段/元素的内部借用(interior borrows)以及数组元素访问均被禁止。

RFC 解释了为何禁止按值引用:如果允许按值引用,则被引用的 static 只能落入两种情形——

  1. 它是初始化器模式(initializer pattern),但这正是const的用途;
  2. 两个值需要保持同步,而这在当时技术上不可行。

既然两种情形都无法合理成立,编译器干脆禁止任何 static 按值引用其他 static。

模式匹配中的使用限制

在原设计中,static允许出现在模式匹配(pattern matching)中。RFC 0246 规定:引入const之后,static将被禁止出现在 match 模式中,只有const可以出现。

这一决定把"哪些全局项可作为模式"的问题锚定在值语义上——模式匹配本质上是对值的结构化比对,与"内存位置"无关。这一规则在后来的 text/1445-restrict-constants-in-patterns.md 中得到深化:RFC 1445 引入了#[structural_match]属性与#[derive(Eq)]自动标记机制,将可出现在模式中的常量限定为"内置类型或派生了Eq的结构化类型",以消解"语义相等(==)"与"结构化相等"之间的分歧;而更晚的 text/3535-constants-in-patterns.md 则以"常量即手写模式的语法糖"为立场,进一步明确了PartialEq派生、浮点 NaN、指针类型的结构化相等判定规则。可以说,RFC 0246 的"只有 const 能进模式"这一定位,是整个常量模式体系后续十余年演进的起点。

后续演进:RFC 0246 的设计如何在现代 Rust 中落地

RFC 0246 提出的不少设想在后续 RFC 中被正式实现和扩展。追踪这些关联文档,可以完整还原一条"从设计到落地"的演进链。

'static生命周期默认值(RFC 1623)

text/1623-static.md(2016 年)指出:当时在static与const声明中书写引用必须显式写出&'static ..,既繁琐又造成严重的右移漂移。该 RFC 将 RFC 0599 为 trait object 设定的默认值规则推广到 static/const 声明:未显式给出生命周期时,编译器在全部引用生命周期(包括通过泛型替换获得的引用生命周期)中默认'static。

例如下面的类型声明可以从:

static my_awesome_tables: &'static [&'static HashMap<Cow<'static, str>, u32>] = ..

简化为:

static my_awesome_table: &[&HashMap<Cow<str>, u32>] = ..

RFC 1623 同时明确:该默认值不覆盖函数签名中的生命周期省略规则,二者协同工作(例如static foo: fn(&u32) -> &u32仍是for<'a> fn(&'a u32) -> &'a u32);显式写出生命周期仍然合法,因此该改动完全向后兼容。

const fn的正式落地(RFC 0911)

如前所述,text/0911-const-fn.md 将 RFC 0246 的"常量函数"设想落地。除语法规则外,RFC 0911 还规定了参数只允许简单的按值绑定(如x: T)、函数体按const块检查、trait 方法不能为const、const fn也可以是unsafe(用于构造需要维护不变量的类型,如非空指针)等细则,并明确"不引入一般性 CTFE 机制"。

rvalue 提升:常量进入静态内存(RFC 1414 / RFC 3027)

RFC 0246 设想"把常量放进只读内存"的优化路径,其运行时形态在 text/1414-rvalue_static_promotion.md 中体现为rvalue 提升(promotion):把 constexpr 右值提升为静态内存中的值,从而可以直接构造'static引用,例如let x: &'static u32 = &42。其约束是:被提升的 constexpr 不得包含UnsafeCell { ... }构造、不得调用返回含UnsafeCell类型的 const fn——这正是 RFC 0246"常量内部不得含UnsafeCell才能进只读内存"思想的直接延续。

而 text/3027-infallible-promotion.md(2020 年)则进一步收紧了该机制:将隐式提升限制在"求值不会失败"的表达式中,并澄清了术语——"让引用获得'static生命周期"称为lifetime extension,而"把代码片段抽取为常量"的底层机制称为promotion。后者还被用于非Copy数组重复表达式、#[rustc_args_required_const]与asm!的 const 操作数等场景。

常量求值上下文的精确定义(RFC 1229)

text/1229-compile-time-asserts.md 从编译器视角精确列举了恰好五处要求表达式为常量的上下文:const/static项的初始化器、数组大小[T; EXPR]、重复表达式长度[VAL; LEN_EXPR]、C 风格枚举判别值、以及模式。可以看到,这份清单中"const/static 初始化器"与"模式"正是 RFC 0246 所定义的核心使用场景,二者一脉相承。

现代static的典型形态:Sync与惰性初始化

RFC 0246 说"static的主要使用场景是全局锁、全局原子计数器",这一判断在标准库层面得到验证。在 text/2788-standard-lazy-types.md 中可以看到现代 Rust 对惰性初始化全局值的标准写法:

use std::sync::Lazy; // `BACKTRACE` 实现了 `Deref<Target = Option<String>>`,首次访问时初始化 static BACKTRACE: Lazy<Option<String>> = Lazy::new(|| { std::env::var("RUST_BACKTRACE").ok() });

该 RFC 明确指出:惰性值本质上是内部可变性(interior mutability)的一种形态,其安全性的关键前提是"单次赋值(single assignment)"——即OnceCell模式。这与 RFC 0246 对"static 类型内部出现UnsafeCell时必须置于可变内存、且依赖同步原语保证安全"的原始设计相互呼应。同时,text/0458-send-improvements.md 关于&T在T: Sync时才是Send的规则,也构成了"static 必须Sync才能被安全共享"这一约束的理论基础。

设计权衡与替代方案

RFC 0246 在 Drawbacks 中坦率承认:该设计为全局数据引入了两个关键字,而全局数据本身是边缘特性,看起来有些"用力过猛"。不过作者也指出,大多数 Rust 程序员只需要掌握const一个关键字——static变量会很少被用到。

备选方案:让static mut的Sync类型访问安全化

RFC 列举了当时正在考虑的另一套设计:保持当时"单一static"的现状,但当static mut的类型满足Sync时,将其访问视为安全。该方案即 RFC 177 讨论的内容(text/0246-const-vs-static.md 的 Alternatives 一节对此有完整描述)。与"两个关键字"方案相比,它不引入新关键字,但也没有解决"没有显著地址的常量值"这一核心语义问题。

混合策略:分阶段实施

考虑到时间窗口(Rust 1.0 排期紧张),RFC 还提出了一种混合路线:短期内维持当时的static规则(或采用"Sync的static mut访问安全"变体),待 Rust 1.0 发布后再引入const声明。从仓库后续文档(RFC 0911 等)看,const与const fn的完整落地确实是在 1.0 之后的演进中逐步完成的。

遗留的未决问题

RFC 0246 在 Unresolved questions 中留下了四个问题,它们在一定程度上预示了后续多年围绕全局声明体系的讨论方向:

  1. 编译器是否可以对"深度不可变"的static变量值进行内联(从而强制下游重新编译)?——这一问题与"内联带来的重编译成本"权衡直接相关,后来与 text/3027-infallible-promotion.md 讨论的隐式提升边界问题交织在一起。
  2. 是否允许类型不满足Sync的static,但将其访问标记为 unsafe?
  3. 是否允许类型不满足Sync、但初始化值实际上不含内部可变性的static(例如初始值为None的Option<UnsafeCell<uint>>,理论上安全)?
  4. 设想的扩展(泛型常量、常量函数)实现难度如何?如果容易实现则宜早做,困难则可延后。

其中第 3 个问题与"Sync是类型层面的静态约束、而内部可变性是否真实存在取决于值"之间的张力,在后来的struct派生与Sync自动实现讨论中反复出现;第 4 个问题则直接导向了 RFC 0911 的const fn落地。

实践指南:如何选择const还是static

综合 RFC 0246 及其后续演进,可以提炼出现代 Rust 中选择全局声明的决策要点:

  • 声明纯值、参与编译期计算、需要内联或进入模式匹配→ 使用const。常量无显著地址、被视为 rvalue、可被内联与复制,且是唯一能合法出现在模式中的全局项。
  • 声明真正的全局内存位置、生命周期为'static、需要被取地址共享→ 使用static。此时类型必须满足Sync;若类型内部不含UnsafeCell,编译器可能将其置于只读内存。
  • 需要全局的可变状态(如原子计数器、锁、惰性初始化容器)→ 在现代 Rust 中优先选择基于内部可变性的安全容器(AtomicUsize、Mutex、OnceCell/Lazy),而非static mut。static mut的所有访问都是 unsafe,且不能放入只读内存,应作为最后手段。
  • 需要'static引用但不想声明全局项→ 依赖 rvalue 提升机制,直接写&expr获取'static引用(受 text/1414-rvalue_static_promotion.md 与 text/3027-infallible-promotion.md 的规则约束)。

这套决策框架的每一条规则,都能在 RFC 0246 的原始文本及其后继 RFC 中找到依据——这正是该 RFC 作为 Rust 全局声明体系"原点性设计文档"的价值所在。

结语

RFC 0246(2014 年 8 月)用"值 vs 内存地址"这一清晰的概念划分,为 Rust 的全局声明体系奠定了语义地基:const承载可内联、无地址、可进只读内存的常量值,static承载'static生命周期、Sync约束下的全局内存位置,而static mut则被边缘化为全 unsafe 的可变全局。此后十余年间,RFC 1623 的'static默认生命周期、RFC 0911 的const fn、RFC 1445 与 RFC 3535 的常量模式规则、RFC 1414 与 RFC 3027 的 rvalue 提升,无不沿着这条主线演进。对于任何希望深入理解 Rust 全局声明语义、或需要做出"用 const 还是 static"决策的开发者,这份 RFC 及其在仓库中的后继文档都是最权威的一手资料。

  • 文档

【免费下载链接】rfcs

RFCs for changes to Rust

项目地址:https://gitcode.com/gh_mirrors/rf/rfcs
点击查看免费下载
上一篇:Tock 内核周会纪要解析(2022-06-10):VolatileCell 许可、Digest/HMAC HIL 缓冲区设计演进与 Ti50 调研
下一篇:探索Peeky可视化界面:如何利用UI提升测试体验与效率

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 18:11:30

10多种数据库汇聚的项目中,KFS如何处理异构DDL差异

在某大型医院的数据同步项目中&#xff0c;260条同步链路同时运行&#xff0c;源端涉及Oracle、MySQL、SQL Server、KES V8R6、Vastbase等10多种数据库&#xff0c;并持续向目标库汇聚数据。 在这样的环境里&#xff0c;把数据同步过去并不难。真正让链路“卡住”的&#xff0c…

作者头像 李华
网站建设 2026/10/20 21:01:03

Apollo-11飞行计算机拆解:嵌入式实时系统的优先级调度与优雅降级

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华