在使用C或C++程式語言的時候,我們有時會將變數宣告在函數之外,使其可以在任何時間於整個程式的任何scope下使用。使用Java程式語言的時候,也有時會替類別加上「static」的類別(靜態)欄位變數,甚至還可以在「static」區塊中,在類別初始化的時候順便對類別欄位進行初始化的動作。我們把這類宣告在函數、方法之外,且獨立於物件實體的變數稱為「全域靜態變數」(C/C++的全域變數有分能給其它原始碼檔案使用的外部變數,與只能在同一原始碼檔案下使用的靜態變數),通常用來儲存在不同函數、方法間共享的資料。「靜態」的涵意是變數在編譯階段的時候就編譯器就會給定其初始值,因此只能給定在編譯階段時就能知道運算結果的表達式。「靜態」的值在程式的執行階段會持續存在。



C語言的全域靜態變數範例:

#include<stdio.h>

static int counter = 0;

int main(){
    counter++;
    printf("%d\n", counter);
    return 0;
}

Java的全域靜態變數範例:

public class MyClass {
    static int counter = 0;
    
    public MyClass(){
        counter++;
    }
    
    public static int getCount(){
        return counter;
    }
}

無論是在C語言或是Java語言中,靜態變數都是可以在任何地方進行改值的,但是在Rust程式語言中,根據這篇文章的說明,我們可以知道,若要存取可變的靜態變數的值,都必須要在不安全的模式下進行。

static mut COUNTER: isize = 0;

fn main() {
    unsafe {
        COUNTER += 1;
    }
    unsafe {
        println!("{}", COUNTER);
    }
}

靜態變數也很常用來暫存需要花費不少時間才能建立出來的大的陣列或是大表格,甚至能暫存某些在實體化的時候,需要花費時間進行一些程式執行階段才能夠知道運算結果的資料結構實體。以Java來說,由於每個類別皆擁有其實體化時所需的建構子,因此我們可以直接在宣告靜態變數的同時,就指定好其所儲存的物件實體,或是我們也可以在類別的「static」區塊中進行靜態變數初始化的動作。

程式如下:

public class MyClass {

    static ClassA ca1 = new ClassA(1, 2);
    static ClassA ca2;

    static {
        int a = ca1.getValue();
        ca2 = new ClassA(a, 6);
    }
}

class ClassA {
    
    private int value;

    ClassA(int a, int b) {
        int c = a + b;
        // and something other which may take time and finally initialize the value field ...
    }
    
    int getValue(){
        return value;
    }
}

以上程式可以看得出,Java對於靜態變數的使用可以說是十分直覺且自由。但如果要在Rust程式語言中實現相同的功能,該怎麼做呢?

struct StructA {
    value: isize
}

