關於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();
}
這個程式會編譯失敗,因為程式第14行,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 }.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);
以上程式雖然可以編譯(舊版Rust不行),但試想一下可以發現,泛型型別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); // compilation 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<dyn 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<dyn MyTrait>
型別定義生命周期,但Rust的編譯器會自動依照以下的規則進行特性物件所指到的實體的生命周期推論:
Box<dyn T>
,其T
預設的生命周期為'static
。2. 參考的特性物件,如
&'a dyn T
或&'a mut dyn T
,其T
預設的生命周期為'a
。以這個例子來說,由於MyTrait
沒有泛型參數,所以Box<dyn MyTrait>
這種特性物件所指到的有實作MyTrait
特性的實體,其生命周期為'static
。換句話說,Box<dyn MyTrait>
會被推論為Box<dyn MyTrait + 'static>
。由於&String::from("Hello!")
的生命周期並不是'static
,因此one_ref
所儲存的OneRefStruct
結構實體,無法被用來產生Box<dyn 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<dyn MyTrait + 'a>,
}
fn main() {
let one_ref = OneRefStruct { text: &String::from("Hello!") };
let s = MyStruct { obj: Box::new(one_ref) };
}
以上程式也可以改寫成:
struct OneRefStruct<'a> {
text: &'a String,
}
trait MyTrait {}
impl<'a> MyTrait for OneRefStruct<'a> {}
struct MyStruct<'a> {
obj: &'a dyn MyTrait,
}
fn main() {
let one_ref = OneRefStruct { text: &String::from("Hello!") };
let s = MyStruct { obj: &one_ref };
}
結論
這個章節中,我們學到了利用「生命周期的子型別」和「替泛型型別參數指定生命周期」來限制生命周期的長短,以及特性物件搭配生命周期的使用方式。
下一章節,我們將會學習進階的特性用法。
下一章:進階的特性用法。