如何在Rust中巧妙地使用全局变量

4,255 阅读5分钟

How to Idiomatically Use Global Variables in Rust

在Rust中声明和使用全局变量是很棘手的。一般来说,对于这种语言,Rust通过强迫我们非常明确来确保鲁棒性。

在这篇文章中,我将讨论Rust编译器想让我们避免的陷阱。然后,我将向你展示不同场景下的最佳解决方案。

概述

在Rust中实现全局状态有很多选择。如果你很着急,这里是我的建议的一个快速概述。

A Flowchart for finding the best solution for global variables

你可以通过以下链接跳转到本文的具体章节。

天真的第一次尝试

让我们从一个例子开始,看看如何不使用全局变量。假设我想在一个全局字符串中存储程序的起始时间。后来,我想从多个线程访问这个值。

一个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 关键字不能在全局范围内使用。我们只能使用staticconst 。后者声明的是一个真正的常量,而不是一个变量。只有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