在使用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結構體的new關聯函數來建立出StructA結構實體,並指派給CA1這個全域靜態變數儲存。所以我們要手動在程式進入之後,於適當的時機,去使用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.rs這個套件來處理靜態變數。

Crates.io

Cargo.toml

lazy_static = "*"

巨集的使用

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

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

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

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

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

套用lazy_static

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

use lazy_static::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巨集宣告了很多靜態變數來儲存很佔記憶體或是會需要花很多時間來初始化的資料,在那個靜態變數被使用到之前,完全不會佔據記憶體(記憶體只會有智慧型指標),也不會增加程式在一開始執行的時候的載入時間。

once_cell

once_cell是繼lazy_static.rs之後出現的免巨集的解決方案,如果專案能夠使用標準函式庫的話,會比較建議使用once_cell。相信它在不遠的將來就會被正式納入Rust的標準函式庫中(目前還在實驗階段)。

Crates.io

Cargo.toml

once_cell = "*"

使用方法

once_cell比起lazy_static.rs最大的優勢就是前者能夠使用在一般區域變數上,可以單純讓變數的值的初始化過程只會被執行一次,之後就能重複取用第一次執行時初始化的結果。

once_cell提供了unsyncsync模組,區域變數在單執行緒下可以使用unsync模組提供的功能,而靜態全域變數要使用sync模組提供的功能。

程式寫法如下:

use once_cell::sync::Lazy;

static 靜態變數名稱: Lazy<型別> = Lazy::new(|| {
    // 初始化程式區塊
});

套用once_cell

將我們剛才舉的例子套上once_cell來用用看,程式改寫如下:

use once_cell::sync::Lazy;

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: Lazy<StructA> = Lazy::new(|| {
    StructA::new(1, 2)
});

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

常數函數

在Rust 1.31之後,我們可以在fn關鍵字前面加上const使其成為常數函數。常數函數會在編譯階段就被編譯器預先執行,並將函數的回傳值直接取代於其被呼叫的位置。

例如:

const fn return_one() -> i32 {
    return 1;
}

fn main() {
    let n = return_one();

    println!("{n}");
}

以上程式,第六行的地方呼叫了return_one常數函數,它會在編譯階段時被取代為return_one常數函數的回傳值。即:

fn main() {
    let n = 1;

    println!("{n}");
}

在宣告全域靜態變數或者常數時,我們可以直接呼叫常數函數來在編譯階段取得其返回值。如下:

struct StructA {
    value: isize
}

impl StructA {
    const 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);

StructA結構體的new關聯函數改為常數函數後,以上第21行的程式敘述就可以通過編譯了!