rust 中的 static
1. 名詞
-
Package:名字可以包含 “-” 減號的,包含 carog.toml 文件,crate 等
-
Crate: 可以是 library 或 binary
-
Lib name: 不包含減號,如果 package name 包含減號而沒有指定 lib name 時,默認會把減號轉換爲下劃線,作爲 lib namev Lifetime: 不是 lifecycle,在 rust 中由於歷史的原因 lifetime 一般翻譯爲 “生命週期”,而“生命週期” 翻譯成英文爲 lifecycle,所以建議在 rust 中直接使用英文 lifetime,不要翻譯成中文。
-
Static lifetime: 與程序生成是一樣的, 語法是”'static”
-
Static bound: static 約束,滿足 static 的約束,並不一定是 static lifetime 的。Static lifetime 一定滿足 static bound。語法是使用在泛型參數的約束上,字符是一樣的”'static”。
2. 分類
與 static 相關的內容分爲四個分類,分別是 static lifetime/bound、static items、“static” 字段、“static methods”/associated function。
2.1. static lifetime/bound
在 rust 中並沒有把 static bound 單獨出來作爲一個概念,但是它與 static lifetime 確實不一樣,爲了方便理解,我們把 static bound 獨立出來。
2.1.1. 下面是代碼:
fn f(a: &'static i32) {
println!("a's type:{},address: {:p}", std::any::type_name::<&'static i32>(), a);
}
fn f2<T: 'static>(a: &T) {
println!("a's type:{},address: {:p}", std::any::type_name::<&T>(), a);
}
fn f3<T: 'static>(a: T) {
println!("a's type:{},address: {:p}", std::any::type_name::<T>(), &a);
}
{
println!("First part ====================================================");
let data = 5;
// f(&data);//`data` does not live long enough
static data2: i32 = 8;
print!("{:<30}","f(&data2);");
f(&data2);
print!("{:<30}","f(&&data2);");
f(&&data2);//自動解引用,實際輸入參數爲 &data2
}
{
println!("Second part ====================================================");
let data = 5;
print!("{:<30}","f2(&data);");
f2(&data);//雖然data是局部變量,但是它仍然滿足'static要求。
// f2(&&data);//編譯不通過“`data` does not live long enough”。data滿足'static要求,但是&data不滿足'static
static data2: i32 = 8;
println!("data address:{:p},data2 address:{:p}", &data, &data2);
print!("{:<30}","f2(&data2);");
f2(&data2);//f2::<i32>(&data2);
print!("{:<30}","f2(&&data2);");
f2(&&data2);//f2::<&i32>(&&data2);
// f2(&&&data2);//f2::<&&i32>(&&data2); 編譯不通過“temporary value is freed at the end of this statement”
println!("call f2::<i32>");
print!("{:<30}","f2::<i32>(&data2);");
f2::<i32>(&data2);
print!("{:<30}","f2::<i32>(&&data2);");
f2::<i32>(&&data2);//自動解引用,實際輸入參數爲 &data2
print!("{:<30}","f2::<i32>(*&&&&data2);");
f2::<i32>(*&&&data2);//自動解引用,實際輸入參數爲 &data2
//上面三個函數調用,參數的值都是相等的,都是&data2,也就是data2的地址
println!("call f2::<&i32>");
print!("{:<30}","f2::<&i32>(&&data2);");
f2::<&i32>(&&data2);//實際輸入參數爲 &&data2
print!("{:<30}","f2::<&i32>(&&&data2);");
f2::<&i32>(&&&data2);//自動解引用,實際輸入參數爲 &&data2
print!("{:<30}","f2::<&i32>(**&&&&data2);");
f2::<&i32>(**&&&&data2);//實際輸入參數爲 &&data2
//上面三個函數調用,但是值不相等,但deref值(*&&data2 == &data2 )是相等的。第二次引用是一個臨時變化的地址
// f2::<&&i32>(&&&data2);//編譯不通過“temporary value is freed at the end of this statement”
}
{
println!("Third part ====================================================");
println!("call f3");
let data = 5;
print!("{:<30}","f3(data);");
f3(data);//f3::<i32> ---> f3(a: 'static i32)
// f3(&data);//編譯不通過“`data` does not live long enough”
static data2: i32 = 8;
println!("data address:{:p},data2 address:{:p}", &data, &data2);
print!("{:<30}","f3(&data2);");
f3(&data2);
print!("{:<30}","f3(data2);");
f3(data2);
// f3(&&data2);//error "temporary value dropped while borrowed"
println!("call f3::<i32>");
print!("{:<30}","f3::<i32>(*&data2);");
f3::<i32>(*&data2);
print!("{:<30}","f3::<i32>(**&&data2);");
f3::<i32>(**&&data2);
println!("call f3::<&i32>");
print!("{:<30}","f3::<&i32>(&data2);");
f3::<&i32>(&data2);
print!("{:<30}","f3::<&i32>(&&data2);");
f3::<&i32>(&&data2);
//問題:當調用f3時,只要泛型參數相同(由於泛型的單態化,泛型參數相同,說明是同一個函數),輸出的地址也相同,如果f3(data)與f3(data2)輸出的地址相等。爲什麼?
//原因之一是f3中輸出的是參數值的內存地址,而不是data或data2的地址。爲什麼同一個函數,參數地址相同,這個還沒有搞清楚是什麼原因
}
2.1.2. 運行的輸出結果如下:
First part ====================================================
f(&data2); a's type:&i32,address: 0x7ff785d9a908
f(&&data2); a's type:&i32,address: 0x7ff785d9a908
Second part ====================================================
f2(&data); a's type:&i32,address: 0x82fd8fe704
data address:0x82fd8fe704,data2 address:0x7ff785d9a90c
f2(&data2); a's type:&i32,address: 0x7ff785d9a90c
f2(&&data2); a's type:&&i32,address: 0x82fd8fe850
call f2::<i32>
f2::<i32>(&data2); a's type:&i32,address: 0x7ff785d9a90c
f2::<i32>(&&data2); a's type:&i32,address: 0x7ff785d9a90c
f2::<i32>(*&&&&data2); a's type:&i32,address: 0x7ff785d9a90c
call f2::<&i32>
f2::<&i32>(&&data2); a's type:&&i32,address: 0x82fd8fe9f0
f2::<&i32>(&&&data2); a's type:&&i32,address: 0x82fd8fea48
f2::<&i32>(**&&&&data2); a's type:&&i32,address: 0x82fd8feaa8
Third part ====================================================
call f3
f3(data); a's type:i32,address: 0x82fd8fe3bc
data address:0x82fd8feb14,data2 address:0x7ff785d9a910
f3(&data2); a's type:&i32,address: 0x82fd8fe3b8
f3(data2); a's type:i32,address: 0x82fd8fe3bc
call f3::<i32>
f3::<i32>(*&data2); a's type:i32,address: 0x82fd8fe3bc
f3::<i32>(**&&data2); a's type:i32,address: 0x82fd8fe3bc
call f3::<&i32>
f3::<&i32>(&data2); a's type:&i32,address: 0x82fd8fe3b8
f3::<&i32>(&&data2); a's type:&i32,address: 0x82fd8fe3b8
2.1.3. 從以結果可以得出如下:
-
當 T 爲 i32 類型時,fn f(a:&'static i32) 與 fn f2<T:'static>(a: &T) 並不等價,參數 a 在 f 表示 lifetime 爲 static 的 i32 的引用,是一種 staticlifetime,在 f2 中表示 i32 的引用,且滿足 static 約束, 是一種 staticbound。例子 Let data = 5 時,f(&data) 編譯不通過,f2(&data) 編譯通過,能很好的說明這個問題。這個規則是這樣的: Owned 且沒有 reference 的滿足 static bound;滿足 static lifetime 的一定滿足 static bound; 滿足 static bound 的,它的引用不一定滿足 static lifetime; 滿足 static lifetime 的,它的引用也滿足 static lifetime。可以這樣理解他們的關係,Static lifetime 是一種特殊的 static bound, 要求更嚴格,反過來說是 static bound 包含 static lifetime。
-
Let data = 1; 其中的 data 可以是 “'static”(static bound), 但是 & data 不是 &'static i32
-
f2(&data) 編譯通過,f3(&data) 編譯不通過。把泛型寫成顯示參數, f2::(&data),f3::<&i32>(&data), 依據上面的規則 “滿足 static bound 的它的引用不滿足 static bound”, 就可以清楚的知道問題在那裏了。
-
在顯式定義時只能有 &'static, 比如定義 d:'static i32 編譯不通過,只能定義 d: &static i32
-
const 是 static lifetime 的
-
在寫範型參數時,一定要注意參數 T 是指代的那一部分。比如:是 & i32 整體作 T 還是隻是 i32 作爲 T.
-
自動解引用,會去掉多餘的引用。在泛型時,可以明確指定參數類型,來適應自動解引用。比如 f3(&&data2),參數類型是 &&i32,如果這樣寫 f3::<&i32>(&&&&&&data2),rust 的的自動解引用就會工作,最後傳給函數的只有 & i32。
static_lifetime:
CommonRust Lifetime Misconceptions
(https://doc.rust-lang.org/rust-by-example/scope/lifetime/static_lifetime.html#trait-bound)
(https://github.com/pretzelhammer/rust-blog/blob/master/posts/common-rust-lifetime-misconceptions.md)
2.2.static items
2.2.1. 語法:
Static See: https://doc.rust-lang.org/reference/items/static-items.html
Constant See: https://doc.rust-lang.org/reference/items/constant-items.html
Static items 與 constant items 很像,除了 static 有確定的的內存地址外(注:constant 是沒有確定的內存地址的)。Static 的特點:
有確定的內存地址
static lifetime
程序結束時不會調用 drop trait
初始化要 constant expression
多線程數據安全
static mut 是 unsafe 的
2.2.1.1. 在函數內部定義 (local)
注:前已有例子,不再重複
2.2.1.2. 直接定義在源代碼文件中(global)
static G_VALUE: i32 = 3;
static mut M_VALUE:u32 = 0;
2.2.1.3. 定義 static 的 String
static G_STRING1:String = String::new();
static G_STRING2:String = "".to_owned();
上面這個代碼是編譯不過的,因爲 String 其實是 Vec,可以修改,需要分配 heap 內存,不具有有常量的性質。而 & str 可以是常量,如下代碼可以工作
static G_STRING:&str = "same string";
是否可以定義 “static name: String”?
不可以,只能定義一個與 String 類似的的包裝,如 “&'static String”(不是 static &String)等。下面是 “&'static String” 的實現:
函數方式 (Cell/UnsafeCell 兩種實現)
pub fn FN_G_STRING2() -> &'static String {
static one:Once = Once::new();
static mut data: Cell<Option<String>> = Cell::new(None);
one.call_once(|| {
unsafe { data.set(Some("string FN_G_STRING2".to_owned())); }
});
unsafe { data.get_mut()}.as_ref().expect("static is not init")
}
pub fn FN_G_STRING3() -> &'static String {
static one:Once = Once::new();
static mut data: UnsafeCell<Option<String>> = UnsafeCell::new(None);
one.call_once(|| {
unsafe { *data.get() = Some("string FN_G_STRING3".to_owned())};
});
unsafe { &*data.get()}.as_ref().expect("static is not init")
}
通用實現
struct StaticData<T>{
one: Once,
data: UnsafeCell<Option<T>>,
}
impl<T> StaticData<T>{
pub const fn new() -> Self {
StaticData {
one: Once::new(),
data: UnsafeCell::new(None),
}
}
pub fn get<F: FnOnce() -> T>(&'static self, builder: F) -> &'static T {
self.one.call_once(||{
unsafe { *self.data.get() = Some(builder()); }
});
unsafe { &*self.data.get()}.as_ref() .expect("static not init")
}
}
unsafe impl<T: Sync> Sync for StaticData<T> {}
unsafe impl<T: Sync> Send for StaticData<T> {}
fn demo_static_data() ->&'static String{
static s_data: StaticData<String> = StaticData::new();
s_data.get(||"some string".to_owned())
}
static 的實現都是類似的,需要解決如下兩個問題:
-
怎麼定義一個常量。當需要的類型爲 String 時,使用 Option, 使用 None 來解決常量的問題
-
怎麼初始化一次(不支持常量的類型或需要寫代碼初始化時,纔有線程安全考慮)。當類型爲 String 時,使用 Cell/UnsafeCell 來解決第一次初始化時的賦值問題。
注:使用 std::sync::Once 只是爲了寫代碼方便,減少引入 package
2.2.2. 相關的庫
從上面實現 “&'static String”,可以看出,rust 對 static 類型有嚴格的限制,使用起來特別不友好,所以出現了三方庫來解決方便使用的問題。爲什麼官方不直接支持呢?猜的原因:
-
少使用 static,它是設計的後門,破壞代碼封裝,unsafe 等多種毛病
-
既然都少使用,那就讓社區自己解決吧
-
解決方式不優雅,那就讓社區自己解決吧
2.2.2.1.lazy_static
這是 macro 實現,非常小,主要方便做 static 的初始化。它的實現與 “通用實現” 類似,其中只使用了 package spin,沒有使用 std::sync::Once,應該是覺得 std 實現的不夠簡單明確,也或者是性能達不到要求。下面是實現概要代碼:
struct G_STRING1 {
__private_field: (),
}
#[doc(hidden)]
static G_STRING1: G_STRING1 = G_STRING1 {
__private_field: (),
};
impl ::lazy_static::__Deref for G_STRING1 {
type Target = String;
fn deref(&self) -> &String {
#[inline(always)]
fn __static_ref_initialize() -> String {
{
String::new()
}
}
#[inline(always)]
fn __stability() -> &'static String {
static LAZY: ::lazy_static::lazy::Lazy<String> = ::lazy_static::lazy::Lazy::INIT;
LAZY.get(__static_ref_initialize)
}
__stability()
}
}
impl ::lazy_static::LazyStatic for G_STRING1 {
fn initialize(lazy: &Self) {
let _ = &**lazy;
}
}
代碼邏輯大概如下:
-
定義一個空的 struct
-
定義一個 static items, 名字與結構體相同
-
在 struct 的 deref 中返回引用值
-
在函數中定義定義 static LAZY, 使用通過 spin::Once 來實現只初始化一次
Lazy_static 的特點:
-
使用 macro
-
輕量級
-
需要定義一個 struct
2.2.2.2.once_cell
功能比 lazy_static 多,沒有使用 macro,其中只使用 parking_lot。有 sync(thread-safe)與 unsync(Not thread safe) 兩個版本。下面是 sync 下概要代碼(由於代碼太多,只列舉 initialize 函數):
/// Safety: synchronizes with store to value via SeqCst read from state,
/// writes value only once because we never get to INCOMPLETE state after a
/// successful write.
#[cold]
pub(crate) fn initialize<F, E>(&self, f: F) -> Result<(), E>
where
F: FnOnce() -> Result<T, E>,
{
let mut f = Some(f);
let mut res: Result<(), E> = Ok(());
let slot: *mut Option<T> = self.value.get();
initialize_inner(&self.state_and_queue, &mut || {
let f = unsafe { take_unchecked(&mut f) };
match f() {
Ok(value) => {
unsafe { *slot = Some(value) };
true
}
Err(err) => {
res = Err(err);
false
}
}
});
res
}
同樣使用到了 Option 及初始化一次(Once),不過 Once 是直接實現,沒有使用庫。Once_cell 的特點:
-
沒有使用 macro
-
返回值爲 Option 類型,在使用時需要處理 Option,增加一點工作量
-
這種實現方式,可以作爲延遲加載(lazy)來使用。
-
輕量級
-
可以定義爲字段
2.2.2.3.static_init
功能比 once_cell 多,直接依賴的 package 變成 6 個,據說性能比前面兩個都好。提供 “statics dropped at program exit”,就是在程序退出時調用 drop 的功能
Static_init 的特點:
-
使用聲明宏(Declarative Macro)和過程宏(Procedural Macro)
-
實現功能多,代碼層次多
-
閱讀代碼有一定的難度
-
使用時沒有代碼提示,因爲使用了 Procedural Macro,編譯不知道有那些方法或字段
2.2.2.4.std::sync::Once
官方實現沒有 parking_lot 好,rust 官方實現與多線程及 future 相關的內容,實現不是很理想,所以有特別多的非官方實現,更有從 rust 出來,然後自己實現的(如:https://github.com/smol-rs),所以 lazy_static,once_cell,static_init 都沒有使用官方的 Once。
2.2.2.5.parking_lot::Once
非常不錯的關於鎖的實現,也提供了 parking_lot::ReentrantMutex(不過這個功能只是使用方便,理想情況下應該不使用 ReentrantLock,如果使用了,設計多多少少都有一點不合理)的實現。
2.3.“static” 字段
不支持這種語法,帶 class(面嚮對象語言,如 c++,java,dart 等)的語言都有,與 class 的所有實例都共享且是唯一,他不屬於實例,只屬於類。要想達到這個效果可以在 fn 中使用 static 定義一個變量作爲返回值來達到相同的目的。這種 fn_static 方式在方法裏面,非常方便初始化,也很容易理解。實現如下:
pub fn fn_static() -> i32{
static STATIC_FIELD: i32 = 10;//在第一次調用時分配,多次調用,也不會初始化多次
return STATIC_FIELD;
}
從使用上來說,這種方法更實用,可能官方也就沒有增加新的語法了。
2.4.“static methods”/associated function
在 rust 中沒有 “static methods” 這種說法,與之一至的是 “associated function”,中文叫關聯方法,就是沒有接收者的方法,即在 trait 或 struct 中,第一個參數不是 self 的方法,在調用時不需要 struct 的實例。這種方法與直接定義的 fn 效果是一樣的,只多了一層“命名空間” 而已。例子如下:
pub struct GlobalData{
name: String,
}
impl GlobalData{
pub fn new()-> GlobalData{
GlobalData{name:"".to_owned()}
}
}
pub fn GlobalData_new() -> GlobalData{
GlobalData{name:"".to_owned()}
}
這裏的函數 GlobalData_new 與 GlobalData::new 效果是一樣的,GlobalData::new 更有層次感覺,能表達出他們之間的關係,也更容易閱讀與理解。
這種 associated function 在 trait 中時,不同的 struct 可以實現不同的 “static” 方法,這是那些帶有 class 的語言所不支持的。當然 C++ 中也是不支持的,但是 C++ 有一個類似效果,是 C++ 泛型 / Template 的副作功能實現(see: c++ template static function specialization),比 Rust 實現複雜。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/KtxMP3Y4dWT4bcTBWJhT7A