關於Rust的生命周期,我們在先前的章節中已經先學習了一部份了。在這個章節,我們將會學習如何使用生命周期的子型別,了解如何替泛型型別參數指定生命周期,以及特性物件的生命周期規則。



生命周期的子型別

生命周期的子型別是一種定義一個生命周期比另一個生命周期還要長的方式。我們直接看底下這個例子吧!

struct Context<'a>(&'a str);

struct Parser<'a> {
    context: &'a Context<'a>,
}

impl<'a> Parser<'a> {
    fn parse(&self) -> Result<(), &str> {
        Err(&self.context.0[1..])
    }
}


fn parse_context(context: Context) -> Result<(), &str> {
    Parser { context: &context }.parse()
}

fn main() {
    parse_context(Context("Hello World")).unwrap();
}

這個程式會編譯失敗,因為程式第15行,Parser結構實體的context欄位所儲存的Context數組結構實體的參考的生命周期,與Context數組結構實體的字串切片的生命周期並不相同。如果要利用我們目前掌握的Rust相關知識來修改這個程式,使其通過編譯的話,可以修改成這樣:

struct Context(&'static str);

struct Parser<'a> {
    context: &'a Context,
}

impl<'a> Parser<'a> {
    fn parse(&self) -> Result<(), &'static str> {
        Err(&self.context.0[1..])
    }
}


fn parse_context(context: Context) -> Result<(), &'static str> {
    Parser { context: &context }.parse()
}

fn main() {
    parse_context(Context("Hello World")).unwrap();
}

將字串切片的生命周期全都改為「'static」,就可以成功使程式通過編譯。但是這樣的話,Context數組結構就只能儲存字串定數了。如果我們想要讓Context數組結構真的能夠儲存任意的字串切片,那就不能使用「'static」作為字串定數的生命周期,需用其它的方式來修正。其實仔細想想的話,不難發現,我們只要將原先的程式碼,「parse_context」函數的context參數改為Context參考型別就可以了。程式改寫如下:

struct Context<'a>(&'a str);

struct Parser<'a> {
    context: &'a Context<'a>,
}

impl<'a> Parser<'a> {
    fn parse(&self) -> Result<(), &'a str> {
        Err(&self.context.0[1..])
    }
}

fn parse_context<'a>(context: &'a Context) -> Result<(), &'a str> {
    Parser { context: context }.parse()
}

fn main() {
    let context = Context("Hello World");
    parse_context(&context).unwrap();
}

但如果我們還是想要讓「parse_context」函數能夠直接從參數傳入一個Context數組結構實體呢?試試看用多個泛型生命周期參數吧!

struct Context<'a>(&'a str);

struct Parser<'c, 's> {
    context: &'c Context<'s>,
}

impl<'c, 's> Parser<'c, 's> {
    fn parse(&self) -> Result<(), &'s str> {
        Err(&self.context.0[1..])
    }
}

fn parse_context(context: Context) -> Result<(), &str> {
    Parser { context: &context }.parse()
}

fn main() {
    parse_context(Context("Hello World")).unwrap();
}

以上程式看起來似乎可行,但現階段依然是會編譯失敗,原因在於我們必須要確定程式第4行的「's」生命周期會活的比「'c」生命周期還要久。重點來了,我們現在要想辦法定義「's」生命周期比「'c」生命周期還要長,要如何使用生命周期的子型別來控制呢?很簡單,就像是在定義變數的型別一樣,在一個生命周期的名稱後面使用冒號「:」,決定其至少要涵蓋哪個另外的生命周期。以上程式再改寫如下:

struct Context<'a>(&'a str);

struct Parser<'c, 's: 'c> {
    context: &'c Context<'s>,
}

impl<'c, 's: 'c> Parser<'c, 's> {
    fn parse(&self) -> Result<(), &'s str> {
        Err(&self.context.0[1..])
    }
}


fn parse_context(context: Context) -> Result<(), &str> {
    Parser { context: &context }.parse()
}

fn main() {
    parse_context(Context("Hello World")).unwrap();
}

如此一來程式就可以編譯成功了!

替泛型型別參數指定生命周期

先舉個例子說明。我們之前有已經使用過Ref結構體了,現在請先忽略它。若我們想要實作出自己的Ref結構體,應該要怎麼做呢?Ref結構體應該會需要儲存某個任意值的參考,也就是說,我們必須用到泛型來表示任意型別,也需要一個欄位來儲存參考。在此我們選擇使用數組結構體,將其定義如下:

struct Ref<'a, T>(&'a T);

嘗試編譯一下,會發現以上程式會有生命周期的問題,因為泛型型別「T」可以是一個任意的型別,這個型別可能也會有參考型別的欄位,因此我們必須要確保泛型型別「T」的實體,其所儲存的參考的生命周期,是比「'a」生命周期還要長的。我們可以在原先要限制泛型型別參數所代表的型別必須要實作的特性的地方,直接加上特定的生命周期參數,限制該泛型型別參數的生命周期必定不會小於該特定的生命周期。程式可以改寫如下:

struct Ref<'a, T: 'a>(&'a T);

這樣一來程式就可以通過編譯了。

另外,我們也可以改為使用「'static」生命周期來做為泛型型別的生命周期限制。如此一來,就會限制該型別的實體不能夠含有非「'static」生命周期的參考型別欄位。

舉例來說:

#[derive(Debug)]
struct Ref<'a, T: 'a>(&'a T);

#[derive(Debug)]
struct StaticRef<'a, T: 'static>(&'a T);

#[derive(Debug)]
struct NoRefStruct {
    value: i32
}

#[derive(Debug)]
struct OneRefStruct<'a> {
    text: &'a String
}

#[derive(Debug)]
struct StrRefStruct {
    text: &'static str
}

fn main() {
    let no_ref = NoRefStruct {
        value: 100
    };
    let one_ref = OneRefStruct {
        text: &String::from("Hello!")
    };
    let str_ref = StrRefStruct {
        text: "Hello!"
    };

    let ref_no_ref = Ref(&no_ref);
    let static_ref_no_ref = StaticRef(&no_ref);
    let ref_one_ref = Ref(&one_ref);
//    let static_ref_one_ref = StaticRef(&one_ref); // compile error
    let ref_str_ref = Ref(&str_ref);
    let static_ref_str_ref = StaticRef(&str_ref);

    println!("{:?}", ref_no_ref);
    println!("{:?}", static_ref_no_ref);
    println!("{:?}", ref_one_ref);
    println!("{:?}", ref_str_ref);
    println!("{:?}", static_ref_str_ref);
}

以上程式,「OneRefStruct」結構實體無法產生出「StaticRef」結構實體,因為其含有非「'static」生命周期的參考型別欄位。

推論特性物件的生命周期

先看看以下程式碼吧!

struct OneRefStruct<'a> {
    text: &'a String
}

trait MyTrait {}

impl<'a> MyTrait for OneRefStruct<'a> {}

struct MyStruct<'a> {
    obj: Box<OneRefStruct<'a>>
}

fn main() {
    let one_ref = OneRefStruct {
        text: &String::from("Hello!")
    };

    let s = MyStruct {
        obj: Box::new(one_ref)
    };
}

以上程式可以通過編譯。「OneRefStruct」結構體有一個「text」欄位,可以存放一個String結構實體的參考。「MyStruct」結構體有一個「obj」欄位,可以存放一個Box結構實體。

如果我們修改「MyStruct」結構體的「obj」欄位,使其儲存「MyTrait」特性的特性物件的話。程式如下:

struct OneRefStruct<'a> {
    text: &'a String
}

trait MyTrait {}

impl<'a> MyTrait for OneRefStruct<'a> {}

struct MyStruct {
    obj: Box<MyTrait>
}

fn main() {
    let one_ref = OneRefStruct {
        text: &String::from("Hello!")
    };

    let s = MyStruct {
        obj: Box::new(one_ref)
    };
}

在進行修改的時候,我們就會發現到一個奇怪的地方,原本的「'a」生命周期好像沒有必要加進去了!?然而,雖然我們修改的部份(程式第9行到第11行)可以通過編譯,但是在程式第19行卻編譯失敗了。

雖然我們沒有替「MyStruct」結構體的「Box<MyTrait>」型別定義生命周期,但Rust的編譯器會自動依照以下的規則進行特性物件所指到的實體的生命周期推論:

1. 智慧型指標的特性物件,如「Box<T>」,其預設的生命周期為「'static」。
2. 參考的特性物件,如「&'a T」或「&'a mut T」,其預設的生命周期為「'a」。
3. 如果特性本身有泛型參數,且有一個泛型型別參數有指定生命周期,如「T<'a, K: 'a>」,則其生命周期為「'a」。
4. 如果特性本身有泛型參數,且有兩個以上的泛型型別參數有指定生命周期,如「T<'a, 'b, K: 'a, V: 'b>」,則無法推論其生命周期,開發者必須明確地用「&'a (T + 'a)」、「&'a mut (T + 'a)」、「Box<T + 'a>」這些方式來指定。

以這個例子來說,由於「MyTrait」沒有泛型參數,所以「Box<MyTrait>」這種特性物件所指到的有實作「MyTrait」特性的實體,其生命周期為「'static」。換句話說,「Box<MyTrait>」會被推論為「Box<MyTrait + 'static>」。由於「&String::from("Hello!")」的生命周期並不是「'static」,因此「one_ref」所儲存的「OneRefStruct」結構實體,無法被用來產生「Box<MyTrait>」特性物件。

如果要讓程式順利通過編譯,我們依然會需要替「MyStruct」結構體加上泛型生命周期參數,來讓「Box<MyTrait>」特性物件能夠明確地指定生命周期,而不會被編譯器自動推論為「Box<MyTrait + 'static>」。修改後的程式碼如下:

struct OneRefStruct<'a> {
    text: &'a String
}

trait MyTrait {}

impl<'a> MyTrait for OneRefStruct<'a> {}

struct MyStruct<'a> {
    obj: Box<MyTrait + 'a>
}

fn main() {
    let one_ref = OneRefStruct {
        text: &String::from("Hello!")
    };

    let s = MyStruct {
        obj: Box::new(one_ref)
    };
}

結論

這個章節中,我們學到了利用「生命周期的子型別」和「替泛型型別參數指定生命周期」來限制生命周期的長短,以及特性物件搭配生命周期的使用方式。

下一章節,我們將會學習進階的特性用法。

下一章:進階的特性用法