在Rust中是否可以使用全局变量?

211

我知道一般来说应该避免使用全局变量。然而,在实际情况下,如果变量对于程序至关重要,有时使用它们是可取的。

为了学习Rust,我正在使用sqlite3和GitHub上的Rust/sqlite3软件包编写一个数据库测试程序。因此,这就需要(在我的测试程序中)将数据库变量在大约十几个函数之间传递(作为全局变量的替代方案)。以下是示例。

  1. Rust中是否可以、是否可行、是否可取使用全局变量?

  2. 根据下面的示例,我能否声明并使用全局变量?

extern crate sqlite;

fn main() {
    let db: sqlite::Connection = open_database();

    if !insert_data(&db, insert_max) {
        return;
    }
}

我尝试了以下方法,但看起来并不完全正确,并导致了下面的错误(我还尝试了带有 unsafe 块的方法):

extern crate sqlite;

static mut DB: Option<sqlite::Connection> = None;

fn main() {
    DB = sqlite::open("test.db").expect("Error opening test.db");
    println!("Database Opened OK");

    create_table();
    println!("Completed");
}

// Create Table
fn create_table() {
    let sql = "CREATE TABLE IF NOT EXISTS TEMP2 (ikey INTEGER PRIMARY KEY NOT NULL)";
    match DB.exec(sql) {
        Ok(_) => println!("Table created"),
        Err(err) => println!("Exec of Sql failed : {}\nSql={}", err, sql),
    }
}

编译产生的错误:

error[E0308]: mismatched types
 --> src/main.rs:6:10
  |
6 |     DB = sqlite::open("test.db").expect("Error opening test.db");
  |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected enum `std::option::Option`, found struct `sqlite::Connection`
  |
  = note: expected type `std::option::Option<sqlite::Connection>`
             found type `sqlite::Connection`

error: no method named `exec` found for type `std::option::Option<sqlite::Connection>` in the current scope
  --> src/main.rs:16:14
   |
