rust學習十一.3、生命週期標記

正在战斗中發表於2024-11-26

生命週期,這是在"引用和借用“章節就提到的概念,大意是每個變數具有其作用域範圍。

所以,我個人更願意理解為作用範圍。 因為它不像java的變數那樣和時間有較為明顯的關聯,畢竟java的變數會被GC銷燬。

一、 生命週期註解概念引入

在原文中,作者是透過兩個例子解釋生命週期問題

fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
fn main() { let x
= 5; // ----------+-- 'b // | let r = &x; // --+-- 'a | // | | println!("r: {r}"); // | | // --+ | } // ----------+

作者給的圖很清楚地展示了作用範圍所導致的生命週期問題(作用範圍問題)。

對於上圖的第一種情況,在沒有特殊處理的情況下,r是借不到到x的值(離開返回後x就會被銷燬了)

另外一個例子

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

這個編譯會發生錯誤,作者給出的大體解釋是這樣:編譯器無法知道返回的是哪一個變數的引用,既然無法知道是哪一個,

那麼就無法知道其作用範圍,所以會報告錯誤(總之,道理都是rust說的)。為了避免這種情況,rust透過編譯器提示我們需要新增符號來表示:無論哪一個引數都是一樣的作用範圍。

這個符號就是一種類似泛型的寫法

例如上面的函式應該修改為:

 fn largest_str<'a>(str1: &'a str, str2: &'a str) -> &'a str {
     if str1.len() > str2.len() {
        str1
     } else {
        str2
     }
 }

這個符號還是挺怪異的:fn largest_str<'a>(str1: &'a str, str2: &'a str) -> &'a str

用<'a>表示這是生命週期說明,而不是一般的通用型別說明(通用型別是<T>)。

只有所有的引數和返回都要帶上‘a,表示它們的週期都是'a,都是一樣的。

完整程式碼

 fn largest_str<'a>(str1: &'a str, str2: &'a str) -> &'a str {
     if str1.len() > str2.len() {
        str1
     } else {
        str2
     }
 }

 fn main() {
    let s1="good";
    let s2="bad";
    let result=largest_str(s1, s2);
    println!("{}和{}種較長的一個是{}", s1,s2,result);
 }

註解符號

  • 必須用單引號(')開頭
  • 單引號後面通常跟上小寫字母
  • <'a>,使用尖括號包括‘a,表示引入生命週期符號'a

注意事項

  • 註解符號作用在單個方法引數上是沒有意義(意思是:如果只是單個,那麼可以不加,因為rustc能夠識別出來)
  • 在單個方法引數情況下無意義,但不表示其它情況下不需要
  • 只有藉助於編譯器,我們才會逐漸明白:哪裡需要新增生命週期註解符號

二、讓人迷惑的生命週期

2.1哪裡要引入生命週期符號

除了前面方法/函式中多個引入引數的情況需要說明,還有說明情況需要說明了?

rust發明人定義了其核心,所以哪裡需要書寫生命週期符號,並不是那麼明確。

#[derive(Debug)]
struct student<'a>{
    name: &'a str,
    age: i32,
}
enum cars<'a>{
    byd(&'a str),
}
fn print_str(s:&str)->&str{
    s
}

fn main() {
    let s = student {
        name: "Tom",
        age: 18,
    };
    println!("{:?}",s); 
    let s1 = print_str(s.name);
}

例如上例,如果結構和列舉沒有引入生命週期符號,那麼編譯都會錯誤,而函式print_str並不會錯誤。

對於函式print_str用書上給出的理由可以說得通,那麼結構中又是什麼意思了? 也許需要檢視編譯器程式碼才知道為什麼。

但對外就不那麼美好多了,好在編譯器足夠體貼。

2.2 是不是提示有問題?

這裡直接給出就是書上的例子:

 fn main() {
    let s1="good";
    let s2="bad";
    let result=largest_str(s1, s2);
    println!("{}和{}種較長的一個是{}", s1,s2,result);        

    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = largest_str(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {result}");
 }

這裡編譯報錯如下:

這個會報錯,利用前面所有權的知識,就能知道為什麼,所以這應該是所有權的問題,而不應該提示“borrowed value does not live long enough”.

當出現讓人迷糊的提示的時候,應該優先考慮是否所有權出現問題,而不是所謂的其他的生命週期。

上面的例子中,如果刪除最有一句那麼是不會報錯的:result = largest_str(string1.as_str(), string2.as_str());

所以,邏輯上,這裡呼叫larget_str不是問題,而是對result超出範圍使用,因為result無法訪問string2.

三、深入理解生命週期

即討論以下幾個問題:

  1. 不同程式結構中的生命週期定義
  2. 如何判斷是否需要定義申明週期定義
  3. 可以省略的註解
  4. 靜態生命週期

部分問題,前面已經討論過。

1、2、3問題其實都可以歸納為一個:到底什麼情況下需要引入生命週期註解符號?

給出一個可以將就的答案:等編譯器提示的時候再錄入不遲。

3.1不同程式結構中的生命週期定義

在以下幾中程式結構中,都可能需要引入:

  1. 結構-如果有引用,則必須有
  2. 列舉-如果有引用,則必須有
  3. 方法/函式-需要看情況。一般如果只有一個引數可以考慮不要;或者如果能夠明確知道總是和某一個引數相關也可以不要

部分在前面的例子中,已經給出結論,此處示例略。

3.2省略的生命週期註解

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

上例中,本來是需要註解,但是rust團隊後來發現遵循一些特定規則的,可以不要,以為可以推定出來。

還有例如:

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

這是因為編譯器可以推定返回的只和x有關。 這是屬於有多個引用引數且返回引用型別的。

還是這個例子。雖然可以透過編譯器推定出返回結果只和第一個引數有關,但是依然需要為第一個引數新增生命週期註解。這頗為迷惑,如果我們再看看下面這個程式碼:

fn f_beat_rustee(content: &str)->&str{
    content
}

這個是不需要的。

對於程式設計師而言,如何比較簡單地判斷:

  1. 如果有多個引用引數且返回也是引用,那麼一般是需要的註解的
  2. 如果只有一個引用引數,通常可以不要

如果實在還不行,就等著編譯器提示吧。

3.3 規則

函式或方法的引數的生命週期被稱為 輸入生命週期input lifetimes),而返回值的生命週期被稱為 輸出生命週期output lifetimes)。

