在Rust中声明和使用全局变量是很棘手的。一般来说,对于这种语言,Rust通过强迫我们非常明确来确保鲁棒性。
在这篇文章中,我将讨论Rust编译器想让我们避免的陷阱。然后,我将向你展示不同场景下的最佳解决方案。
概述
在Rust中实现全局状态有很多选择。如果你很着急,这里是我的建议的一个快速概述。
你可以通过以下链接跳转到本文的具体章节。
- 没有全局。重构为Arc / Rc
- 编译时初始化的球状物:const T / static T
- 使用外部库以方便运行时初始化球状体:lazy_static / once_cell
- 实现你自己的运行时初始化:std::sync::Once + static mut T
- 单线程运行时初始化的特殊情况:thread_local
天真的第一次尝试
让我们从一个例子开始,看看如何不使用全局变量。假设我想在一个全局字符串中存储程序的起始时间。后来,我想从多个线程访问这个值。
一个Rust的初学者可能会想用let
来声明一个全局变量,就像Rust中的其他变量一样。然后整个程序就会变成这样。
use chrono::Utc;
let START_TIME = Utc::now().to_string();
pub fn main() {
let thread_1 = std::thread::spawn(||{
println!("Started {}, called thread 1 {}", START_TIME.as_ref().unwrap(), Utc::now());
});
let thread_2 = std::thread::spawn(||{
println!("Started {}, called thread 2 {}", START_TIME.as_ref().unwrap(), Utc::now());
});
// Join threads and panic on error to show what went wrong
thread_1.join().unwrap();
thread_2.join().unwrap();
}
在操场上自己试试吧!
这对Rust来说是无效的语法。let
关键字不能在全局范围内使用。我们只能使用static
或const
。后者声明的是一个真正的常量,而不是一个变量。只有static
给我们一个全局变量。
这背后的原因是,let
在运行时在堆栈上分配了一个变量。请注意,当在堆上分配时,这仍然是真实的,如let t = Box::new();
。在生成的机器代码中,仍然有一个指向堆的指针,它被存储在堆上。
全局变量被存储在程序的数据段中。它们有一个固定的地址,在执行过程中不会改变。因此,代码段可以包括常数地址,并且完全不需要堆栈中的空间。
好了,我们可以理解为什么我们需要一个不同的语法。Rust作为一种现代系统编程语言,希望在内存管理方面非常明确。
让我们再试一下static
。
use chrono::Utc;
static START_TIME: String = Utc::now().to_string();
pub fn main() {
// ...
}
编译器并不满意,还没有。
error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
--> src/main.rs:3:24
|
3 | static start: String = Utc::now().to_string();
| ^^^^^^^^^^^^^^^^^^^^^^
嗯,所以静态变量的初始化值不能在运行时计算。那就干脆让它不被初始化吧?
use chrono::Utc;
static START_TIME;
pub fn main() {
// ...
}
这产生了一个新的错误。
Compiling playground v0.0.1 (/playground)
error: free static item without body
--> src/main.rs:21:1
|
3 | static START_TIME;
| ^^^^^^^^^^^^^^^^^-
| |
| help: provide a definition for the static: `= <expr>;`
所以这也行不通!在任何用户代码运行之前,所有的静态值都必须完全初始化并且有效。
如果你是从其他语言(比如JavaScript或Python)来到Rust的,这可能看起来是不必要的限制。但任何C++大师都可以告诉你关于静态初始化顺序惨败的故事,如果我们不小心,就会导致未定义初始化顺序。
例如,想象一下这样的情况。
static A: u32 = foo();
static B: u32 = foo();
static C: u32 = A + B;
fn foo() -> u32 {
C + 1
}
fn main() {
println!("A: {} B: {} C: {}", A, B, C);
}
在这个代码片断中,由于循环依赖,没有安全的初始化顺序。
如果是C++,它不关心安全问题,结果会是A: 1 B: 1 C: 2
。它在任何代码运行前都会进行零初始化,然后在每个编译单元内从上到下定义顺序。
至少它定义了结果是什么。然而,当静态变量来自不同的.cpp
文件,因此也是不同的编译单元时,"惨败 "就开始了。那么这个顺序就无法定义了,通常取决于编译命令行中文件的顺序。
在Rust中,零初始化是不存在的。毕竟,对于许多类型来说,零是一个无效的值,比如Box
。此外,在Rust中,我们不接受奇怪的排序问题。只要我们远离unsafe
,编译器应该只允许我们写出正常的代码。而这就是为什么编译器阻止我们使用直接的运行时初始化。
但我是否可以通过使用None
,即相当于空指针的方式来规避初始化呢?至少这都是符合Rust类型系统的。当然,我可以把初始化移到主函数的顶部,对吗?
static mut START_TIME: Option<String> = None;
pub fn main() {
START_TIME = Some(Utc::now().to_string());
// ...
}
啊,好吧,我们得到的错误是......
error[E0133]: use of mutable static is unsafe and requires unsafe function or block
--> src/main.rs:24:5
|
6 | START_TIME = Some(Utc::now().to_string());
| ^^^^^^^^^^ use of mutable static
|
= note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior
在这一点上,我可以把它包在一个unsafe{...}
块里,这样就可以了。有时,这也是一种有效的策略。也许是为了测试代码的其余部分是否按预期工作。但这并不是我想向你展示的成文法解决方案。因此,让我们来探讨一下被编译器保证安全的解决方案。
继续阅读《如何在Rust中直截了当地使用全局变量》,请点击SitePoint。