16 |     match DB.exec(sql) {
   |              ^^^^

7
为了得到一个安全的解决方案,请参考如何创建一个全局可变的单例? - Shepmaster
1
我应该在这里指出,OP遇到的错误与尝试将Connection存储在Option<Connection>类型中以及尝试将Option<Connection>用作Connection有关。如果通过使用Some()解决了这些错误并且像他们最初尝试的那样使用了一个unsafe块,则他们的代码将工作(虽然是线程不安全的方式)。 - TheHans255
这个回答解决了你的问题吗?如何创建一个全局可变的单例? - vaporizationator
8个回答

115

可以实现,但是不允许直接进行堆分配。堆分配是在运行时执行的。以下是一些示例:

static SOME_INT: i32 = 5;
static SOME_STR: &'static str = "A static string";
static SOME_STRUCT: MyStruct = MyStruct {
    number: 10,
    string: "Some string",
};
static mut db: Option<sqlite::Connection> = None;

fn main() {
    println!("{}", SOME_INT);
    println!("{}", SOME_STR);
    println!("{}", SOME_STRUCT.number);
    println!("{}", SOME_STRUCT.string);

    unsafe {
        db = Some(open_database());
    }
}

struct MyStruct {
    number: i32,
    string: &'static str,
}

25
使用 static mut 选项,是否意味着每个使用连接的代码都必须标记为不安全? - Kamek
1
@Kamek 初始访问必须是不安全的。我通常使用一个宏的薄包装来掩盖它。 - jhpratt
8
我认为在宏中使用 unsafe 有点违背了借用检查器的初衷。如果你从两个不同的位置可变地访问 db ,那么程序可能会崩溃,最好保留 unsafe,这样你可以明确地表示“如果我不自己进行借用检查,我可能会导致程序崩溃”。除非你的宏的名称中包含了 "unsafe",否则请勿如此操作。 - Nicholas Pipitone
10
你说得对,@NicholasPipitone。我甚至不确定我两年前发的那条评论具体指的是什么。 - jhpratt

84

只要将静态变量设置为线程本地的,就可以很容易地使用它们。

缺点是对象不会对程序中可能生成的其他线程可见。好处是,与真正的全局状态不同,它完全安全且不难使用。任何语言中的真正全局状态都非常麻烦。这里是一个例子:

extern mod sqlite;

use std::cell::RefCell;

thread_local!(static ODB: RefCell<sqlite::database::Database> = RefCell::new(sqlite::open("test.db"));

fn main() {
    ODB.with(|odb_cell| {
        let odb = odb_cell.borrow_mut();
        // code that uses odb goes here
    });
}

这里我们创建了一个线程本地的静态变量,然后在一个函数中使用它。请注意,它是静态且不可变的;这意味着它所在的地址是不可变的,但由于有了RefCell,值本身将是可变的。

与常规的static不同,在thread-local!(static ...)中,您几乎可以创建任意对象,其中包括那些需要堆分配进行初始化的对象,例如VecHashMap等。

如果您不能立即初始化该值,例如它取决于用户输入,则还可能需要在其中加入Option,在这种情况下,访问它会有点麻烦:

extern mod sqlite;

use std::cell::RefCell;

thread_local!(static ODB: RefCell<Option<sqlite::database::Database>> = RefCell::New(None));

fn main() {
    ODB.with(|odb_cell| {
        // assumes the value has already been initialized, panics otherwise
        let odb = odb_cell.borrow_mut().as_mut().unwrap();
        // code that uses odb goes here
    });
}

1
你必须使用线程安全编译sqlite,这样它才会使用系统(内核)级互斥锁。请参见此处的警告 https://www.sqlite.org/faq.html#q6 sqlite软件包最终在此处编译sqlite本身 https://github.com/stainless-steel/sqlite3-src/blob/master/build.rs 如何确保它是“完全安全且不难使用”的?最好让一个线程处理sqlite连接,其他线程向该线程发送消息,而不是要求sqlite API多次打开同一个数据库文件。 - Flip

39

请查看 Rust 书籍中的 conststatic 章节。

您可以使用以下内容:

const N: i32 = 5;
或者
static N: i32 = 5;

在全局空间中。

但是它们不可更改。如果需要可变性,可以使用类似以下的内容:

static mut N: i32 = 5;

然后像这样引用它们:

unsafe {
    N += 1;

    println!("N: {}", N);
}

5
请解释一下 const Var: Tystatic Var: Ty 之间的区别。const Var: Ty 表示定义了一个常量,其值在编译时确定,不能被修改。这个变量只在当前作用域中有效。static Var: Ty 表示定义了一个静态变量,其生命周期为整个程序运行期间,可以被多次访问和修改。该变量默认被初始化为0或空值,也可以手动指定初始值。 - Nawaz
1
@Nawaz const 使全局变量成为不可变,而 static 则使其可变。请注意,对 static 变量的赋值是不安全的。 - sb27
2
@sb27 实际上,const 只是一个值,编译器可以在任何地方内联使用,而且不会占用固定的内存空间。(链接器可能不会将其放置在任何位置。)而 static 并不使其可变。使其可变的是 mut。至少,如果我理解正确的话... 我学习 Rust 才几天。 - Peter Hansen
2
@PeterHansen 你好,同为 Rustacean 的朋友,希望你喜欢这门语言^^。但你说得没错,至少文档是这么说的(请参见 https://doc.rust-lang.org/std/keyword.const.html)。 - sb27

20

我对Rust还不熟悉,但这个解决方案看起来有效:

#[macro_use]
extern crate lazy_static;

use std::sync::{Arc, Mutex};

lazy_static! {
    static ref GLOBAL: Arc<Mutex<GlobalType> =
        Arc::new(Mutex::new(GlobalType::new()));
}

另一种解决方案是将一个跨线程的通道传输/接收对声明为不可变的全局变量。通道应该是有界的,并且只能容纳一个元素。在初始化全局变量时,将全局实例推送到通道中。在使用全局变量时,弹出通道以获取它,并在使用完毕后将其推回。

这两种解决方案都应该提供一种安全的使用全局变量的方法。


19
&'static Arc<Mutex<...>> 没有意义,因为它永远不会被销毁,并且没有理由去克隆它;你可以只使用 &'static Mutex<...> - trent

18

如果你使用lazy_static宏,就可以为静态变量进行堆分配,如文档中所示:

使用此宏,可以拥有在运行时需要执行代码才能初始化的静态变量。这包括需要堆分配(如向量或哈希映射)的任何内容,以及需要函数调用来计算的任何内容。

// Declares a lazily evaluated constant HashMap. The HashMap will be evaluated once and
// stored behind a global static reference.

use lazy_static::lazy_static;
use std::collections::HashMap;

lazy_static! {
    static ref PRIVILEGES: HashMap<&'static str, Vec<&'static str>> = {
        let mut map = HashMap::new();
        map.insert("James", vec!["user", "admin"]);
        map.insert("Jim", vec!["user"]);
        map
    };
}

fn show_access(name: &str) {
    let access = PRIVILEGES.get(name);
    println!("{}: {:?}", name, access);
}

fn main() {
    let access = PRIVILEGES.get("James");
    println!("James: {:?}", access);

    show_access("Jim");
}

2
现有的答案已经讨论了“lazy static”。请编辑您的答案,清楚地说明这个答案相对于现有的答案提供了什么价值。 - Shepmaster

10

4
从Rust 1.70开始,还有OnceLock同步原语可以在我们只需要一个静态全局变量且仅需初始化(写入)一次的情况下使用。
以下是一个静态全局只读HashMap的示例:
use std::collections::HashMap;
use std::sync::OnceLock;

static GLOBAL_MAP: OnceLock<HashMap<String, i32>> = OnceLock::new();

fn main() {
    let m = get_hash_map_ref();
    assert_eq!(m.get("five"), Some(&5));
    assert_eq!(m.get("seven"), None);
    std::thread::spawn(|| {    
        let m = get_hash_map_ref();
        println!("From another thread: {:?}", m.get("five"));
    }).join().unwrap();
}

fn get_hash_map_ref() -> &'static HashMap<String, i32> {
    GLOBAL_MAP.get_or_init(|| {
        create_fixed_hash_map()
    })
}

fn create_fixed_hash_map() -> HashMap<String, i32> {
    let mut m = HashMap::new();
    m.insert("five".to_owned(), 5);
    m.insert("ten".to_owned(), 10);
    m
}

正如所见,该映射可以从不同的线程中访问。

请注意,HashMap::new() 不是常量(至少目前不是),这就是为什么我们(仍然)不能在 Rust 中拥有像 const MY_MAP: HashMap<...> = ... 这样的东西。


2

https://github.com/rust-lang/rust/issues/74465 和 https://github.com/rust-lang/rfcs/pull/2788 正在跟踪将此功能转移到稳定版本。 - John Vandenberg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接