rust中生命周期使用

wdf.jpg

以這段Go代碼為例作為開場

func createInt() *int {
    i := 42   // int 類型分配在棧上
    return &i // 這里由于返回引用類型,分配到堆上
} 

func main() {
    num := createInt()
    println(*num)  // 程序結(jié)束時(shí),num指向的堆內(nèi)存會被釋放
}

這是一段Go程序,是健康可運(yùn)行的,createInt函數(shù)返回指針,main函數(shù)調(diào)用,這里叫做 內(nèi)存逃逸

Go語言中的Gc回收器+逃逸分析,這兩個東西組合保障了程序能夠正常運(yùn)行,且不必要擔(dān)心安全問題

當(dāng)這段逃逸的堆內(nèi)存沒有地方再引用時(shí),會被回收掉

懸垂引用

一個引用(或指針)仍然指向一塊內(nèi)存地址,但這塊內(nèi)存已經(jīng)被釋放或不再有效。此時(shí)再繼續(xù)操作程序就會異常

下面是一個C語言例子

#include <stdio.h>

int* createInt() {
    int i = 42;      // int 類型分配在棧上
    return &i;       // 返回 i 的地址
}                    // ?? 函數(shù)結(jié)束,棧的內(nèi)存會被回收

int main() {
    int* num = createInt();  // 接收了已釋放內(nèi)存的地址(
    printf("%d\n", *num);    // ?? 未定義行為!可能打印 42,也可能打印垃圾值,或者崩潰
    return 0;
}

C語言沒Go那一套逃逸分析的自動堆內(nèi)存分配機(jī)制,這里的代碼實(shí)現(xiàn)就稱為 懸垂引用,或野指針

如果再C語言中實(shí)現(xiàn)該邏輯,需要手動將函數(shù)createInt返回內(nèi)容分配到堆上,為什么要分配到堆上?

因?yàn)闂5臇|西在函數(shù)執(zhí)行完就被回收了,注意哈,C語言沒有Gc回收機(jī)制,但棧上的內(nèi)存操作系統(tǒng)是會自動管理的

改良后的完整代碼

#include <stdio.h>
#include <stdlib.h>  // 包含malloc/free的頭文件

int* createInt() {
    int* i = (int*)malloc(sizeof(int));  // 分配堆內(nèi)存
    if (i == NULL) {  // 檢查內(nèi)存分配是否成功(必做)
        printf("內(nèi)存分配失敗\n");
        exit(1);
    }
    *i = 42;          // 給堆內(nèi)存賦值
    return i;         // 返回堆內(nèi)存地址(安全)
}

int main() {
    int* num = createInt();
    printf("%d\n", *num);  // 輸出 42(安全)
    free(num);             // 釋放堆內(nèi)存(避免內(nèi)存泄漏)
    num = NULL;            // 置空指針(避免野指針)
    return 0;
}

rust中懸垂引用

概念都是一樣的,rust中沒有C語言的malloc/free,沒有Go語言的Gc/逃逸分析

fn main() {
    let i = create_int();
    println!("{}", i);
}

fn create_int() -> &i32 {
    let s = 32;
    &s
}

rust的編譯器不僅僅用來轉(zhuǎn)換二進(jìn)制代碼,還提供了代碼解決方案,這是完整的報(bào)錯信息,里面已經(jīng)提示你正確的代碼書寫方式

error[E0106]: missing lifetime specifier
  --> src/main.rs:12:20
   |
