Rust生命周期之個人理解


在Rust里,類型分為基礎類型名(想不到更好的描述),類型泛型名,和生命周期(可以顯示或自動推斷一個生命周期標志)組成,其中生命周期[標志]也可以不算類型組成的一部分,因為它具有動態性是可變的;

生命周期標志在Rust編譯器足夠智能后單線程下有部分其實是可以不要的【對於結構體屬性如果是引用的rust強制要求提供生命周期標志】(多線程是需要的,多線程的作用域rust沒法比較,人都沒法完全比較對;單線程下生命周期函數'static也是蠻有用的可以限制調用者必須傳什么參數,比如用了format!這樣的宏,外部的參數如果是非static的是不能作為formatter字符串的),除非我們希望提供足夠詳細的生命周期報錯信息(最簡單的例子就是longest(a, b)->c的例子,其實本質上沒有生命周期標志rust編譯器也是能檢測到相關調用這類函數代碼是否有問題,但是目前rust要求必須聲明這種函數時就要指定生命周期標志);

我們通過代碼來理解聲明周期到底是什么:

1.

let mut list: Vec<&str> = Vec::new(); 
    let str1 = "ssf".to_string();
    list.push(str1.as_str());
    let str2: String = "ccc".to_string();
    list.push(str2.as_str());
    println!("{:?}", list);

上面的代碼我們可以看到list的元素類型是&str,且兩個元素str1和str2的生命周期都是一樣的(被編譯器自動推斷),且元素生命周期要大於等於list的生命周期(從使用的時候開始算,因此println!時list和str1和str2的生命周期一致);

因此println!(...)不會報錯(或說不會造成前者錯誤【后面的代碼示例可以看到是什么意思】);

2.

let mut list: Vec<&str> = Vec::new(); 
    list.push("aaa");
    {
        let str2: String = "ccc".to_string();
        list.push(str2.as_str());  // flag0
    }
    println!("{:?}", list);  // flag1

這里則會報錯,因為str2的生命周期比list要小,如果我們把flag1注釋掉不會報錯(以前估計是會報錯的,后來智能化了一點),因為rust編譯器能夠檢測到后續沒有再用list了所以不會造成內存安全問題(比如list.get(1)會引用已經釋放的內存,不過rust編譯器沒有那么智能所以發現是用到了list就認為可能產生內存安全從而報錯【因此哪怕后續我們只用list.get(0)也會報錯】,不過報錯的地方是flag0,這也是上面說的不會造成前者錯誤的解釋)

3.

let mut list: Vec<&str> = Vec::new(); 
    list.push("aaa");
    let str2: String = String::new();
    list.push(str2.as_str());
    println!("{:?}", list);

這個也不會報錯,明顯第一個元素和第二個元素的生命周期是不一致的,一個是static,一個是暫且叫'a,所以之前自己在知乎看的一篇文章說的是有問題的;這里是只要元素的生命周期大於等於list即可;

4.

#[derive(Debug)]
struct Kkk<'a, 'b> {
  // 這里不是說pro1是'a的,它只是一個引用/借用/指針(引用作為結構屬性和部分特殊的函數【比如longest這類】必須有生命周期標志)
  // 這個是說pro1這個引用指向的空間是一個String對象空間,然后這個對象空間的生命周期是'a pro1:
&'a String, pro2: &'b i32, } ------------------------ { let sse = 3; let mut k = Kkk {pro1: &"sss".to_string(), pro2: &sse}; println!("{:?}", k); { let ssk = 4; k.pro2 = &ssk; println!("{:?}", k); } //println!("{:?}", k); // flag1 //println!("{:?}", k.pro1); // flag2 }

接下來看這個不會報錯,由這個例子可以看出結構體里不要求所有的屬性生命周期是一致的,也不要求屬性的生命周期標志只能對應一個生命周期(比如pro2的先后兩次賦值的生命周期顯然是不同的,但是都用的'b標志。。【這就是一開始說的生命周期作為類型一部分是可變的意思】)

如果我們只把flag1取消注釋則會報錯,因為此時pro2的生命周期要小於k,所以會產生內存安全問題(引用已經釋放的屬性pro2,對應ssk變量);

但是如果我們只取消flag2的注釋也是會報錯的,盡管我們只引用了pro1,而pro1的生命周期是大於等於k的,這個是因為rust編譯器不夠智能導致的(以后可能會改善);就像我們閉包里如果用了一個全局變量則必須加鎖才行,哪怕我們的程序是單線程的,因為rust編譯器不夠智能,它不知道我們會不會把這個閉包用在多線程里,因此干脆就直接提示可能存在問題需要加鎖(雖然是可以更智能,不過代價很大,可能會造成編譯速度下降和編譯器占用內存啥的都升高)

 

【非智能指針對象,即棧上的對象,不存在RAII銷毀對象的說法,在C++里相當於不是通過new創建的,自然不需要delete【類似比如int i = 3;我們是不需要說delete i的,int也可以是struct類型前提是棧上的對象(字面量賦值后的變量不是static的)】,只是作用域過后不可見,之后棧幀彈出后釋放變量值(棧上對象值)】

接下來是多線程下的生命周期問題,目前有這些需要特殊關注的點:

1.我在A線程創建k對象,然后k的兩個屬性也都是在A線程創建的,所以在A線程創建這些變量的方法或作用域塊結束后就會釋放這些對象及屬性,

因此開啟一個線程B的方法里如果捕獲了在線程A創建的變量時需要用到move【標准庫里的是必須move除非捕獲的“變量”是'static的,但是如果我們明確知道開啟的線程B一定比A先銷毀,我們可以用第三方庫實現不move只是借用一下和只read,貌似crossbeam可以】,將這些變量及屬性的所有權move到線程B里(對應的變量值(棧上的)會復制一份在closure的棧上創建一個同名變量【這個復制和Copy trait不是一個概念,就是連續內存空間的復制】),這樣當A線程創建這些對象的作用域結束后就不會去釋放這些對象或屬性了(前提是棧變量是智能指針類型)【Box創建的對象不會釋放,但是棧變量還是會銷毀的】,只不過在創建線程move之后A線程是沒法再使用這些變量了);

不過這造成了一個問題,就是當我們在A線程開啟這樣的線程后,在接下來的A線程代碼里就無法引用這些變量了,這個和Java之類的語言很不一樣;

如果我們想要在A線程后續繼續用move的變量該怎么辦呢,目前我知道的就是要么在A線程用完相關變量之后再創建B線程move這些變量(廢話),要么就是把這些變量創建為全局變量,然后用的時候都要加鎖。。;

好像之前還聽過可以在A線程里將move的變量再move回來,不知道是不是有這個功能(聽起來像Go的channel功能,好像rust里有個mpsc channel就類似這個功能,不過channel對象應該是要求全局的【或說是創建在堆上】)

,這個功能如果用類似channel的方式做應該是這個樣子,A線程在創建線程move了k對象后,然后繼續執行其他代碼,然后在需要再次用到k的時候通過全局的channel的引用來接收B線程發過來的k對象,B線程在做完一些邏輯后也是通過堆 channel的引用來send處理后的k對象,在A線程receive k對象的過程里是會阻塞A線程的直到接收到了消息(即k對象【或包含其的對象】)然后繼續A線程的處理邏輯;

Rust的channel是一個函數創建一個receiver和一個sender,在堆空間里;因此A和B線程分別用其中一個即可,都是直接move過去;

Rust應該還有可以在多個線程共用的數據類型,即不是那種必須move到B然后A就不能用了的(不過這種不是安全的操作),比如上面的receiver我們如果希望A和B線程都能receiver時就要這個功能(go里沒記錯的話好像是send一條數據后會“隨機”一個協程通過同一個channel接收到數據【但應該是先循環探測到數據的那個協程收到數據】)

 

注意,Rust里let p = Point { x: 0.0, y: 0.0 }是創建在棧上(所以p所在作用域結束后只是后面代碼無法訪問,但是沒有delete,而是等對應棧幀彈出后釋放棧幀上所有對象)

而let p = Box::new(Point { x: 0.0, y: 0.0 })則是創建在堆上,因此創建p對象的作用域結束后會自動delete p引用的內存空間,而p變量變成不可訪問,但是p也是占用一個指針地址的需要棧幀結束后彈出;

因此對於需要move到其它線程的對象,最好創建在堆上開銷小一點;

 

再來看這個代碼:

static mut KKK: String = String::new();

fn main() {
    unsafe {
        KKK = "ss".to_string();
        test(&KKK);
        /*let kkk = "ss".to_string();
        test(&kkk);*/
    }
}

const fn confn() -> i32 {
    8
}

fn test(a: &'static String) {
    println!("{}, {}", a, confn())
}

上面的代碼不報錯,由此可知static生命周期標志(非static生命周期標志必須在結構體名或方法名上先聲明才能在參數或屬性上用,static不需要)並不是說必須要字面量,也不是說必須是常量,可變的static變量也是可以的,因為它在棧上且不會被delete掉;

像Box::new(..)的結構體雖然也在棧上,但是它指向的對象是可能被自動delete掉的,因此不符合static生命周期的定義。

再看這個代碼:

fn main() {
    test2("aaa")
}

const fn confn() -> i32 {
    8
}

fn test2(a: &'static str) {  // 是指a這個 str類型的借用(指針)指向的str類型空間是static的【不能被RAII釋放的堆空間,無法被解引用,能被解引用的指針(借用)都是指向的可以被RAII的空間】
    println!("{}, {}", a, confn())
}

這里也不報錯,因為"aaa"是作為程序元數據的一部分的在加載程序后(加載程序指令,而"aaa"直接作為了指令的一部分而不需要創建棧空間或者堆空間),因此符合static

再看這個:

fn main() {
    test();
}

struct Kkk {
    pro1: i32
}

fn test() {
  // 這里可以打印{:p}來輸出k指向的地址,然后多次調用test()可以發現都是指向的一個地址,說明確實是靜態局部變量; let k
= &Kkk{pro1: 8};  // 這里如果換成&mut Kkk{pro1:8};則報錯了,因為這種生成的是局部變量,而非mut的生成的是static的局部綁定變量,所以沒有內存泄漏,因為下次調用這個方法用的k指向的對象還是上一次調用這個方法產生的 test2(k); /*let k2 = Kkk{pro1: 8}; test2(&k2);*/ } fn test2(a: &'static Kkk) { println!("{}", a.pro1) }

這里也不報錯,但是注釋的那部分是會報錯的,這里感覺和Go很像不&則是分配在棧上,而&則是分配在堆上(但是注意不是說分配在堆上就符合static);

所以這里要消除一個錯誤的認知,即let kk = &Kkk{pro1:8};並不是生成這樣的代碼(至少不是唯一的方式,可能會根據上下文產生不同的情況):

let tmp1 = Kkk {pro1:8};
let kk = &tmp1;
/*
好吧,這里的生成代碼的方式和是否有mut有關,let k = &Kkk{pro1: 8};這種方式生成的是static tmp1 = Kkk {pro1:8};【tmp1只讀,而且也沒有產生內存泄漏,這個就和C++的方法里的static變量是一樣的】
而如果是let k = &mut Kkk{pro1:8};這種方式則是生成的是let mut tmp1 = Kkk {pro1:8};
*/

不過話說回來,上上的代碼示例里的情況應該會產生內存泄漏把?k指向的地址是static的,因此根據之前的理解應該是不能被RAII 自動delete釋放的,但是test里執行完后k指向的堆空間應該是沒法再訪問到的。

let bbb = "sfjlk";  // 這個字符串字面量類型是&str,即這個字符串實際上是保存在不能被RAII釋放的堆空間里的(應該有類似享元模式的str優化)
// ,然后獲取這個str的地址(對該str對象借用)賦值給bbb,因此不能用*"sss",因為static 對象無法被move【可以move的都是能被釋放的】

注意上面的代碼bbb是static的,通過這個代碼測試出的:【沒問題,字符串的字面量確實是static的,因為它本身就是&str,但是數值的字面量不是static,因為數值是i32,所以let bbb = "kkk";bbb是static,而let bbb = 3;bbb不是static,但是let bbb = &3;則bbb是static】

【還有一點很重要:(a: &'static str),這里是說a是一個static的str的引用,而不是說a這個引用是static的,這一點要明確,理解了這個后用人的思維看這個就很好理解了,bbb雖然是棧上的變量,但是它是一個static str的引用因此符合這個條件】

fn test3(a: &'static str) {
    println!("{}$$$", a);
}

fn main() {
    let bbb = "sfjkl";
  // 這里可以寫成&bbb或&&bbb都是一樣的,因為會被自動解引用為bbb,但是closure捕獲的變量是不會自動解引用的,因為本身就沒有需要轉換為什么類型 test3(bbb); }

/*
而且這里將bbb換成:
let uuks = String::new();
let bbu = uuks.as_str();

里的bbu則編譯不通過提示bbu存活時間不夠長,說明不是編譯器“智能”的發現bbb雖然非static但是確實要比test3更長存活所以也通過了,而是bbb就是static的;【宏參數則要求必須是字面量,static的變量都不行,宏比較特殊就不考慮那么多了】
*/

但是這個代碼又不能運行成功不知道為什么【經過大佬提點,closure如果沒有move則這里其實是捕獲的bbb的借用,即這里的bbb的類型實際上是&&'static,因此它類似let b = 3;我們捕獲b一樣(只不過3的類型換成指針類型),

或者這么說,bbb指向的字符串確實不會在調用者方法結束后RAII掉,但是bbb這個指針變量是會的,而不move則這里捕獲的是指針變量的引用,因此存活期可能不夠長】:

【這里再換個概念對個人理解更好,就是rust closure不能理解為捕獲一個變量,而應該理解為在closure里產生一個地址空間的引用/指針(某種意義上也叫變量的引用),如果這個地址空間是static的則可以不用move,否則必須move(這個時候就不要非得用生命周期標志來理解了
,按人的思維理解反而更好,即f: 'static是指f里捕獲的變量【捕獲的對象空間(i32的也是一種對象)】必須是static的【不被釋放的堆空間】,然后closure會自動獲取其(這個空間)借用)
,因此這里沒有move時bbb對象所在的空間是棧空間,是會消亡的,因此不是static,一下子就理解了,如果非得生搬硬套生命周期標志就很蛋疼(也可以理解,就要把它展開為let bbb_tmp = &bbb)】
let bbb = "sfjlk"; // 換成這個可以:static bbb: &str = "sfjlk";,這個時候調用者函數結束也不會釋放bbb變量【但是如果換成static mut則又不行,因為這里可能存在並發問題(可以用unsafe解決,不過最好加鎖)】 thread::spawn(|| { println!("{}####", bbb); }).join();
// 這里如果用了move個人認為是編譯器產生了魔法,即在closure最上面(即println上面)隱式的添加一個代碼let bbb = 外部bbb的值【一個地址】;,這個是在運行到這里時執行的,編譯期間應該沒法知道bbb的值是多少吧
    for v in 1..=2 {
        test8();
    }
}

fn test8() {
    static mut m: i32 = 0;
    unsafe {
        println!("{}", m);
        m += 1;
    }
}
 
         

這里輸出兩次分別是0和1可以知道static局部變量的用處函數結束后不會釋放m變量,因此m變量其實是在堆里的一個i32變量,只不過可見性是test8函數里,因此這里其實捕獲的不是局部變量而是全局變量了【和C++一樣】

 
        

按理這個f確實捕獲的是static的變量才對啊,為什么不允許呢【經過大佬提點,閉包捕獲的變量如果沒有move,其實是捕獲的是這個變量的借用,因此捕獲的變量的類型其實是&&'static,即bbb是&'bbb,但是這里又弄了它的借用因此不是'static的了】

這部分代碼換一下就可以了,但是還是沒有解釋上面的為什么不行:

fn main() {
    static bbb: &str = "sfjlk";
    thread::spawn(|| {
        println!("{}####", bbb);
    }).join();
}

 

還有就是類型的static和對象/變量的static不是一個概念,看代碼:

struct TestStruct(i32);
trait Hello: Sized {
    fn say_hello(&self) {  // 默認trait實現
        println!("hello");
    }
}
impl Hello for TestStruct{}

fn test1<T: Hello + 'static + Sized>(t: &T) {
    t.say_hello();
}

fn test2() {
    let x = TestStruct(666);
    test1(&x);
}

fn main(){
    test2();
}

上面的代碼通過,可能有人會覺得疑惑,這里不是要求T是static的嗎,但是x明顯是局部變量啊;注意這里的static是針對T類型的,即T類型必須是static的類型聲明(前提是它必須聲明生命周期標志,那么編譯器判定發現生命周期標志不等於static則不符合上面的約束);

(注意impl trait是可以寫在局部的,類似use一樣可以局部use)

【注意,在方法里聲明的類型也是static的,只不過是作用域可見性變小了,類似函數里static變量一樣】

看這段代碼:

struct TestStruct<'a>(&'a i32);
trait Hello: Sized {
    fn say_hello(&self) {
        println!("hello");
    }
}

impl<'a> Hello for TestStruct<'a>{}

fn test1<T: Hello + 'static + Sized>(t: &T) {
    t.say_hello();
}

fn test2() {
    let y = 666;
    let x = TestStruct(&y);  // 這種不行,【因為y不是static(注意不是說字面量就是static的,所以之前的那個字面量寫到程序元數據里所以是static的理解是錯誤的,static就是因為指向的空間是不會被RAII的堆空間)】
    test1(&x);
}

fn main(){
    test2();
}

這個就會報錯了,因為這里經過編譯分析,發現TestStruct<'a>不等於TestStruct<'static>(當然不存在這種寫法,static不需要先聲明才能在屬性上使用),因此這里判定T不符合T: 'static的條件;

我這邊再換成這種寫法:

struct TestStruct<'a>(&'a i32);
trait Hello: Sized {
    fn say_hello(&self) {
        println!("hello");
    }
}

impl<'a> Hello for TestStruct<'a>{}

fn test1<T: Hello + 'static + Sized>(t: &T) {
    t.say_hello();
}

fn test2() {
    //let y = 666;
    let x = TestStruct(&666);  // 改動點
    test1(&x);
}

fn main(){
    test2();
}

這種寫法又可以了(因為&666實際上是生成了一個static的局部變量,類似static tmp1: i32 = 666;

 

結構體的屬性如果是引用類型則rust要求必須寫生命周期標志(因為引用/借用類型的屬性,它本身沒有這個屬性指向的對象的所有權,所以它是可能用着用着就被其他地方給釋放掉了,因此必須用生命周期標志);


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM