#lazy-evaluation #static #content

no-std once_cell_serde

单次赋值单元和懒值

1 个稳定版本

1.17.1 2023 年 3 月 14 日

2491Rust 模式

Download history 2528/week @ 2024-03-14 2167/week @ 2024-03-21 1025/week @ 2024-03-28 2041/week @ 2024-04-04 1731/week @ 2024-04-11 2839/week @ 2024-04-18 2512/week @ 2024-04-25 2524/week @ 2024-05-02 3775/week @ 2024-05-09 2408/week @ 2024-05-16 2742/week @ 2024-05-23 1217/week @ 2024-05-30 2613/week @ 2024-06-06 2062/week @ 2024-06-13 577/week @ 2024-06-20 600/week @ 2024-06-27

5,943 每月下载量
用于 kodept-ast

MIT/Apache

89KB
1K SLoC

该项目是 https://github.com/matklad/once_cell,在 https://github.com/matklad/once_cell/pull/104 中提出了对 serde 的支持。


lib.rs:

概述

once_cell 提供了两种新的类似单元类型,unsync::OnceCellsync::OnceCell。一个 OnceCell 可能存储任意非 Copy 类型,最多只能赋值一次,并提供对存储内容的直接访问。核心 API 看起来大致如下(还有很多其他功能,请继续阅读!)

impl<T> OnceCell<T> {
    const fn new() -> OnceCell<T> { ... }
    fn set(&self, value: T) -> Result<(), T> { ... }
    fn get(&self) -> Option<&T> { ... }
}

注意,与 RefCellMutex 类似,set 方法只需要一个共享引用。由于单次赋值限制,get 可以返回一个 &T 而不是 Ref<T>MutexGuard<T>

sync 风格是线程安全的(即实现了 Sync 特性),而 unsync 风格则不是。

食谱

OnceCell 可能适用于各种模式。

全局数据的安全初始化

use std::{env, io};

use once_cell_serde::sync::OnceCell;

#[derive(Debug)]
pub struct Logger {
    // ...
}
static INSTANCE: OnceCell<Logger> = OnceCell::new();

impl Logger {
    pub fn global() -> &'static Logger {
        INSTANCE.get().expect("logger is not initialized")
    }

    fn from_cli(args: env::Args) -> Result<Logger, std::io::Error> {
       // ...
    }
}

fn main() {
    let logger = Logger::from_cli(env::args()).unwrap();
    INSTANCE.set(logger).unwrap();
    // use `Logger::global()` from now on
}

延迟初始化的全局数据

这本质上与 lazy_static! 宏相同,但没有宏。

use std::{sync::Mutex, collections::HashMap};

use once_cell_serde::sync::OnceCell;

fn global_data() -> &'static Mutex<HashMap<i32, String>> {
    static INSTANCE: OnceCell<Mutex<HashMap<i32, String>>> = OnceCell::new();
    INSTANCE.get_or_init(|| {
        let mut m = HashMap::new();
        m.insert(13, "Spica".to_string());
        m.insert(74, "Hoyten".to_string());
        Mutex::new(m)
    })
}

此外,还有简化该模式的便捷类型 sync::Lazyunsync::Lazy

use std::{sync::Mutex, collections::HashMap};
use once_cell_serde::sync::Lazy;

static GLOBAL_DATA: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| {
    let mut m = HashMap::new();
    m.insert(13, "Spica".to_string());
    m.insert(74, "Hoyten".to_string());
    Mutex::new(m)
});

fn main() {
    println!("{:?}", GLOBAL_DATA.lock().unwrap());
}

请注意,持有 Lazy 的变量声明为 static而不是 const。这很重要:使用 const 会导致编译错误。

通用惰性求值

lazy_static! 不同,Lazy 可以与局部变量一起使用。

use once_cell_serde::unsync::Lazy;

fn main() {
    let ctx = vec![1, 2, 3];
    let thunk = Lazy::new(|| {
        ctx.iter().sum::<i32>()
    });
    assert_eq!(*thunk, 6);
}

如果您需要在结构体中有一个惰性字段,您可能应该直接使用 OnceCell,因为这将允许您在初始化期间访问 self

use std::{fs, path::PathBuf};

use once_cell_serde::unsync::OnceCell;

struct Ctx {
    config_path: PathBuf,
    config: OnceCell<String>,
}

impl Ctx {
    pub fn get_config(&self) -> Result<&str, std::io::Error> {
        let cfg = self.config.get_or_try_init(|| {
            fs::read_to_string(&self.config_path)
        })?;
        Ok(cfg.as_str())
    }
}

惰性编译的正则表达式

这是一个 regex! 宏,它接受一个字符串字面量并返回一个 表达式,该表达式求值为一个 &'static Regex

macro_rules! regex {
    ($re:literal $(,)?) => {{
        static RE: once_cell_serde::sync::OnceCell<regex::Regex> = once_cell_serde::sync::OnceCell::new();
        RE.get_or_init(|| regex::Regex::new($re).unwrap())
    }};
}

