Rust 高階程式設計 變性的直觀解釋
協變
在Rust中是一個今人迷惑的話題。對於新手可能感知不到這些概念。本篇內容將給出一個更通俗的理解。
首先, 有兩個重要的點我們要關注一下。
變性是一個與通用引數
T
有關的概念
Rust的生命週期引數(‘a)具有子型別化的設計
?關於子型別是什麼,如果你學習過其它高階語言,就很容易理解。
?關於生命週期,你可以理解為一個變數在棧上什麼時候有效,沒被釋放。更通俗的來說。 就是
一個作用域
,
程式碼塊
。
fn
main
()
{
// 這{}括號包起來的,就是一個作用域,叫行生命週期範圍也行。
// 在括號內的區域性棧變數只在這個括號內有效,
}
// ———————— 結束
Rust中的型變就是允許對一個生命週期引數進行如何的變化:
壽命變短:協變 (子型別向基類進化。縮減作用域。)
壽命變長:逆變 (基類向子型別進化,聽進來很荒謬,擴大生命週期,這時危險的)
沒有關係:不變 (不允許協變與逆變,只允許替換成為相同的型別)
注意:變性這些概念實際上是我們需要做的理論假設, 這樣我們才能確保我們的實現是正確的。不然, 程式會炸,出現各種無效指標。
在Rust中無論你在哪裡看到變性,預設情況下,它表示協變。
現在這裡有一個經典的例子。
先試著猜測輸出結果:
struct
MyCell
<
T
>
{
value
:
T
}
impl
<
T
>
MyCell
<
T
>
{
fn
new
(
value
:
T
)
->
Self
{
MyCell
{
value
}
}
fn
get
(
&
self
)
->
&
T
{
&
self
。
value
}
fn
set
(
&
self
,
new_value
:
T
)
{
// 注意這裡,方法獲取的是一個不可變的例項(&self)不是&mut self。如果你改成了。那就不會
// 有問題了。 編譯時會提示new_value生命週期不足
// 我們需要使用不安全的記憶體寫入方法,製造這個’BUG‘
unsafe
{
std
::
ptr
::
write
(
&
self
。
value
as
*
const
_
as
*
mut
T
,
new_value
);
}
}
}
fn
set_value
(
source
:
&
MyCell
<&
i32
>
)
{
println
!
(
“舊的值是:{}”
,
source
。
get
());
let
bugvar
=
100
;
source
。
set
(
&
bugvar
);
}
fn
main
()
{
let
a
=
MyCell
::
new
(
&
10
);
set_value
(
&
a
);
println
!
(
“現在的值是:{}”
,
a
。
get
());
}
// 最終輸出:
// 舊的值是:10
// 現在的值是:-65374792
Rust忽然變的記憶體不安全了。 造成這種原因的。就是今天所說的
變性。
我們展開一下類似編譯的MIR風格程式碼解析一下問題所在。
’a
:
{
let
a
=
MyCell
::
new
(
&
‘a
10
);
’b
:
{
let
bugvar
:
i32
=
100
;
// <—— 這個BUG變數建立在這裡
‘c
:
{
’d
{
source
。
set
(
&
‘d
bugvar
);
}
}
// <—— bufvar 釋放在這裡
}
println
!
(
“end value: {}”
,
cell
。
value
);
}
問題的根源在於,我們允許
改變
了MyCell裡包裹的val。
(重點:*這種關係,MyCell對於val來說。是
協變
)
我們把生命週期只有
'b
範圍的變數bugvar,強行塞進了生命週期比它更長的
'a
範圍裡。那麼一但較短的那個離開了作用域。那麼更長的變數卻儲存著他。 結果就是:
炸
指向了一個無效的地址。所以,這就相當於, 我借了100萬給你, 1個月後。 你竟然說是我欠了你100萬。你說會不會炸? (你不經我同意單方面修改了內部的值)
這段程式碼的生命週期關係:a > b > c > d
劃重點,如果要讓MyCell包裹的值有效,那麼必須裹入一個至少同樣>=a的作用域
那如何告訴編譯器,對MyCell做一些限制呢。
先參考Cell原始碼,核心在於多了一條
#[lang = “unsafe_cell”]
編譯器會認為這是一個不安全的塊,限制你傳入的引數生命週期必須和Cell一樣或更長,否則 編譯報錯。不過
#[lang = “unsafe_cell”]
這條屬性並不對外開放,語言內部使用, 你只有透過其它方式標記我們的MyCell了
pub
struct
Cell
<
T
:
?
Sized
>
{
value
:
UnsafeCell
<
T
>
,
}
#[lang =
“unsafe_cell”
]
// ——> 只是比我們寫的MyCell多了一條這個屬性
pub
struct
UnsafeCell
<
T
:
?
Sized
>
{
value
:
T
,
}
在標準庫中
,有一個叫幽靈資料
PhantomData
,它用於模擬另一個型別,Rust在編譯時會檢查這個標誌,讓MyCell模擬一個特性
struct
MyCell
<
T
>
{
value
:
T
,
_pd
:
std
::
marker
::
PhantomData
<
std
::
cell
::
UnsafeCell
<
T
>>
}
這裡MyCell模擬了UnsafeCell。變相加上了
#[lang = “unsafe_cell”]
這條屬性讓MyCell的變性保持
不變
。到此。你編譯時就得到一個生命週期不足的錯誤,自己動手試試吧。
變性 整體上來說。 就是一個容器與被包裹值的一種關係
。
變性只是一個概念,不要過度理解造成負擔,你可以想成其實啥也不是,就是一種我們假設的理論性規範, 遵守它, 程式才能正確的執行。不遵守。程式就是一個字: 炸
變性還有
逆變
與
不變
,之後有機會將繼續