12 | fn create_int() -> &i32 {
   |                    ^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
   |
12 | fn create_int() -> &'static i32 {
   |                     +++++++
help: instead, you are more likely to want to return an owned value
   |
12 - fn create_int() -> &i32 {
12 + fn create_int() -> i32 {
   |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `naive` (bin "naive") due to 1 previous error

rust中解決懸垂引用的方法為,-> 生命周期

rust 兩大數(shù)據(jù)類型

在搞清楚生命周期之前,要先區(qū)分兩大類型

  • 所有權(quán)類型:你是數(shù)據(jù)的“主人”,數(shù)據(jù)存在你的變量里
  • 引用類型:你只是數(shù)據(jù)的“借閱者”,數(shù)據(jù)屬于別人,你只能看或改,不能銷毀它
fn main() {
    let i = create_int();
    let j = create_intptr(); // 會報(bào)錯
}

// 32 這個值是變量 s 的,該函數(shù)返回的是 s,誰接收誰能隨便用
// 不管你是堆還是棧分配,所有權(quán)會自動管理
fn create_int() -> i32 {
    let s = 32;
    s
}

// 32 這個值是變量 s 的,該函數(shù)返回的是 s 的引用指針,接收方只能看/改
// 但是,函數(shù)create_intptr執(zhí)行結(jié)束了,s 釋放銷毀了,main函數(shù)中的 j 將變成懸垂引用
fn create_intptr() -> &i32 {
    let s = 32;
    &s
}

換句話來說,

在局部塊兒(函數(shù)、結(jié)構(gòu)體..)返回引用類型,會產(chǎn)生懸空指針;返回所有權(quán)類型,可以隨便玩

rust 中解決懸空指針的方法為 -> 生命周期

這里整理了一份 所有權(quán)類型 和 引用類型 的具體體現(xiàn)

所有權(quán)類型

特征:變量直接包含數(shù)據(jù)(或在堆上擁有數(shù)據(jù)),當(dāng)變量離開作用域時(shí),數(shù)據(jù)會被自動釋放(Drop)。
符號:通常沒有 & 符號。

類別 具體類型示例 說明
基本標(biāo)量類型 i32, f64, bool, char 這些類型大小固定,直接存儲在棧上。賦值時(shí)會復(fù)制一份新數(shù)據(jù)。
元組 (Tuple) (i32, bool), (String, i32) 如果元組內(nèi)包含擁有所有權(quán)的類型,元組本身也擁有它們。
數(shù)組 (Array) [i32; 5], ["a", "b"] 大小固定,直接存儲在棧上(除非很大)。賦值時(shí)復(fù)制。
結(jié)構(gòu)體 (Struct) struct User { name: String } 如果結(jié)構(gòu)體字段是 String 等擁有所有權(quán)的類型,結(jié)構(gòu)體實(shí)例就擁有這些數(shù)據(jù)。
枚舉 (Enum) enum Option<T> { Some(T), None } 同上,擁有內(nèi)部數(shù)據(jù)的所有權(quán)。
字符串 (String) String::from("hello") 重點(diǎn):這是堆分配的字符串,變量擁有堆上數(shù)據(jù)的所有權(quán)。
集合 (Collections) Vec<T>, HashMap<K, V>, Box<T> 這些都在堆上分配數(shù)據(jù),變量擁有堆數(shù)據(jù)的句柄(指針+容量+長度),負(fù)責(zé)釋放內(nèi)存。
閉包 (Closure) |x| x + 1 (捕獲所有權(quán)時(shí)) 閉包可以捕獲變量的所有權(quán)。

代碼示例

let a = 10;                 // a 擁有 10
let s = String::from("hi"); // s 擁有堆上的 "hi"
let v = vec![1, 2, 3];      // v 擁有堆上的數(shù)組
let my_struct = User { name: s }; // my_struct 現(xiàn)在擁有了 name (s 的所有權(quán)轉(zhuǎn)移了)

引用類型

特征:變量不包含數(shù)據(jù),只包含數(shù)據(jù)的地址。它們不負(fù)責(zé)釋放內(nèi)存。必須依附于某個擁有所有權(quán)的變量存在。
符號:必須帶有 & (不可變引用) 或 &mut (可變引用)。

類別 具體類型示例 說明
不可變引用 &i32, &String, &str 只能讀數(shù)據(jù),不能改??梢杂卸鄠€同時(shí)存在。
可變引用 &mut i32, &mut Vec<i32> 可以修改數(shù)據(jù)。同一時(shí)間只能有一個。
切片 (Slices) &[i32], &str 重點(diǎn):切片是對連續(xù)內(nèi)存部分的引用。&str 是對字符串?dāng)?shù)據(jù)的引用(通常指向 String 內(nèi)部或字面量)。
原始指針 *const T, *mut T 類似 C 的指針,不安全(unsafe),但也屬于引用語義(不擁有所有權(quán))。

代碼示例

let a = 10;
let r = &a;            // r 是引用,借用 a,r 的類型是 &i32

let st = "hello";      // 注意:st是引用類型,引用的一個 "hello" 的串,"hello" 這個字符并不屬于變量 st


let s = String::from("hello");
let slice: &str = &s;  // slice 是引用,借用 s 的一部分,類型是 &str

let mut x = 5;
let m = &mut x;        // m 是可變引用,類型是 &mut i32
*m = 10;               // 通過引用修改數(shù)據(jù)

rust 生命周期

生命周期是編譯器用來確保所有引用都是有效的機(jī)制。它們的主要目的是防止懸垂引用,即引用指向了已經(jīng)被釋放的內(nèi)存

語法規(guī)則

生命周期標(biāo)注以 ' 開頭,通常使用小寫字母,如 'a, 'b, 'c, 'static 等。通常會搭配泛型符號使用。

rust中,泛型不僅僅能用來定義自定義類型,也可用于自定義生命周期類型

static是一個特殊的關(guān)鍵字,下面會單獨(dú)說

不使用生命周期,會報(bào)錯

fn main() {
    let st = longest("hello", "world");
    println!("{}", st);
}

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

加上生命周期

fn main() {
    let st = longest("hello", "world");
    println!("{}", st);
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

這里聲明了一個生命周期為'a的泛型,返回的引用也使用了'a生命周期,這樣就保證了返回的引用指向的數(shù)據(jù)和傳入的引用指向的數(shù)據(jù)是同一個生命周期,不會出現(xiàn)懸垂引用

拓展

結(jié)構(gòu)體類型使用

struct Excerpt<'a> {
    part: &'a str,
}

// impl 方法實(shí)現(xiàn)中也需聲明生命周期: impl<'a> xxx<'a>
impl<'a> Excerpt<'a> {
    fn return_str(&self, announcement: &str) -> &str {
        println!("Announcement: {}", announcement);
        self.part // 返回的是 self 的一部分,生命周期與 self 綁定
    }
}

fn main() {
    let st = Excerpt { part: "hello" };
    println!("{}", st.return_str("world"));
}

多個生命周期

fn get_first<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    y // 明確返回的是 x 的切片,與 y 的生命周期無關(guān) 返回x可編譯成功,返回y會編譯失敗
}

