Rust惑点启示系列(七):使用全局变量和单例

发布时间:2024-10-16 13:28
最后更新:2024-10-16 14:37
所属分类:
Rust

全局变量和单例模式都是我们在其他各种语言中可以非常熟练使用的概念,但是在Rust里这两个概念在使用起来就不是那么顺利。这主要跟Rust对于内存的控制规则有关。本文将尝试讨论一下如何在Rust里实现这两种功能。

本文是《Rust惑点启示笔记》系列文章中的第七篇。这个系列的文章主要计划对Rust语言使用过程中经常会出现的一些容易迷惑的惑点进行一个启发式的讨论和记录。

本系列专题还有以下文章:

  1. Rust惑点启示系列(一):避免随意使用Clone
  2. Rust惑点启示系列(二):从函数中返回一些东西
  3. Rust惑点启示系列(三):引用的生命期从来就不够长
  4. Rust惑点启示系列(四):到处都是的大括号
  5. Rust惑点启示系列(五):工具类型太多了
  6. Rust惑点启示系列(六):如何下手编写一个函数
  7. Rust惑点启示系列(七):使用全局变量和单例
  8. Rust惑点启示系列(八):奇形怪状的Rust闭包

创建全局变量

全局变量这个事情,在其他的一些语言里是非常容易定义的,例如在Go中可以直接在一个包里定义变量,在一切都必须包装在类的Java里,可以在类里定义静态成员,就不用说更加自由的C和C++了。

Rust中虽然也可以直接在模块里定义变量,但是却会受到比较大的限制。尤其是全局变量中往往会用来保存数据库连接、全局资源等内容,这些内容有的不是在程序开始就可以完成初始化的,有的是在程序运行过程中需要修改的。

定义在模块里的变量都是程序全局生命期的,而且明确是使用static关键字声明的。所以,根据Rust中不允许出现未初始化变量的规则,这个全局变量在声明的时候就必须完成初始化。

如果这个全局变量携带的是固定值,那么还好办一些,直接声明就可以了。例如:

1
static GLOBAL_PARAM: i32 = 42;

全局变量的声明除了要求使用static关键字以外,还要求必须显式书写其类型,这一点不要忘记。像上例中这样声明的全局变量是不可变的,如果想要声明可变全局变量,也可以使用static mut来声明,但是这个全局变量在使用的时候就必须放置在unsafe {}块里,因为静态全局可变变量的并发安全性是需要手动保证的。

出于这个限制,在需要可变的全局变量的时候,我们往往会选择使用其他的解决方案。

根据这个全局变量在程序中的使用,是可以选择std::cell或者std::sync里的结构体来实现的。它们的区别是,如果程序是同步的,那么可以直接使用std::cell里的OnceCell或者LazyCell来声明全局变量,如果程序是异步的或者需要在多线程共享全局变量,那么就需要使用std::sync里的OnceLock或者LazyLock来声明全局变量。

  • std::cell::OnceCellstd::sync::OnceLock声明的都是只读变量,它们必须在声明的时候就立刻完成初始化。
  • std::cell::LazyCellstd::sync::LazyLock声明的也是只读变量,但不同的是它们会在首次访问的时候完成初始化。

这两种形式的全局变量类型的主要区别在于,OnceCellOnceLock可以对初始化的过程进行显式的更加精细的控制。如果只是打算简单的使用全局变量,可以优先选择LazyLock。例如:

1
2
3
static GLOBAL_DATA: LazyLock<String> = LazyLock::new(|| {
  "Some resource".to_string()
});

这样完成定义的全局变量可以通过*GLOBAL_DATA的形式使用。

LazyLock实现了Deref特征,所以可以直接使用解引用操作符*访问。

只是定义只读的全局变量是不够的,定义一个可写的全局变量才是我们最终的目标。要完成这个目标,我们还需要至少一个套娃。

接下来依旧使用std::sync模块里提供的结构体来完成这项任务,毕竟同步的程序已经不多了,现在更多的是多线程程序和异步程序。在std::sync模块里,能够支持写入的结构体一般是MutexRwLock。所以实际上我们是可以使用Mutex或者RwLock来代替LazyLock作为全局变量的包装的。

既然可以支持全局变量的写入,那么最好这个全局变量可以在程序运行过程中再完成其内部内容的初始化。要支持这个能力,可以借用Option。比如现在,这个全局变量的声明就变成了static GLOBAL_DATA: RwLock<Option<String>>。看到这个类型就知道这个全局变量在声明的是时候应该初始化成什么值了吧,的确就是RwLock::new(None)。之后可以在获取写入锁以后,将其中的值替换成所需要的内容。

实现单例模式非常简单

其实我们都过高的高估了单例模式,仔细观察一下上一节的示例。全局变量实际上就是一个单例。所以最简单的单例模式的实现就是用全局变量来实现。

不过真正的单例模式还是需要像C++和Java那样定义一个类来实现。在Rust里,可以借助LazyLockMutex或者RwLock来实现。这里给出一个基于RwLock实现的示例,毕竟使用RwLock的性能会好一些。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Singleton {
  value: String,
}

impl Singleton {
  fn instance() -> &'static RwLock<Self> {
    static INSTANCE: OnceLock<RwLock<Self>> = OnceLock::new();

    INSTANCE.get_or_init(|| {
      RwLock::new(Self {
        value: "Singleton".to_string(),
      })
    })
  }

  fn get_value(&self) -> &str {
    &self.value
  }

  fn set_value(&mut self, new_value: &str) {
    self.value = new_value.to_string();
  }
}

或者还可以使用LazyLock完成一个更简单的单例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Singleton {
  value: String,
}

static INSTANCE: LazyLock<RwLock<Singleton>> = LazyLock::new(|| {
  RwLock::new(Singleton {
    value: "Singleton".to_string(),
  })
})

impl Singleton {
  fn instance() -> &'static RwLock<Self> {
    &INSTANCE
  }

  fn get_value(&self) -> &str {
    &self.value
  }

  fn set_value(&mut self, new_value: &str) {
    self.value = new_value.to_string();
  }
}

如果对直接使用RwLock实例不放心,可以在RwLock外面再包裹一层Arc


索引标签
Rust
全局变量
单例