從程序的內存管理說起
所(suo)有(you)的程(cheng)(cheng)序都必須(xu)和(he)(he)計(ji)(ji)(ji)算機內(nei)(nei)存(cun)(cun)打(da)交道,如(ru)何從(cong)內(nei)(nei)存(cun)(cun)中申請(qing)空間(jian)來存(cun)(cun)放(fang)程(cheng)(cheng)序的運行內(nei)(nei)容,如(ru)何在(zai)不需要(yao)的時候釋放(fang)這(zhe)些空間(jian),成了(le)重中之(zhi)重,也是所(suo)有(you)編程(cheng)(cheng)語言設計(ji)(ji)(ji)的難點之(zhi)一。在(zai)計(ji)(ji)(ji)算機語言不斷演變(bian)過(guo)程(cheng)(cheng)中,先后出(chu)現(xian)過(guo)兩種主要(yao)的內(nei)(nei)存(cun)(cun)管(guan)理模式:手(shou)動內(nei)(nei)存(cun)(cun)管(guan)理(由程(cheng)(cheng)序員通過(guo)編碼申請(qing)和(he)(he)釋放(fang)內(nei)(nei)存(cun)(cun),典型(xing)代表C,C++),通過(guo)垃圾回收機制(zhi)實施(shi)的自動內(nei)(nei)存(cun)(cun)管(guan)理(典型(xing)代表Java,Golang)。
手動內存管理
在程序中,通過函數調用的方式(shi)來申請(qing)和釋(shi)放內存。其特征(zheng)在于:
(1)程(cheng)序員可以控(kong)(kong)制(zhi)內(nei)存(cun)分(fen)配、使(shi)用、釋(shi)放(fang)(fang)的(de)各個環節,控(kong)(kong)制(zhi)粒度可以做到(dao)很細;另一(yi)方(fang)面(mian),也要求程(cheng)序員對內(nei)存(cun)分(fen)配是否(fou)成功、數(shu)(shu)據(ju)初始化、內(nei)存(cun)使(shi)用、數(shu)(shu)據(ju)共享、數(shu)(shu)據(ju)競爭(zheng)、數(shu)(shu)據(ju)銷(xiao)毀(hui)、釋(shi)放(fang)(fang)等(deng)做全面(mian)且細致的(de)管(guan)控(kong)(kong),否(fou)則就可能(neng)引入內(nei)存(cun)安全問題(ti)。
(2) C/C++語言(yan)支持隱式(shi)類(lei)型轉換,指(zhi)針運算,數組(zu)和(he)指(zhi)針轉換,這為程序使用內存(cun)提供了巨大(da)的靈活性;另一方(fang)面(mian),也極易引(yin)入訪問越界、類(lei)型錯(cuo)誤(wu)(或數據溢(yi)出)、無效地址(zhi)訪問等內存(cun)安全問題。
總結而言,采用(yong)手動(dong)內(nei)存管理(li)方式的編(bian)程(cheng)語言,極度依賴程(cheng)序(xu)員(yuan)的編(bian)程(cheng)經(jing)驗(yan),導(dao)致整體程(cheng)序(xu)質量參差不齊。遺憾(han)的是(shi),那怕是(shi)經(jing)驗(yan)豐(feng)富的程(cheng)序(xu)員(yuan),也有考慮不周(zhou)的情況,稍不留神(shen)就(jiu)會引入內(nei)存安全問題。
垃圾回收機制(運行時GC)
垃圾(ji)回收(shou)機(ji)制(GC),在程(cheng)(cheng)序運行時(shi)不(bu)斷尋找不(bu)再使用的內(nei)存(cun)(cun),并在合適(shi)的時(shi)機(ji)將其釋(shi)放。擁有(you)垃圾(ji)回收(shou)機(ji)制的編程(cheng)(cheng)語言,在內(nei)存(cun)(cun)安全管理方面有(you)巨(ju)大(da)(da)提升:大(da)(da)幅(fu)度降低了空指(zhi)針、野指(zhi)針、懸(xuan)垂指(zhi)針訪問(wen)的異常(chang)情況,大(da)(da)幅(fu)度降低內(nei)存(cun)(cun)泄漏的風險。
但是,引入垃圾回收后(hou),有(you)引入了新的問題(ti):
(1)性(xing)能開銷(xiao):垃(la)圾回(hui)收(shou)機制需(xu)要額外的(de)系統資源和計算開銷(xiao)來進(jin)行垃(la)圾收(shou)集,這可能會影響程(cheng)序的(de)性(xing)能,尤其(qi)是對(dui)于實時(shi)性(xing)要求較高的(de)應用(yong)程(cheng)序。
(2)不確定(ding)的(de)(de)回(hui)收時(shi)(shi)間:垃圾回(hui)收的(de)(de)時(shi)(shi)間是不確定(ding)的(de)(de),并(bing)且垃圾回(hui)收可能會在(zai)程序(xu)運行時(shi)(shi)導致(zhi)停頓(STOP THE WORLD),影響用(yong)戶(hu)體驗或者系統的(de)(de)實(shi)時(shi)(shi)性(在(zai)一些實(shi)時(shi)(shi)要(yao)求極高(gao)的(de)(de)應用(yong)中,這種不可預期的(de)(de)停頓是不可接受的(de)(de))。
(3)系統(tong)資源占用(yong)變高(gao)或者出現波峰(feng):由于垃(la)(la)圾(ji)回收機制通常(chang)需(xu)(xu)要維護一些附(fu)加數據(ju)結構(gou)來跟蹤對象的引用(yong)關系,可(ke)能(neng)帶(dai)來額外的內存開銷;同(tong)時,在(zai)執行(xing)垃(la)(la)圾(ji)回收時,通常(chang)需(xu)(xu)要經過復雜的計算進行(xing)目(mu)標(biao)篩選,這(zhe)可(ke)能(neng)出現CPU資源消耗飚高(gao)。
總結而言,采用垃圾回收機(ji)制的(de)編(bian)程(cheng)語言,在(zai)內存(cun)安全提升方面(mian)效(xiao)果(guo)是顯著的(de);但(dan)會引(yin)(yin)入一(yi)些系(xi)統(tong)資(zi)源、系(xi)統(tong)性能、系(xi)統(tong)響應即時(shi)性方面(mian)的(de)消耗或(huo)開銷,在(zai)一(yi)些特定場景中引(yin)(yin)入問題。
Rust的所有權機制
作為編程語言(yan)的后起之秀,Rust站在巨(ju)人(ren)(們)的肩膀上,審視著前(qian)人(ren)(們)走過(guo)的坎(kan)坎(kan)坷坷!GC這(zhe)把重劍他(ta)拿起又放下......
多少次徘(pai)徊在圖(tu)(tu)書(shu)館(guan)(guan)里,希望(wang)從知識的(de)(de)海洋里找到新的(de)(de)方向和真(zhen)理。突然(ran)某天發現,他尋求的(de)(de)真(zhen)理就在圖(tu)(tu)書(shu)館(guan)(guan)的(de)(de)管理機制里(以圖(tu)(tu)書(shu)借用類比(bi)內存管理)!
你看:
C/C++館
借閱規則: 大(da)家自由取閱(yue)(yue),閱(yue)(yue)讀后自行(xing)歸還到書(shu)架相應的位置。
現狀:
(1) 大部分人自取、自閱、閱讀完歸(gui)還到書(shu)架指(zhi)定位置;少部分人忘記歸(gui)還,將書(shu)籍遺留在(zai)讀書(shu)大廳,閉館時由管理員統(tong)一整(zheng)理(進程銷毀回收)!
(2) A借閱后,給B傳(chuan)閱,而后傳(chuan)閱給C、D、E,最終由E還回 (共享)。
(3) A借閱后,B告訴A先不要(yao)還,一會他要(yao)看;結果(guo)B沒有看、也沒有還,這本書(shu)遺留(liu)在讀書(shu)大廳(泄漏)!
(4) A借閱后,傳閱 B,C,D, 隨后D將書還回,A離(li)開時準備去還書,卻發(fa)現書找不到了(le),急得上蹦下串(重(zhong)復釋(shi)放)!
(5) A借閱(yue)后(hou),與(yu)B共閱(yue), A 三下(xia)五除二看完,然后(hou)將(jiang)書歸還(huan),留下(xia)B一臉懵(meng)逼(懸垂訪問)!
(6) A告訴B說有(you)本精(jing)裝《詩經》帶(dai)插圖和注(zhu)解,非常適合陶冶情(qing)操(cao),已經借(jie)出放在site108; B到位置上(shang)查閱,是一本《金瓶梅》 (野指針,臟(zang)數據)!
觀感:過(guo)度自由,圖書管理不善,經常失(shi)控。
Java/Golang館
借閱規則: 大(da)家自(zi)由取閱,在座位上(shang)閱讀,離席(xi)后管理(li)員收拾圖書;離席(xi)保留(liu)圖書請留(liu)下標簽。
現狀:
(1)大(da)家(jia)都是自(zi)取圖書,找位置(zhi)落座(zuo)閱讀(du),閱讀(du)完成后離開即可;
(2)圖書(shu)館(guan)有(you)管(guan)理員會不定期巡邏,發現(xian)空座位上的圖書(shu)會自動收(shou)走(zou),放(fang)回書(shu)架(jia);
(3)A借閱 book1, 而后去借book2, 為了保(bao)留book1,需(xu)要在座位(wei)上立一個(ge)暫留的牌子(zi)(引(yin)用(yong)保(bao)留);
(4)A借閱(yue) book1,放置在site108;告(gao)訴B可以去查閱(yue);B 到site108發現書已(yi)經(jing)被(bei)收走了(地(di)址(zhi)引用(yong)無效)!
(5)A正(zheng)在專(zhuan)注閱讀,管理員過來要求他(ta)站起來一下,以(yi)方便(bian)收(shou)取附近閑置的書籍(GC阻斷(duan))!
觀感:圖書管(guan)理妥善,只(zhi)是來回(hui)穿梭勞(lao)作(zuo)的管(guan)理員(yuan),破壞(huai)了這安靜學習(xi)的氛(fen)圍。
Rust琢磨,如果我管理這個(ge)(ge)圖(tu)書館,讓每(mei)個(ge)(ge)人每(mei)次借一本(ben)圖(tu)書并登記;閱(yue)讀完畢后,舉手示意(yi),管理員進行登記注銷,并回(hui)收圖(tu)書。如此,圖(tu)書借閱(yue)、歸(gui)還(huan)不(bu)就(jiu)變得井然有序(xu)了?
類比于此,在內存管理時,如果能夠確保每次分配的內存,都由一個變量來唯一擁有;那么,當這個唯一擁有該內存的變量達到其生命周期的盡頭時,這塊內存就可以自動釋放(并且必須釋放)!如此這般,就可以實現內存安全管理、自動回收,并且不引入垃圾回收機制所帶來的新問題!
一試之下,果然靈應!
而這個機制,Rust將其稱為所有權!
理解Rust所有權機制
經過上一章節說明(ming),我(wo)們明(ming)白了Rust引入所有權機(ji)制所解決的核心問題(ti)是:在無GC的前提下,可以做(zuo)到自動內存(cun)回(hui)收! 即保(bao)證編(bian)程過程中內存(cun)安全,又(you)不會引入傳(chuan)統GC機(ji)制所帶(dai)來的負面效果。
接下(xia)來(lai),我們將(jiang)更加詳細說(shuo)明,Rust所有權機制的規則和使用。
所有權規則
進一(yi)步總(zong)結(jie),Rust的所有權機制包括三條核(he)心(xin)規則(ze):
- Rust 中每一個值都被一個變量所擁有,該變量被稱為值的所有者
- 一個值同時只能被一個變量所擁有,或者說一個值只能擁有一個所有者
- 當所有者(變量)離開作用域范圍時,這個值將被丟棄(drop)
變量綁定
先看下面的例子:
fn var_bind_string() {
let mut vstr = String::new();
vstr.push_str("origin data");
let mut nstr = vstr;
nstr.push_str(" append something");
println!("check the string values:: first ={}, updated={}", vstr, nstr);
}
示例中(zhong),創建了一個字(zi)符串變量(動態分配(pei))vstr,并更新vstr的(de)內(nei)容為 “origin data”;
而(er)后(hou),創建新變量nstr,并將(jiang)vstr “賦值”給nstr,并更新nstr的內容,追(zhui)加“append something”;
最(zui)后,嘗試打(da)印vstr,nstr的內容。
乍一(yi)看(kan),這個(ge)代碼沒(mei)有什么問題;有編(bian)程經驗的童鞋,可能還在(zai)考慮 vstr 和nstr 是否一(yi)致(nstr 和vstr指向同一(yi)塊(kuai)(kuai)地址(zhi),內(nei)容一(yi)致?nstr基(ji)于(yu)vstr做(zuo)了(le)深度拷貝,是兩塊(kuai)(kuai)獨立的內(nei)存地址(zhi)?)......
然后,現實是,nstr和vstr 確(que)實指向(xiang)同一塊內存(cun)地址,但二(er)者并不能同時存(cun)在:

通過idel預(yu)編譯(yi)提示,當我們將vstr “賦值”給nstr之后,nstr已經被“moved”,已經不可(ke)用了(le),當我們嘗試訪問(wen)它(ta)時就會(hui)報錯!
下(xia)圖展示變(bian)量的內存結(jie)構變(bian)化:

由此可見:
(1)當我們將一個包含內存分配的變量,“賦值”給另一個變量時,相應的內存所有權發生了轉移,由此來保證“一個值只能被一個變量所擁有”;
(2) rust中“=”的含義和行為相比于一般編程語言是有所區別的;在rust中,這個過程不再使用“賦值”這個術語,而使用“變量綁定”這個術語,更貼切的表述了內存所有權發生轉移的行為;
(3)特別需要注意的是,變量綁定不僅發生在塊邏輯中,也發生在函數調用過程中;因此,在函數調用時,需要謹慎處理入參和返回,需要注意是否發生了所有權移動;另外,函數傳遞時,根據需要,可以通過引用傳遞,來避免所有權轉移。

堆變量和棧變量
棧(zhan)和堆(dui)(dui)是(shi)編程(cheng)語言(yan)最核心的(de)(de)數據結構,但是(shi)在很多(duo)語言(yan)中,你(ni)并不需(xu)要(yao)深入(ru)了解棧(zhan)與堆(dui)(dui)(例如golang,你(ni)并不需(xu)要(yao)了解變量是(shi)在堆(dui)(dui)上(shang)(shang)分配(pei)還是(shi)在棧(zhan)上(shang)(shang)分配(pei),因為golang會自(zi)動轉換)。 但對于 Rust 這(zhe)樣的(de)(de)系統編程(cheng)語言(yan),值是(shi)位(wei)于棧(zhan)上(shang)(shang)還是(shi)堆(dui)(dui)上(shang)(shang)非常重要(yao), 因為這(zhe)會影響程(cheng)序的(de)(de)行為和性能(neng)。
如果讀者對堆(dui)和棧相關(guan)的(de)基礎知識(shi)(shi)不夠(gou)牢固,小(xiao)編強烈建議(yi)您去(qu)補習一下相關(guan)知識(shi)(shi),這對您了解(jie)rust相關(guan)機制大有裨(bi)益!
Rust的變(bian)量綁(bang)定在處理堆變(bian)量和棧變(bian)量時,也是不一樣的,如下(xia)圖:

由圖可見:
(1) b=a; 變(bian)量綁定后(hou),a依然(ran)可用;
(2) 而 str_d= str_c; 變(bian)量綁定后,str_c就(jiu)不可用(yong)了。
這是因為(wei) a,b都是棧變(bian)量,rust在(zai)處理棧變(bian)量“賦值”時,采(cai)用了copy方(fang)(fang)式(shi)(shi)(由于棧變(bian)量通(tong)常很小(xiao),copy數據量不大;而(er)且(qie)棧復制效(xiao)率也高,所(suo)有rust選(xuan)擇通(tong)過clone的(de)方(fang)(fang)式(shi)(shi)處理棧變(bian)量的(de)綁定)。而(er)String類型(xing)是堆變(bian)量,所(suo)以需要通(tong)過所(suo)有權(quan)轉移(yi)來滿足內存值的(de)所(suo)有權(quan)規則,使(shi)得系統能(neng)自動(dong)管理內存分配和回(hui)收!
因此,可以理解為rust的所有權規則其實就是針對堆分配的內存管理!
另外補充一點,rust在函數傳遞時,是值傳遞。這就可以解釋,為何堆變量在傳遞到函數后,所有權發生了轉移(值傳遞的函數調用,入參s 傳入后,在內部生成了 一個同名的局部變量s',函數體內用的其實是這個局部變量;等效于,函數調用時 發生 s'=s的變量綁定)!
引用與借用
前兩章節,我們詳(xiang)細解釋了(le)Rust的所有權機制(zhi)(zhi)的原理和實現,闡述(shu)了(le)所有權機制(zhi)(zhi)如(ru)何使(shi)Rust在沒有垃圾(ji)回收機制(zhi)(zhi)的前提下,實現了(le)自動化(hua)內(nei)存管理,從而(er)(er)提高了(le)內(nei)存安全。然而(er)(er),如(ru)果僅僅支持通過轉移(yi)所有權的方式獲(huo)取一(yi)(yi)個值,那會讓程序變得復雜(就(jiu)像上(shang)面示例的打印字符(fu)(fu)串函數一(yi)(yi)樣,只是(shi)調用了(le)一(yi)(yi)個打印,字符(fu)(fu)串變量(liang)就(jiu)變得不(bu)可(ke)用了(le))。 Rust 能否像其它編程語言一(yi)(yi)樣,使(shi)用某(mou)個變量(liang)的指(zhi)針或者引用呢(ni)?答案是(shi)可(ke)以。
Rust 也支持引(yin)用,是一(yi)個指向變量的地址,與其他(ta)語言(C,Golang)含義一(yi)致。
Rust 通過 借用(Borrowing) 這個概念來達成上述的目的,獲取變量的引用,稱之為借用(borrowing)。正如(ru)現實生活中,如(ru)果一個人擁有某樣(yang)東(dong)西,你可以從他那里(li)借來,當(dang)使(shi)用完(wan)畢(bi)后,也必須要物歸原主(zhu)。
改下print_stringx函數(shu)如下:

函數入參是(shi)一個字符串(chuan)引用(而不是(shi)字符串(chuan)實例(li)),這(zhe)樣函數調用就不會(hui)發生所(suo)有權(quan)轉移。在rust中,這(zhe)是(shi)常用的方式!

如(ru)上圖(tu),pstr就(jiu)是對vstr 的借(jie)用(yong)(不可變借(jie)用(yong)),此時pstr不擁有(you)內存(cun)數據(ju),但可以共享vstr的值(zhi)。
可變引用
上面的(de)例(li)子我(wo)們(men)(men)可以看見,通過(guo)借用,我(wo)們(men)(men)可以共享(xiang)變量的(de)值(zhi)。但這似乎還不夠,因為我(wo)們(men)(men)需要一(yi)些函數(shu)和接口來對數(shu)據進行加工(gong)(修改)。我(wo)們(men)(men)期望(wang)有這樣的(de)代(dai)碼:
fn append_string(dst: &String, data: &str) {
dst.push_str(data);
}

但(dan)上面的(de)代(dai)碼(ma)編(bian)(bian)譯(yi)時就(jiu)會報(bao)錯:無(wu)法將(jiang)dst引(yin)(yin)用(yong),借(jie)用(yong)為(wei)(wei)可寫的(de)借(jie)用(yong)!建議將(jiang)dst引(yin)(yin)用(yong)改(gai)變為(wei)(wei)可寫引(yin)(yin)用(yong)(Rust的(de)編(bian)(bian)譯(yi)器是真的(de)保姆級編(bian)(bian)譯(yi)器)。
調整如下
fn append_string(dst: &mut String, data: &str) {
dst.push_str(data);
}
fn test_modify_string() {
let mut vstr = String::from("data from heap"); // 注意,可變引用成立的前提是變量自身必須申明為可變
append_string(&mut vstr, "something to append");
println!("{}", vstr)
}
多個借用間的競態檢查
見下的例子:
fn append_string(dst: &mut String, data: &str) {
dst.push_str(data);
}
fn test_mutilp_borrow() {
let mut vstr = String::from("data from heap");
let pstr = &vstr;
print_stringx(&pstr);
let mstr = &mut vstr;
append_string(mstr, "append");
println!("{} {}", pstr, mstr);
}
測試(shi)程序(xu)嘗試(shi)對vstr進行:1打(da)印初始值,2,修改內(nei)容(rong);3,打(da)印修改后的(de)值。