此宏可以避免每次循环迭代都编译正则表达式的问题。

运行时 include_bytes!

include_bytes 宏用于包含测试资源,但它会大大减慢测试编译速度。一种替代方法是运行时加载资源

use std::path::Path;

use once_cell_serde::sync::OnceCell;

pub struct TestResource {
    path: &'static str,
    cell: OnceCell<Vec<u8>>,
}

impl TestResource {
    pub const fn new(path: &'static str) -> TestResource {
        TestResource { path, cell: OnceCell::new() }
    }
    pub fn bytes(&self) -> &[u8] {
        self.cell.get_or_init(|| {
            let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
            let path = Path::new(dir.as_str()).join(self.path);
            std::fs::read(&path).unwrap_or_else(|_err| {
                panic!("failed to load test resource: {}", path.display())
            })
        }).as_slice()
    }
}

static TEST_IMAGE: TestResource = TestResource::new("test_data/lena.png");

#[test]
fn test_sobel_filter() {
    let rgb: &[u8] = TEST_IMAGE.bytes();
    // ...
}

lateinit

LateInit 类型用于延迟初始化。它类似于 Kotlin 的 lateinit 关键字,并允许构建循环数据结构

use once_cell_serde::sync::OnceCell;

pub struct LateInit<T> { cell: OnceCell<T> }

impl<T> LateInit<T> {
    pub fn init(&self, value: T) {
        assert!(self.cell.set(value).is_ok())
    }
}

impl<T> Default for LateInit<T> {
    fn default() -> Self { LateInit { cell: OnceCell::default() } }
}

impl<T> std::ops::Deref for LateInit<T> {
    type Target = T;
    fn deref(&self) -> &T {
        self.cell.get().unwrap()
    }
}

#[derive(Default)]
struct A<'a> {
    b: LateInit<&'a B<'a>>,
}

#[derive(Default)]
struct B<'a> {
    a: LateInit<&'a A<'a>>
}


fn build_cycle() {
    let a = A::default();
    let b = B::default();
    a.b.init(&b);
    b.a.init(&a);
    
    let _a = &a.b.a.b.a;
}

与 std 的比较

!Sync 类型 访问模式 缺点
Cell<T> T 对于 get 需要 T: Copy
RefCell<T> RefMut<T> / Ref<T> 可能在运行时引发恐慌
unsync::OnceCell<T> &T 只能分配一次
Sync 类型 访问模式 缺点
AtomicT T 仅与某些 Copy 类型一起工作
Mutex<T> MutexGuard<T> 可能在运行时发生死锁,可能阻塞线程
sync::OnceCell<T> &T 只能分配一次,可能阻塞线程

技术上,调用 get_or_init 也会导致恐慌或死锁,如果它递归调用自己。然而,由于赋值只能发生一次,此类情况应比使用 RefCellMutex 的等效情况更少。

最低支持的 rustc 版本

此包最低支持的 rustc 版本是 1.56.0

如果仅启用 std 功能,MSRV 将保守更新,支持至少编译器的最新 8 个版本。当使用其他功能,如 parking_lot 时,MSRV 可能会更新得更频繁,直至最新稳定版本。在两种情况下,增加 MSRV 都不被视为 semver 破坏性更改。

实现细节

实现基于 lazy_staticlazy_cell 库以及 std::sync::Once。在某种程度上,once_cell 只是简化并统一了这些 API。

为了实现 OnceCell 的同步版本,此库使用自定义的 std::sync::Onceparking_lot::Mutex 重实现。这由 parking_lot 功能控制(默认禁用)。两种情况下的性能相同,但基于 parking_lotOnceCell<T> 更小,最多可达 16 字节。

此库使用 unsafe

常见问题解答。

我应该使用 lazy_static 还是 once_cell?

从第一近似来看,once_cell 既有更多的灵活性,又更方便,应该优先选择。

once_cell 不同,lazy_static 支持基于自旋锁的阻塞实现,这与 #![no_std] 一起工作。

lazy_static 已经接受了大量的实际测试,但 once_cell 也是一个广泛使用的库。

我应该使用同步还是非同步版本?

因为 Rust 编译器会为你检查线程安全,所以不可能意外地在使用 sync 时使用 unsync。因此,在单线程代码中使用 unsync,在多线程代码中使用 sync。如果代码后来变为多线程,很容易在这两种之间切换。

目前,unsync 有一个额外的优点,即重入初始化会导致 panic,这比死锁更容易调试。

这个库支持异步操作吗?

不支持,但你可以使用 async_once_cell

我能自带互斥锁吗?

generic_once_cell 允许这样做。

相关库

此库的大部分功能在 nightly Rust 中的 std 中可用。请参阅 跟踪问题

依赖项

~0–5MB