最简单的办法
安装使用受到广泛应用的 lazy_static 包:
lazy_static! {
static ref HASHMAP: HashMap<u32, &'static str> = {
let mut m = HashMap::new();
m.insert(0, "foo");
m.insert(1, "bar");
m.insert(2, "baz");
m
};
}
fn main() {
println!("{}", HASHMAP.get(&0));
}
基于标准库的方法
如果你不想引入额外的包来实现全局对象,那么不妨看看更加灵活的标准库方法。
首先,在 Rust 中真正的可以被全局访问的值修饰符只有 const
和 static
:
const N: i32 = 114514;
static M: i32 = 1919810;
fn main() {
println!("{} {}", N, M);
}
他们的工作原理分别如下:
const
值相当于把它内联到所有它出现的地方,你可以把它看成 C/CPP 中的宏值定义。它只在编译时被计算和考虑,目的是为了替代一些经常被使用的值来简化代码编写。
const N: i32 = 114514;
fn main() {
println!("{} {}", N, N);
// 以上与实际上会变成以下
println!("{} {}", 114514, 114514);
}
static
值不会内联,在程序启动的时候会固定在一个地址上,只有一个副本,所有使用static
值的地方都会指向和访问这个地址。
因此只有 static
值才能满足我们真正所需要的全局性。
对于 static
值,它的所有权属于整个程序,且生命周期也是整个程序。
static
的限制
无法修改
因为 static
值可以在任何地方被使用,为了保证读写内存安全,Rust 不允许对 static
值进行修改。
常量限制
进一步的,因为 static
在程序开始时被初始化(甚至在 main
函数之前),因此它的初始化值必须是 常量 或者由 const fn
(常函数)计算出来的常量以保证无副作用且安全。
这实际上限制了我们 static
值的类型,我们没办法使用自定义类型和大部分的标准库容器初始化。
解决修改的问题
我们现在需要复习一些 Rust 的小知识,来让我们找到解决 static
修改的方法。
Rc
和 Arc
在一些情况下,一个值可能拥有多个 Owner,那么这时候我们怎么追踪所有 Owner 的生命信息,使得能够在所有的 Owner 结束生命后自动释放值的内存呢?
其实这个问题在 Rust 之前早就被解答了,那就是使用 引用计数。
每次需要有新的 Owner 时自动 +1 计数,一个 Owner 结束生命后自动 -1 计数,计数为 0 时自动释放内存。
这也是 Rc
(Reference Counter)名字的由来。
至于 Arc
,即 Atomic Reference Counter(原子引用计数),是一种针对多线程环境的引用计数,保证计数在多线程下的正确。
有了 Rc
和 Arc
依然不能实现修改,因为如果不做限制,多个 Owner 他们各自又将值可以进行可变/不可变借用,这就会发生 读写竞争 和 写入竞争,导致内存不安全行为。因此 Rust 限制了 Rc
和 Arc
只能进行不可变借用。
看起来这俩东西他们的行为是不是跟 static
很像?
他们都不允许修改,且 static
的全局所有权可以视作它可能拥有任意数量的 Owner。
RefCell
和 Mutex
为了能够修改 Rc
和 Arc
内部值,Rust 又提供了 RefCell
和 Mutex
搭配使用。
RefCell
和 Mutex
实际上提供了一种 内部可变性,让我们能够绕过编译器的限制,手动处理可变与不可变借用。
在 Rust 中为了内存安全,对于借用有着如下的限制:
- 不允许同时出现可变借用和不可变借用,这样会产生读写竞争
- 允许同时出现多个不可变借用,相当于有多个读者的情况
- 不允许同时出现多个可变借用,这样会产生写入竞争
RefCell
提供了手动管理这种借用关系的机会,通过 borrow
和 borrow_mut
手动进行不可变借用和可变借用,而编译器不会对这种借用手段进行检查。当然如果你在实际使用中违反了如上限制,那么程序会直接 panic。
通过组合 Rc<RefCell<T>>
和 Arc<RefCell<T>>
,我们就可以在保证值有多个 Owner 的情况下,实现对内部数据 T
的可变或不可变借用,实际上就是给了我们修改的能力,在这过程中 RefCell
本身是不变的,因此也满足了 Rc
和 Arc
的要求。
值得注意的是,RefCell
不是线程安全的,因此如果你要在多线程环境下使用内部可变性,你只能使用 Mutex
了。
Mutex
通过锁机制实现内部数据的修改。当需要获取可变或不可变借用时我们需要给数值用 lock
上锁,这样其他 Owner 希望在操作的时候就必须等待,直到解锁为止。
由于一次性只会有一个 Owner 获取到数据的访问权,因此天然就可以满足借用的 3 条限制,且 Mutex
本身不变,也可以满足 Rc
和 Arc
的要求。
由于我们的 static
值全局可见,因此先天性就要求可以在多线程环境下安全使用,因此 RefCell
在这里完全不可用,你只有 Mutex
这一种选择。
修改的解决方案
由上所述,我们就可以使用如下手段使得 static
值可以进行修改了:
static N: Mutex<i32> = Mutex::new(114514);
fn main() {
let mut lock = N.lock().unwrap();
*lock += 1;
println!("{}", lock);
}
这里我们发现使用了 Mutex::new
函数来生成我们的 static
值,因为 Mutex::new
是一个常函数,且 114514
是一个常量,因此我们仍然遵守了 static
的约束。
解决类型限制
如果我们想要初始化一个数组作为全局量会发生什么?
static N: Mutex<Vec<i32>> = Mutex::new(vec![1, 2, 3]);
实际上经过宏展开,以上代码会变成:
static N: Mutex<Vec<i32>> = Mutex::new({
let mut x = Vec::new();
x.push(1);
x.push(2);
x.push(3);
x
});
但是我们会发现编译报错了,因为以上赋值代码 push
不是常函数,即使 Mutex::new
是常函数。这限制了我们的全局变量类型,使得我们无法使用一些不能进行常量初始化的类型。
这时我们就需要用到 LazyCell
/LazyLock
/OnceCell
/OnceLock
家族了。
它们实际上是 2 种概念的组合:
对于 Lazy
系列,所有的值会在第一次获取的时候初始化,而不是在定义时进行,即为 懒赋值。
对于 Once
系列,它们在拥有 Lazy
系列性质的同时,又保证只会进行一次初始化,之后内部的值不再发生改变,即为 仅一次写入。
对于 Cell
和 Lock
系列,它们一个代表线程不安全版本,另一个则是线程安全版本。
因为以上家族的 new
函数都是常函数,所以我们可以通过这种延迟初始化的方式实现任意类型的全局初始化。
通常来说,我们会在 Mutex
外面套上以上家族,又因为 Mutex
实际上在初始化之后就不会再改变,且 static
要求我们线程安全,最终我们就只剩下了一种组合:
static N: OnceLock<Mutex<Vec<i32>>> = OnceLock::new();
fn main() {
let mut x = N.get_or_init(|| Mutex::new(vec![1, 2, 3])).lock().unwrap();
x.push(4);
println!("{:?}", x);
}
可以看到,我们每次获取值的时候都要带上一串初始化代码,非常的难看,因此就有了一种基于函数作用域的,更加优雅的全局变量实现方法:
fn global_vec() -> &'static Mutex<Vec<i32>> {
static INSTANCE: OnceLock<Mutex<Vec<i32>>> = OnceLock::new();
INSTANCE.get_or_init(|| Mutex::new(vec![1, 2, 3]))
}
fn main() {
let mut x = global_vec().lock().unwrap();
x.push(4);
println!("{:?}", x);
}
以上的形式也是 lazy_static
中已经写出来的推荐的使用标准库实现的方式。
整点花活
在有了基于标准库的全局变量实现方式后,实际上我们可以活用这种机制来整点花活了。
单例模式
我们仔细观察上述的 global_vec
函数,这玩意不就是现成的单例模式实现吗?
struct Pool {
names: Vec<String>,
addresses: Vec<String>,
}
impl Pool {
pub fn get_instance() -> &'static Mutex<Self> {
static INSTANCE: OnceLock<Mutex<Pool>> = OnceLock::new();
INSTANCE.get_or_init(|| Mutex::new(Pool::new()))
}
fn new() -> Self {
Self {
names: vec!["a".into(), "b".into()],
adresses: Vec::new(),
}
}
}
单例的部分借用
有些情况你并不希望获得整个全局变量的所有成员的借用,你只需要部分借用,那么你可以把 Mutex
给转移到 struct
内部:
struct Pool {
names: Mutex<Vec<String>>,
addresses: Mutex<Vec<String>>,
}
impl Pool {
pub fn get_instance() -> &'static Self {
static INSTANCE: OnceLock<Pool> = OnceLock::new();
INSTANCE.get_or_init(|| Pool::new())
}
fn new() -> Self {
Self {
names: Mutex::new(vec!["a".into(), "b".into()]),
addresses: Mutex::new(Vec::new()),
}
}
}
我们在使用的时候只需要 get_instance
后再对对应的成员进行上锁即可,这样一来如果有别的线程希望给别的成员上锁,就不会发生等待。
实际上你可以用这种结构实现全局变量集合单元,来避免对每个全局变量都实现一个函数:
struct Global {
name: Mutex<String>,
age: Mutex<i32>,
is_male: Mutex<bool>,
}
impl Global {
pub fn get() -> &'static Self {
static INSTANCE: OnceLock<Global> = OnceLock::new();
INSTANCE.get_or_init(|| Global::new())
}
fn new() -> Self {
Self {
name: Mutex::new("Tom".into()),
age: Mutex::new(24),
is_male: Mutex::new(true)
}
}
}
fn main() {
let name = Global::get().name.lock().unwrap();
println!("{}", name);
}
小结
我们在标准库下实现了全局对象,当然这离 lazy_static
所展示出来的易用性还不在一个等级上,它使用了很多宏生成技术还有一些 unsafe 代码,使得最终我们只像使用普通变量一样使用全局变量就可以了。
某种程度上 lazy_static
作为在没有标准库实现的情况下的工作已经结束了。