Rust 1.80后如何使用延遲初始化模式?
在應(yīng)用程序開始時最常見的事情之一是初始化各種資源。這可以是應(yīng)用程序配置、日志服務(wù)或某些數(shù)據(jù)庫連接。然而,并非所有這些都需要在應(yīng)用開始時就準(zhǔn)備好,因為這可能會導(dǎo)致啟動緩慢。
這就需要在使用資源的時候再進行初始化,延遲初始化模式可以幫助我們推遲資源的初始化。如果資源根本不使用,也可以完全跳過初始化。
在Rust的舊版本中,其標(biāo)準(zhǔn)庫不支持這種延遲初始化。在生態(tài)系統(tǒng)中有幾個流行的crate通常用于此功能,例如lazy_static和once_cell。從Rust 1.80開始,這些crate提供的許多功能現(xiàn)在可以在標(biāo)準(zhǔn)庫中使用,并且可以用來代替這兩個crate。
在這篇文章中,我們將介紹什么是延遲初始化模式,lazy_static和once_cell如何提供延遲初始化的功能,如何使用標(biāo)準(zhǔn)庫進行延遲初始化,標(biāo)準(zhǔn)庫與lazy_static和once_cell之間的比較。
延遲初始化模式
考慮一個示例,我們有一個不經(jīng)常使用的API接口,需要從磁盤讀取和解析一個大文件。
我們可以在應(yīng)用程序開始時執(zhí)行讀取和解析,但是,這可能會阻止服務(wù)器在解析完成之前提供服務(wù)。還有一種情況是,這個特定的API接口根本沒有被調(diào)用,因此用于加載文件的資源是沒有用的。
另一個例子是,如果應(yīng)用程序使用一些內(nèi)存DB,如sqlite或redis。但是,實際上并非應(yīng)用程序的所有調(diào)用都需要數(shù)據(jù)庫。在這種情況下,將DB加載到內(nèi)存中并每次維護連接開銷是沒必要的。
我們可以將這些資源的初始化推遲到需要的時候,在第一次使用時初始化它們,并保留它們以供以后使用,這種模式被稱為延遲初始化模式。
然而,這在Rust中出現(xiàn)了一個小問題,我們必須將延遲初始化的資源作為參數(shù)傳遞給每個函數(shù),或者將其設(shè)置為靜態(tài)全局的,并在運行時使用unsafe的Rust代碼對其進行初始化。
為了避免這種情況,像lazy_static或once_cell這樣的crate為不安全的操作提供了安全的封裝,我們可以使用它們在代碼中安全地使用延遲初始化的值。
lazy_static和once_cell如何提供延遲初始化
lazy_static提供了一個宏來編寫靜態(tài)變量的初始化代碼,并且在運行時第一次使用時初始化變量。一般語法是:
use lazy_static::lazy_static;
lazy_static!{
static ref VAR : TYPE = {initialization code}
}
例如,將日志級別設(shè)置為靜態(tài)變量,如下所示:
use lazy_static::lazy_static;
lazy_static! {
static ref LOG_LEVEL: String = get_log_level();
}
fn get_log_level() -> String {
match std::env::var("LOG_LEVEL") {
Ok(s) => s,
Err(_) => "WARN".to_string(),
}
}
fn main() {
println!("{}", *LOG_LEVEL);
}
lazy_static!宏定義在運行時使用get_log_level函數(shù)來設(shè)置日志級別。
雖然這很簡單,但它也有一些自己的問題。我們必須使用靜態(tài)ref,這不是一個有效的Rust語法,我們需要取消對LOG_LEVEL的引用,以便在println語句中使用。
我們可以使用once_cellcrate做同樣的事情:
use once_cell::sync::OnceCell;
static LOG_LEVEL: OnceCell<String> = OnceCell::new();
fn get_log_level() -> String {
match std::env::var("LOG_LEVEL") {
Ok(s) => s,
Err(_) => "WARN".to_string(),
}
}
fn main() {
let log_level = LOG_LEVEL.get_or_init(get_log_level);
println!("{}", log_level);
}
在這里,我們沒有在聲明中指定代碼,而是在需要獲取值時使用了get_or_init方法。
如果值未初始化,則使用給定函數(shù)初始化該值,否則將返回現(xiàn)有值。因為我們直接獲取值,所以不需要任何額外的解引用操作。
雖然這兩種方法各有優(yōu)缺點,但是需要在依賴項中再添加一個外部crate。Rust 1.80后的標(biāo)準(zhǔn)庫提供了延遲初始化的類型,我們就可以直接使用這些類型,從而減少依賴項的數(shù)量。
使用標(biāo)準(zhǔn)庫進行延遲初始化
在Rust 1.80后,類似lazy_static和once_cell的延遲初始化類型已經(jīng)在Rust標(biāo)準(zhǔn)庫中穩(wěn)定下來了。我們可以使用它們代替任何外部crate來實現(xiàn)類似的功能。
標(biāo)準(zhǔn)庫中的OnceLock類型可以類似于once_cellcrate的OnceCell類型使用:
use std::sync::OnceLock;
static LOG_LEVEL: OnceLock<String> = OnceLock::new();
fn get_log_level() -> String {
match std::env::var("LOG_LEVEL") {
Ok(s) => s,
Err(_) => "WARN".to_string(),
}
}
fn main() {
let log_level = LOG_LEVEL.get_or_init(get_log_level);
println!("{}", log_level);
}
與once_cell的例子相比,我們用OnceLock代替了OnceCell,但其余的代碼仍然是相同的。OnceLock類型還公開了一個名為get_or_init的方法,該方法提供了與OnceCell的get_or_init相同的功能。
與lazy_static相比,我們可以使用LazyLock類型在聲明級別指定初始化函數(shù),而不必使用宏:
use std::sync::LazyLock;
static LOG_LEVEL: LazyLock<String> = LazyLock::new(get_log_level);
fn get_log_level() -> String {
match std::env::var("LOG_LEVEL") {
Ok(s) => s,
Err(_) => "WARN".to_string(),
}
}
fn main() {
println!("{}", *LOG_LEVEL);
}
這里我們傳遞一個初始化函數(shù)給LazyLock的new方法,當(dāng)變量的值第一次被訪問時,類型內(nèi)部調(diào)用這個函數(shù)來初始化這個值。
標(biāo)準(zhǔn)庫與lazy_static和once_cell之間的比較
與lazy_static和once_cell這兩種crate相比,標(biāo)準(zhǔn)庫提供的延遲初始化類型的一大優(yōu)點是不需要任何額外的依賴項。盡管這兩個crate本身只有幾個依賴項,但這仍然意味著你的項目將有更多的依賴項,并且需要更多的編譯時間。
標(biāo)準(zhǔn)庫提供的延遲初始化類型的另一個優(yōu)點是它們是由官方rust標(biāo)準(zhǔn)庫團隊直接開發(fā)和維護的。
總結(jié)
將lazy_static初始化類型引入Rust標(biāo)準(zhǔn)庫本身,為在代碼中使用延遲初始化提供了很多便利,而無需添加另一個crate作為依賴項。
有了這些類型,我們就可以只在需要的時候進行昂貴的計算,并且只初始化一次昂貴的結(jié)構(gòu),比如正則表達(dá)式,而不必每次都手工進行初始化檢查。
相信隨著時間的推移,我們會看到更多項目使用標(biāo)準(zhǔn)庫中的延遲初始化類型,而不是在新創(chuàng)建的項目中引入外部crate。