編譯器採用三條規則來判斷引用何時不需要明確的註解。

第一條規則適用於輸入生命週期,後兩條規則適用於輸出生命週期。如果編譯器檢查完這三條規則後仍然存在沒有計算出生命週期的引用,編譯器將會停止並生成錯誤。這些規則適用於 fn 定義,以及 impl 塊。

注意:這個規則只適用於函式/方法

第一條規則(輸入、即用於引數)

編譯器為每一個引用引數都分配一個生命週期引數。換句話說就是,函式有一個引用引數的就有一個生命週期引數:fn foo<'a>(x: &'a i32),有兩個引用引數的函式就有兩個不同的生命週期引數,fn foo<'a, 'b>(x: &'a i32, y: &'b i32),依此類推。

第二條規則(輸出、用於返回)

如果只有一個輸入生命週期引數,那麼它被賦予所有輸出生命週期引數:fn foo<'a>(x: &'a i32) -> &'a i32

第三條規則(輸出、用於返回)

如果方法有多個輸入生命週期引數並且其中一個引數是 &self&mut self,說明是個物件的方法 (method)(譯者注:這裡涉及 rust 的物件導向參見 17 章),那麼所有輸出生命週期引數被賦予 self 的生命週期。第三條規則使得方法更容易讀寫,因為只需更少的符號。

3.4靜態生命週期

'static,其生命週期能夠存活於整個程式期間。所有的字串字面值都擁有 'static 生命週期

let s: &'static str = "I have a static lifetime.";
作者提醒我們:應該謹慎使用,畢竟這些會在整個程式執行期間佔用內容。

通常而言,只要不是過分,也不應過於擔心。看看java寫的後臺程式碼,那是一坨坨,一堆堆,一簇簇...的靜態常量。

但java是不用關心效能(相對而言)。

對於rust中,該用還是用,不過分以至於影響效能即可。

3.5 同時引入生命週期標記和通用型別

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {ann}");
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

這個例子主要告訴我們:如何引入

四、小結

  1. 為什麼需要生命週期註解?主要原因是因為編譯器無法推斷出變數的作用範圍,必須人工告知它們的作用範圍是一致的。如果你不告訴它,讓它在執行時處理,那麼大費心機弄得所有權和生命週期就麼有太大意義了
  2. 生命週期註解的作用就在於告知編譯器:某些引數它們的生命週期是一樣的,你放過我吧!
  3. 對於函式/方法而言,註解標記的必要性是因為引用型別引數過多導致的,並且是在返回型別也是引用的情況下
  4. 對於其它物件(結構體、列舉等),只要有引用型別,都必須定義生命週期註解標記
  5. 也有不需要新增註解情況,通常適用於只有一個引用引數,返回也是一個引用的情況。但是情況並不是只有這種。
  6. ‘static是一個靜態生命週期標記,常見的字串字面量都是這樣的。注意使用
  7. 一個方法中如果又有通用型別,又有引用,那麼還是可以寫出來的的,例如<'a,T>

rust為了維護程式的高效和安全,需要把大部分的問題消滅在編譯階段,所以這提高了對程式設計師的要求。

雖然如此,rust也提供了大概有史以來最體貼的編譯器。

相關文章