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