最近写 Rust 程序需要用到全局状态,我希望有一个状态在公共的作用域里,其他模块能通过引入等方式去访问他,在 JS 中做起来很简单:
const globalState = {
name: 'abc'
}
function foo() {
console.log(globalState)
}
function bar() {
globalState.name = '123'
}
由于 JS 的限制较少,变量声明的位置没有限制,在代码顶部声明后,后续所有作用域都能使用。
在 Rust 中就不能直接写成 JS 这样,由于 Rust 的所有权和生命周期限制,这种写法是不可能出现的。
但 Rust 是多范式的编程语言,要实现这种“全局状态”的功能,也会有好几种替代的方案:
static
既然要管控生命周期,那就把生命周期拉长。
将变量修饰成 static
后,这个变量的生命周期就变成了程序运行期间,这就意味着任何地方都能访问他,因为这是最长的生命周期了:
static STATE: u32 = 1;
但 static
存在一些限制,其一是当他配合 mut
修饰的时候,需要使用 unsafe
标记才能修改变量的值,并且还有隐含的状态共享、并发修改问题。
其二是他的值只能是在编译期能确定的(常量,常量表达式,常量函数,还有实例化的语句),比如说不能将函数的值赋给 static
变量。
题外话,如果要求 safe,可以配合 LazyLock 使用:
pub fn get_val() -> i32 {
1
}
static A: LazyLock<i32> = LazyLock::new(get_val);
LazyLock 本身是可以在编译期确定的,get_val
的结果不需要那么早确定,在运行时,第一次使用时才会对 get_val
求值,所以这种写法是合理,且带有 lazy 特性的。
thread_local!
当我想到 static
解法的时候,我第一反应是 static
+ RefCell
直接绕过检查去修改指针的值,但这也是不行的,因为 RefCell
和 Cell
都没有实现 Sync
trait,所以在多线程场景下不安全,作为 static
变量必须得考虑多线程的情况。
但如果确定这个变量只在单线程内有效,可以使用 thread_local
宏:
thread_local! {
pub static FOO: u32 = 1;
}
pub fn main() {
FOO.with(|v| println!("{v}"));
}
这样限制了使用场景只在单线程,是可以配合 RefCell
使用的,因为不需要考虑并发修改的情况。
单例模式
这就像回归了 Java 等 OOP 语言的编码思路,在 Java 中就会做一些单例的状态管理类,在任何地方都能初始化,能同时访问、管理内部状态。
但我查了一圈,发现 Rust 中没有“标准”的单例模式写法,又或者说写法五花八门,最接近 OOP 写法的应该是这样的:
struct Singleton {
data: String;
}
impl Singleton {
fn get_instance() -> Arc<Mutex<Singleton>> {
static mut INSTANCE: Option<Arc<Mutex<Singleton>>> = None;
unsafe {
INSTANCE.get_or_insert(|| {
Arc::new(Mutex::new(Singleton {
data: String::from("abc");
}))
}).clone()
}
}
}
后话
在看完 actix_web 和 tauri 的文档后,我发现他们都有一个相似的解决“全局状态”的办法:
App::new()
.app_data(state.clone())
.run()
在创建服务的时候,他们会提供一个“设置状态”的 API,在 tauri 中是 manage
,通过这个 API 设置的状态会被保存到 app 中,在后续定义 service/command 时可以直接通过 app 实例访问。