impl StructA {
    fn new(a: isize, b: isize) -> StructA {
        let c = a + b;
        // and something other which may take time and finally initialize the value field and return a new instance of StructA ...

        // The below is just used for passing the compilation.
        StructA {
            value: c
        }
    }

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

static CA1: StructA = StructA::new(1, 2);

如果將程式寫成以上這樣的話,程式第21行會編譯錯誤,因為編譯器無法在編譯階段就初始化「StructA」結構實體。所以我們必須手動在程式進入之後,於適當的時機,去使用「StructA」結構體的「new」關聯函數建立出「StructA」結構實體,接著再讓Rust程式進入不安全模式來儲存它。程式如下:

struct StructA {
    value: isize
}

impl StructA {
    fn new(a: isize, b: isize) -> StructA {
        let c = a + b;
        // and something other which may take time and finally initialize the value field and return a new instance of StructA ...

        // The below is just used for passing the compilation.
        StructA {
            value: c
        }
    }

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

static mut CA1: StructA;

fn main() {
    let ca1 = StructA::new(1, 2);

    unsafe{
        CA1 = ca1;
    }
}

然而,尷尬的是,程式第21行還是不會編譯成功,因為編譯器還是不知道靜態變數的值到底是什麼。此時我們最好的解決方案就是將靜態變數的型別改為Rust程式語言內建的「Option」列舉型別,來包裹「StructA」型別。程式改寫如下:

struct StructA {
    value: isize
}

impl StructA {
    fn new(a: isize, b: isize) -> StructA {
        let c = a + b;
        // and something other which may take time and finally initialize the value field and return a new instance of StructA ...

        // The below is just used for passing the compilation.
        StructA {
            value: c
        }
    }

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

static mut CA1: Option<StructA> = None;

fn main() {
    let ca1 = StructA::new(1, 2);

    unsafe{
        CA1 = Some(ca1);
    }
}

但這樣的作法還是會有一個小尷尬的地方是,每次在讀取「CA1」靜態變數的值時,我們都必須使Rust程式進入不安全模式,且要先經過「unwrap」的動作,從「Option」列舉取出「StructA」結構實體的參考來使用。例如:

fn use_ca1(){
    let ca1 = unsafe {
        CA1.as_ref().unwrap()
    };
}

「Option」列舉實體的「as_ref」方法可以將「Option<StructA>」轉成「Option<&StructA>」,如此一來使用「unwrap」方法才不會去嘗試改變「StructA」結構實體的擁有者,而導致編譯失敗。雖然透過這樣的方式是可以在Rust程式語言中應用全域靜態變數,但實在是太麻煩了!

lazy-static.rs

有鑑於原生Rust程式語言對於靜態變數的不友善,絕大多數的Rust開發者都選擇使用「lazy_static.rc」這個套件來處理靜態變數。

Crates.io

https://crates.io/crates/lazy_static

Cargo.toml

lazy_static = "*"

巨集的使用

「lazy_static.rc」提供了「lazy_static」巨集,允許我們在這個巨集內定義擁有初始化程式區塊的靜態變數。語法格式如下:

lazy_static! {
    static ref 靜態變數名稱: 型別 = {
        // 初始化程式區塊
    };

    static ref 其它靜態變數名稱: 型別 = {
        // 初始化程式區塊
    };

    static ref 其它靜態變數名稱: 型別 = {
        // 初始化程式區塊
    };

    pub static ref 其它公開的靜態變數名稱: 型別 = {
        // 初始化程式區塊
    };
}

套用lazy_static

將我們剛才舉的例子套上「lazy_static」巨集來用用看,程式改寫如下:

#[macro_use]
extern crate lazy_static;

struct StructA {
    value: isize
}

impl StructA {
    fn new(a: isize, b: isize) -> StructA {
        let c = a + b;
        // and something other which may take time and finally initialize the value field and return a new instance of StructA ...

        // The below is just used for passing the compilation.
        StructA {
            value: c
        }
    }

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

lazy_static! {
    static ref CA1: StructA = {
        StructA::new(1, 2)
    };
}

fn use_ca1() {
    let ca1: &StructA = &*CA1;
}

有了「lazy_static」巨集後,我們就完全不需要手動初始化靜態變數,而且也不用再寫「unsafe」關鍵字,在讀取靜態變數的資料時,也只需要加上「*」或是「&*」做「解參考」和「取參考」的動作即可。

如果要在初始化靜態變數之後,修改靜態變數的值的話,為了避免在多執行緒下出現競態修件的問題,必須搭配標準函式庫「std::sync」模組中的「Mutex」結構體來包裹使用。有關於Rust的並發與並行,可以參考這篇文章

「lazy_static」巨集還有一個不錯的特性是,它能定義靜態變數沒錯,但它所定義出來的靜態變數在編譯階段時只需初始化智慧型指標,而指標實際指到的值是在程式執行階段,當程式第一次使用到這個靜態變數時候才會去做值的初始化動作。也就是說,就算用「lazy_static」巨集宣告了很多靜態變數來儲存很佔記憶體或是會需要花很多時間來初始化的資料,在那個靜態變數被使用到之前,完全不會佔據記憶體(記憶體只會有智慧型指標),也不會增加程式在一開始執行的時候的載入時間。