Rust - 延迟初始化(静态/非静态)变量

lazy_static

A macro for declaring lazily evaluated statics.

Using this macro, it is possible to have statics that require code to be executed at runtime in order to be initialized. This includes anything requiring heap allocations, like vectors or hash maps, as well as anything that requires function calls to be computed.Syntax

lazy_static! {
    /// This is an example for using doc comment attributes
    static ref EXAMPLE: u8 = 42;
}

Semantics

写一个 static ref NAME: TYPE = EXPR;, 该宏会生成一个类型是 Deref<TYPE> 且叫做 NAME 的字段以静态形式保存。

第一次deref, EXPR 才会开始计算其值并自动保存(原子安全), 接下来的所有deref都将返回同一个对象也就是第一次初始化的对象.

因为初始化的顺序不是按照上下来定义的,尽量不要在您的代码里面写出以下这种东西!

lazy_static! {
    static ref NUM_1: u32 = 222 + *NUM_2;
    static ref NUM: u32 = *NUM_1 + 222;
    static ref NUM_2: u32 = 222 + *NUM_1;
}

除了懒初始化以外, 生成的字段(变量)和普通的static字段没什么区别。但是有以下要求:

  • 保存的数据类型必须实现 Sync 特征

例如以下代码在lazy_static是不允许的:

struct M<T> {
    a: Rc<T>
}

lazy_static! {
    static ref NUM: M<u32> = M {
        a: Rc::new(11111)
    };
}
  • 如果保存的对象里面有析构函数,那么即使进程退出他也不会运行!(建议使用signal-hook去处理这个问题)

什么是Send?什么是Sync

  • Send: 可以在多线程中转移所有权

  • Sync: 可以在多线程中共享引用

没有实现Sync的类型

  1. Rc<T>: 单线程引用计数类型,不安全用于多线程上下文。

  2. Cell<T>: 提供内部可变性,但不保证线程安全。

  3. RefCell<T>: 提供运行时可变性检查,但不保证线程安全。

  4. UnsafeCell<T>: 任何类型的可变性容器,是其他不安全类型的基础。

  5. NonNull<T>: 包含一个原始指针,可能会违反线程安全性。

同类库once_cell

pub static ECDH_SHARE_KEY: Lazy<Vec<u8>> = Lazy::new(|| hex::decode("3F539B2549AB1F71421F2C3A66298D05").unwrap());

上面是一个once_cell使用的例子,可以看出这个库使用起来更加方便快捷,适用于需要单次初始化的各种场景,包括非静态变量。

once_cell提供了诸如OnceCell, Lazy<T> 这类,去实现延迟初始化。

OnceCell 已进入标准库

从 Rust 1.70 开始,once_cell crate 的 once_cell::sync::OnceCell 集成到标准库中,成为 std::sync::OnceLock。在 Rust 存在以来,这是第一次,你不需要编写不安全的代码,也不需要引入封装它的外部 crate,就能够创建在首次使用时初始化的全局/静态变量。用法基本与once_cell相同:

use std::sync::OnceLock;
use regex::Regex;

pub fn log_file_regex() -> &'static Regex {
    static LOG_FILE_REGEX: OnceLock<Regex> = OnceLock::new();
    LOG_FILE_REGEX.get_or_init(|| Regex::new(r#"^\d+-[[:xdigit:]]{8}$"#).unwrap())
}

// use log_file_regex().is_match(some_name) anywhere in your program

这个新增功能乍看起来可能并不像个大事件,因为once_cell 多年来一直提供了相同的功能。然而,将其加入到标准库中在几个方面极大地有益于该语言。首先,应用程序和库都广泛使用 initialize-on-first-use 的全局变量,现在两者都可以从它们的依赖项中淘汰像once_celllazy_static这样的 crate。其次,现在可以通过宏生成的代码创建全局变量,而不会出现笨拙的 once_cell 再导出和其他逻辑问题。第三,它使得教授这门语言变得更容易,教材不再需要决定是否涵盖once_celllazy_static,也不需要解释为什么一开始就需要外部 crate 来处理全局变量。这个煞费苦心的长篇 StackOverflow 回答就是一个深陷泥潭的好例子,我先前关于这个主题的博客文章也是如此。后者中的整个stdlib/unsafe部分现在已经变得过时,因为使用OnceLock可以在不损失性能的情况下安全地实现相同的效果。

然而,工作还没有完成。请注意静态变量如何被放置在包含对OnceLock::get_or_init()进行唯一调用的函数内部。这种模式确保对静态OnceLock的每次访问都通过一个位置,该地方还对其初始化。once_cell通过once_cell::sync::Lazy减少了这种冗长性,但等效的stdlib类型尚未稳定,卡在一些技术问题上。将全局变量放置在函数内的解决方法并不是一个重大障碍,但值得一提。当比较OnceLock的使用便捷性与lazy_static::lazy_static!once_cell::sync::Lazy相比时,这一点尤为重要,后两者都提供了在单一位置初始化而无需额外工作的便利性。