喜歡的話別忘了點贊、收藏加關注哦(加關注即可閱讀全文),對接下來的教程有興趣的可以關注專欄。謝謝喵!(=・ω・=)
這篇文章在Rust初級教程的基礎上對生命周期這一概念進行了補充,建議先看【Rust自學】專欄的第10章的文章。
1.12.1. 生命周期變型(Lifetime Variance)
變型(Variance)是Rust類型系統中的一個概念,它描述了泛型參數(特別是生命周期參數)在類型層次結構中的繼承關系。
我們可以簡單地理解為變型是用于描述哪些類型是其他類型的“子類”的,這里的“子類”比較類似于Java和C#中的子類。
除此以外,變型還會關注什么時候“子類”能夠替換“超類”(反之亦然)。
通常來說,如果A是B的子類,那么A至少和B一樣有用。舉一個Rust語言的例子:如果函數接收&'a str
的參數,那么就可以傳入&'static str
的參數。因為'static
是'a
的子類,'static
至少跟任何'a
存活的時間一樣長('static
能夠在程序運行中一直保持有效)。
1.12.2. 生命周期的3種變型
所有類型都有變型,每個類型所對應的變型定義了哪些類似類型可以用在該類型的位置上。
注意:以下的內容比較難,建議你先回憶一下高中學的充分條件、必要條件等知識
1. 協變(covariant)
協變(covariant)指的是某類型只能用“子類型”來代替。
協變表示:
如果 A <: B(A是B的子類型),那么 F<A> <: F<B>(F<A>也是F<B>的子類型)
這是一個從小到大繼承關系的傳遞,類似于充分條件的推導:如果 A 成立,則 B 一定成立(A 是 B 的充分條件)。
例如&'static T
可以代替&'a T
,因為&T
是對'a
這個生命周期的協變,所以'a
可以由它的子類(比如說'static
)來替代。
2. 不變(invariant)
不變(invariant)意味著必須提供指定的類型。
不變表示:
A <: B 不能推導出 F<A> <: F<B> 也不能 F<B> <: F<A>
這意味著F<A>
和F<B>
之間沒有足夠的關系,無法形成推導關系,因此它們既不是充分條件,也不是必要條件,它們是獨立的。
比如說&mut T
這個可變引用對于T
來說就是不變的。
3. 逆變(contravariant)
逆變表示:
如果 A <: B(A是B的子類型),那么 F<B> <: F<A>(F<B>反而是F<A>的子類型)
這里的邏輯是 “想要F<A>
成立,B必須滿足A的條件”,更像必要條件:如果 B 成立,則 A 也必須成立(A 是 B 的必要條件)。
你可以把逆變理解為“成反比”:函數吧對參數的要求越低,參數可發揮的作用越大。
舉兩個例子:
-
假如有兩個變量
x1
和x2
,其中x1
的生命周期是'static
,x2
的生命周期是'a
。那么毫無疑問x1
的作用比x2
大,因為它活得更長。 -
假如有兩個函數
take_func1
和take_func2
,其中take_func1
接收的參數是&'static str
,take_func2
接收的參數是&'a str
。毫無疑問,take_func1
對參數的要求比take_func2
嚴格,這就導致了take_func1
沒有take_func2
作用大。
由上述的兩個例子看出,給變量聲明一個更長的生命周期會使它的作用更大,但是要求函數的參數是更長的生命周期就會使函數的作用更小。這就是所謂的逆變。
那么這叫誰和誰的逆變呢?就是函數對它里面的參數類型的逆變。
1.12.3. 生命周期變型的作用
我們通過一個例子來看一下生命周期變型的作用:
struct MutStr<'a, 'b> { s: &'a mut &'b str,
} fn main() { let mut s = "hello"; *MutStr { s: &mut s }.s = "world"; println!("{}", s);
}
這個代碼令人比較困惑的地方在于MutStr
這個結構體,我們來解析一下:
- 這個結構體只有一個字段,但是擁有兩個生命周期
&'a mut
表示 一個可變引用,這個可變引用的生命周期是'a
&'b str
表示 一個字符串切片的引用,這個字符串切片的生命周期是'b
- 換句話說,
MutStr
允許你存儲一個可變引用,這個引用指向一個字符串切片的引用。你可以修改s
本身,但不能修改&'b str
指向的字符串內容
接下來我們來看一下主函數的邏輯:
-
let mut s = "hello";
聲明了s
這個變量,類型是&str
,值是"hello" -
*MutStr { s: &mut s }.s = "world";
這其實是好幾步被合在了一行,我們分開來看:MutStr { s: &mut s }
傳遞給MutStr
結構體s
的可變引用,此時MutStr
下的s
字段的值就是"hello"*MutStr { s: &mut s }.s = "world";
的.s
表示訪問s
字段(此時這個字段的值是&mut s
)。*
解引用s
,即獲得s
這個字符串切片的引用本身。= "hello"
修改了指向的值——s
之前指向"hello",現在被修改為"world",即s = "world"
。
那如果只有一個生命周期還能這么寫嗎?
struct MutStr<'a> { s: &'a mut str,
} fn main() { let mut s = "hello"; *MutStr { s: &mut s }.s = "world"; println!("{}", s);
}
這段代碼的問題在于類型不匹配和 Rust 中可變引用的不變性(invariance)。
具體來說:
- 變量
s
的類型是&str
(引用一個字符串切片)。 - 當寫
&mut s
時,其類型實際上是&mut &str
,即對變量s
的可變引用。然而,結構體MutStr
的定義要求字段s
的類型&mut str
。
雖然不可變引用(&T
)支持“unsizing coercion”(例如可以將&[T; N]
自動轉換為&[T]
),但是 可變引用 (&mut T
) 是不變(invariant)的,這意味著Rust不允許自動將&mut &str
轉換為&mut str
。
因此,編譯器會報類型不匹配的錯誤,因為它無法將&mut s
(類型&mut &str
)傳給MutStr { s: ... }
期望的&mut str
參數。
在這里解釋一下不變性:
- 不可變引用 (
&T
) 是協變(variant)的:允許自動的unsizing轉換,比如可以將&[T; N]
當作&[T]
使用 - 可變引用 (
&mut T
) 是不變(invariant)的:這就意味著&mut &str
與&mut str
被視為完全不同的類型,編譯器不會自動進行轉換
也可以這么理解:
- 字符串字面值("hello"和"world"就是字符串字面值)是
&str
類型,有隱式的'static
生命周期標注,也就是說&str
實際上是&'static str
。原本的結構體里的'b
對應它 - 結構體的’a對應可變引用的生命周期,也就是
*MutStr { s: &mut s }.s = "world"
這一行中的&mut
這個可變引用的生命周期 - 修改之后的這個只有一個泛型生命周期的結構體無法同時對應這兩個生命周期標注,因為可變引用
&mut T
是不變(invariant)的,&mut &str
與&mut str
被視為完全不同的類型,所以導致了報錯