fn main() {
    let st = get_first("hello", "world");
    println!("{}", st);
}

'static 生命周期

系統(tǒng)預(yù)設(shè)的一個特殊的生命周期,表示該引用在整個程序運(yùn)行期間都有效。(那怕是聲明方的程序已經(jīng)?;厥樟?

以字面量字符引用為例let s = "world";,這個類型編輯器顯示是&str類型,這里其實(shí)給省略了,它應(yīng)當(dāng)是一個定義了全局生命周期的&'static str類型。

案例_1

// 報(bào)錯
fn longest_1() -> &str {
    let s = "world";
    s
}

// 正常
fn longest_2(x: &str) -> &str {
    let y = "world";
    y
}

這是因?yàn)閞ust編譯器存在一套生命周期省略規(guī)則,有一條規(guī)則為 如果函數(shù)有一個輸入引用參數(shù)(如 &str),且返回值是引用類型,編譯器會自動將返回值的生命周期推斷為與這個輸入?yún)?shù)的生命周期相同。

longest_1 手動聲明返回類型為 -> &'static str 可以編譯通過

這個除外,編譯器根據(jù)邏輯,不確定使用x還是y 的生命周期

fn longest_6(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

案例_2

// 沒顯示聲明,任何生命周期都可以傳 'static 'a 'b
fn partial(s: &str) {
    println!("Static string: {}", s);
}

// 顯示聲明了 static 周期
fn global(s: &'static str) {
    println!("Static string: {}", s);
}

總結(jié)

各語言解決懸空指針的方法

  • Go 逃逸分析+Gc垃圾回收
  • C 手動分配堆內(nèi)存,手動回收
  • rust 生命周期
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容