見截圖(tu),程序編譯時(shi)(shi)報錯:嘗試將vstr 進行可變(bian)借(jie)用到mstr時(shi)(shi)異常,原因(yin)是vstr在這之前已(yi)經被pstr進行了不可變(bian)借(jie)用!
Rust在編譯時會進(jin)行(xing)(xing)(xing)數(shu)據(ju)(ju)競態檢查,確保流程(cheng)中沒(mei)有明顯的(de)數(shu)據(ju)(ju)競爭。例子中,pstr對vstr進(jin)行(xing)(xing)(xing)了不可變(bian)借用,其含義為在pstr的(de)生命(ming)周(zhou)期內,期望(wang)vstr保持不變(bian),以(yi)保證pstr對vstr數(shu)據(ju)(ju)引(yin)用的(de)一(yi)致性;但是mstr嘗(chang)試對vstr進(jin)行(xing)(xing)(xing)可變(bian)借用,那么意味著其生命(ming)周(zhou)期內可能對vstr進(jin)行(xing)(xing)(xing)修(xiu)改,并且mstr的(de)生命(ming)周(zhou)期與pstr的(de)生命(ming)周(zhou)期存在重疊;基于此,Rust編譯檢查器(qi),可以(yi)判定mstr的(de)可變(bian)借用會破(po)壞pstr的(de)數(shu)據(ju)(ju)一(yi)致性要求,因而(er)被禁止(zhi)!
Rust對于借用是數(shu)據(ju)競態的檢查要求為:
(1)確保不存在多個可變借用同時對數據進行同時修改的情況;即,同一時間,最多只能存在一個可變借用。
(2)當有不可變借用存在的時候,不允許進行可變借用;反之依然。
(3)可以同時存在多個不可變借用。
(4)借用期間,引用必須總是有效的(在)。
關于(yu)Rust如(ru)何保(bao)障在借用期間(jian),引用必須總是有效(xiao),我們將(jiang)開(kai)一(yi)個新的(de)主題——rust的(de)生命(ming)周期 進行(xing)詳細說明。
那就下回分解了老鐵(tie)!