亚欧色一区w666天堂,色情一区二区三区免费看,少妇特黄A片一区二区三区,亚洲人成网站999久久久综合,国产av熟女一区二区三区

  • 發布文章
  • 消息中心
點贊
收藏
評論
分享
原創(chuang)

Rust入門(四) —— 理解所有權

2024-03-28 01:35:25
52
0

從程序的內存管理說起

所(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):

  1. Rust 中每一個值都被一個變量所擁有,該變量被稱為值的所有者
  2. 一個值同時只能被一個變量所擁有,或者說一個值只能擁有一個所有者
  3. 當所有者(變量)離開作用域范圍時,這個值將被丟棄(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)!

0條評論
0 / 1000
huskar
20文(wen)章數
3粉絲(si)數
huskar
20 文章(zhang) | 3 粉絲
原創

Rust入門(四) —— 理解所有權

2024-03-28 01:35:25
52
0

從程序的內存管理說起

所(suo)(suo)有的(de)(de)程(cheng)序都(dou)必須和(he)計算機內存打交(jiao)道(dao),如何從內存中(zhong)申(shen)請(qing)空(kong)間(jian)來存放(fang)程(cheng)序的(de)(de)運行內容,如何在不(bu)需要的(de)(de)時候釋放(fang)這些空(kong)間(jian),成了重中(zhong)之重,也是所(suo)(suo)有編(bian)程(cheng)語言設計的(de)(de)難點之一(yi)。在計算機語言不(bu)斷演(yan)變過(guo)程(cheng)中(zhong),先后出現過(guo)兩種主(zhu)要的(de)(de)內存管(guan)理(li)模(mo)式(shi):手動內存管(guan)理(li)(由程(cheng)序員(yuan)通過(guo)編(bian)碼申(shen)請(qing)和(he)釋放(fang)內存,典型代(dai)表(biao)C,C++),通過(guo)垃圾回收機制實施的(de)(de)自動內存管(guan)理(li)(典型代(dai)表(biao)Java,Golang)。

手動內存管理

在(zai)程序(xu)中,通過函(han)數調用的(de)方式來申請和釋放內存。其特(te)征在(zai)于:

(1)程序(xu)員可以(yi)控(kong)制(zhi)內(nei)存分(fen)配、使用、釋放的各個環(huan)節,控(kong)制(zhi)粒度可以(yi)做(zuo)到很細;另(ling)一方面,也要求程序(xu)員對(dui)內(nei)存分(fen)配是否成功、數據初始化、內(nei)存使用、數據共享(xiang)、數據競爭、數據銷(xiao)毀、釋放等做(zuo)全(quan)面且(qie)細致的管控(kong),否則就(jiu)可能引入內(nei)存安(an)全(quan)問題。

(2) C/C++語(yu)言支持(chi)隱式類型轉換,指(zhi)針(zhen)運算,數組和指(zhi)針(zhen)轉換,這為程序使用內存提供了(le)巨大(da)的靈活性;另(ling)一方面,也(ye)極(ji)易引(yin)入(ru)訪問越界、類型錯誤(或數據(ju)溢出)、無效地址訪問等(deng)內存安全問題。

總結而言,采(cai)用手動內存(cun)管理方式的(de)(de)(de)編(bian)程(cheng)(cheng)語言,極度(du)依賴程(cheng)(cheng)序(xu)員的(de)(de)(de)編(bian)程(cheng)(cheng)經驗,導致整體(ti)程(cheng)(cheng)序(xu)質量參差不(bu)(bu)(bu)齊(qi)。遺憾的(de)(de)(de)是,那怕(pa)是經驗豐(feng)富的(de)(de)(de)程(cheng)(cheng)序(xu)員,也有考慮不(bu)(bu)(bu)周的(de)(de)(de)情況,稍不(bu)(bu)(bu)留神就(jiu)會引入內存(cun)安全問(wen)題。

垃圾回收機制(運行時GC)

垃圾回收機制(GC),在程(cheng)序(xu)運行時不(bu)斷尋找不(bu)再使用(yong)的內存,并在合(he)適的時機將其釋放。擁(yong)有(you)垃圾回收機制的編程(cheng)語言,在內存安全管理方面有(you)巨大(da)(da)提升:大(da)(da)幅度降低(di)了空指(zhi)針、野(ye)指(zhi)針、懸垂指(zhi)針訪問的異常情況,大(da)(da)幅度降低(di)內存泄漏的風(feng)險。

但是(shi),引入垃圾(ji)回收后(hou),有引入了新的問題:

(1)性能開銷:垃(la)圾回(hui)收機制需要額(e)外的系統(tong)資源和計算開銷來(lai)進行垃(la)圾收集,這可(ke)能會(hui)影響程序的性能,尤其是(shi)對于實時性要求較(jiao)高的應用程序。

(2)不(bu)(bu)確定的(de)(de)(de)回(hui)收時間(jian):垃圾回(hui)收的(de)(de)(de)時間(jian)是不(bu)(bu)確定的(de)(de)(de),并且垃圾回(hui)收可(ke)能(neng)會在程(cheng)序運行時導致(zhi)停(ting)頓(STOP THE WORLD),影(ying)響用(yong)戶(hu)體驗或者系統(tong)的(de)(de)(de)實時性(在一些實時要求極(ji)高的(de)(de)(de)應用(yong)中(zhong),這(zhe)種不(bu)(bu)可(ke)預(yu)期的(de)(de)(de)停(ting)頓是不(bu)(bu)可(ke)接受(shou)的(de)(de)(de))。

(3)系統資(zi)源(yuan)占(zhan)用(yong)變高或者出(chu)現波峰:由于垃(la)(la)圾回收機制通常需要(yao)維護一些附加數據結構來跟(gen)蹤對象(xiang)的引用(yong)關系,可能帶(dai)來額外的內(nei)存(cun)開(kai)銷;同時,在(zai)執行(xing)垃(la)(la)圾回收時,通常需要(yao)經過(guo)復雜(za)的計算進(jin)行(xing)目標篩選,這可能出(chu)現CPU資(zi)源(yuan)消耗(hao)飚高。

總結而言,采用垃圾(ji)回收機制(zhi)的(de)編程語言(yan),在內(nei)存(cun)安全提升方面效果(guo)是顯著的(de);但會引入(ru)一些系(xi)統(tong)資源、系(xi)統(tong)性(xing)能、系(xi)統(tong)響應(ying)即時性(xing)方面的(de)消(xiao)耗或開銷,在一些特定(ding)場景中引入(ru)問題。

Rust的所有權機制

作為編程語言的(de)后起之(zhi)秀,Rust站在(zai)巨人(ren)(們(men))的(de)肩膀上,審視著前(qian)人(ren)(們(men))走(zou)過的(de)坎(kan)坎(kan)坷坷!GC這把(ba)重劍他拿起又放下......

多少次徘徊在圖(tu)書(shu)館里,希望從知識的海洋里找(zhao)到新的方向和真理。突然某(mou)天發現,他尋求的真理就在圖(tu)書(shu)館的管(guan)理機制(zhi)里(以圖(tu)書(shu)借用類比內存管(guan)理)!

你看:

 

C/C++館

借閱規則: 大家自由取閱(yue),閱(yue)讀后自行(xing)歸還到(dao)書架相應的位置。

現狀

(1) 大部分人(ren)自取、自閱、閱讀完歸還到書架指定位置;少部分人(ren)忘記歸還,將(jiang)書籍遺(yi)留在(zai)讀書大廳,閉館時由管理(li)(li)員統一整(zheng)理(li)(li)(進程銷毀回收)!

(2) A借閱后,給B傳(chuan)(chuan)閱,而(er)后傳(chuan)(chuan)閱給C、D、E,最終由E還回 (共享)。

(3) A借(jie)閱后,B告訴A先不(bu)要還(huan),一會(hui)他要看;結果B沒有看、也沒有還(huan),這(zhe)本(ben)書(shu)(shu)遺留在讀書(shu)(shu)大(da)廳(泄漏)!

(4) A借閱后(hou),傳閱 B,C,D, 隨后(hou)D將書(shu)還(huan)回(hui),A離開時準備去還(huan)書(shu),卻(que)發現書(shu)找不(bu)到了(le),急得上蹦下串(chuan)(重復釋放)!

(5) A借(jie)閱(yue)后,與B共閱(yue), A 三下五(wu)除二看完,然后將書(shu)歸還,留下B一臉懵逼(bi)(懸垂訪問)!

(6) A告訴B說有(you)本精裝《詩經》帶插圖(tu)和注解,非常適合陶冶(ye)情操,已經借(jie)出放在(zai)site108; B到(dao)位置上查閱,是(shi)一本《金(jin)瓶梅》 (野(ye)指針,臟數據(ju))!

觀感:過度自由,圖書管理不(bu)善(shan),經常失控。

 

Java/Golang館

借閱規則: 大家自由取閱,在座位(wei)上閱讀,離席后管理(li)員收拾(shi)圖書;離席保留(liu)圖書請留(liu)下標簽(qian)。

現狀

(1)大(da)家(jia)都是自(zi)取圖書,找位(wei)置落座閱讀,閱讀完成后離開即(ji)可;

(2)圖書(shu)館有(you)管(guan)理員會不定期巡邏,發現(xian)空座位(wei)上的(de)圖書(shu)會自動收(shou)走,放回書(shu)架;

(3)A借(jie)閱 book1, 而后去借(jie)book2, 為(wei)了保留(liu)book1,需要在(zai)座位上立一個暫留(liu)的牌子(引用保留(liu));

(4)A借閱 book1,放(fang)置在site108;告訴B可以去(qu)查閱;B 到site108發(fa)現書(shu)已經被收走(zou)了(地址(zhi)引用(yong)無(wu)效)!

(5)A正在專注閱讀,管(guan)理員過來要求他站起來一下,以方便收(shou)取附近閑(xian)置的書(shu)籍(GC阻(zu)斷)!

觀感:圖書管(guan)理(li)妥善,只是來回穿梭勞作的管(guan)理(li)員(yuan),破壞(huai)了這安靜(jing)學習的氛圍。

 

Rust琢磨,如果我管理(li)這(zhe)個圖(tu)書(shu)館,讓(rang)每個人每次(ci)借(jie)一本圖(tu)書(shu)并登記;閱讀完畢后,舉手(shou)示意(yi),管理(li)員進行登記注銷,并回收圖(tu)書(shu)。如此,圖(tu)書(shu)借(jie)閱、歸還不(bu)就變得井然有序了?

類比于此,在內存管理時,如果能夠確保每次分配的內存,都由一個變量來唯一擁有;那么,當這個唯一擁有該內存的變量達到其生命周期的盡頭時,這塊內存就可以自動釋放(并且必須釋放)!如此這般,就可以實現內存安全管理、自動回收,并且不引入垃圾回收機制所帶來的新問題

一(yi)試之下,果(guo)然靈應(ying)!

而這個機制,Rust將其稱為所有權

 

理解Rust所有權機制

經過(guo)上一章節說(shuo)明,我們明白了Rust引入所有權機(ji)制所解決的(de)核心問題是:在無GC的(de)前提下,可以做到(dao)自(zi)動內存回收! 即保證編程過(guo)程中內存安全,又不會引入傳統GC機(ji)制所帶來的(de)負(fu)面效果。

接下來,我們將更加詳細說明(ming),Rust所有權(quan)機制(zhi)的(de)規則和使用。

所有權規則

進(jin)一(yi)步總結,Rust的所有權機制包括三條核(he)心規則:

  1. Rust 中每一個值都被一個變量所擁有,該變量被稱為值的所有者
  2. 一個值同時只能被一個變量所擁有,或者說一個值只能擁有一個所有者
  3. 當所有者(變量)離開作用域范圍時,這個值將被丟棄(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);
}

示例中,創建了一個(ge)字符(fu)串變量(動(dong)態分配)vstr,并更新vstr的(de)內容為 “origin data”;

而后,創建新變量nstr,并將(jiang)vstr “賦值”給(gei)nstr,并更新nstr的內容(rong),追(zhui)加“append something”;

最后,嘗試打印vstr,nstr的內容。

乍一(yi)看,這(zhe)個代碼沒有(you)什(shen)么問題;有(you)編程經驗的童鞋(xie),可能(neng)還在考慮 vstr 和nstr 是否一(yi)致(nstr 和vstr指向同(tong)一(yi)塊地(di)址(zhi),內容一(yi)致?nstr基于vstr做了深度拷(kao)貝,是兩塊獨立(li)的內存地(di)址(zhi)?)......

然后,現實(shi)是,nstr和vstr 確實(shi)指向同一塊(kuai)內存地址,但二者并不(bu)能同時存在:

通過idel預編譯(yi)提示,當我們將vstr “賦值”給nstr之后(hou),nstr已經被“moved”,已經不可用了,當我們嘗(chang)試(shi)訪(fang)問它時就會報錯!

下圖展示變量的(de)內存結構變化:

由此可見:

(1)當我們將一個包含內存分配的變量,“賦值”給另一個變量時,相應的內存所有權發生了轉移,由此來保證“一個值只能被一個變量所擁有”;

(2) rust中“=”的含義和行為相比于一般編程語言是有所區別的;在rust中,這個過程不再使用“賦值”這個術語,而使用“變量綁定”這個術語,更貼切的表述了內存所有權發生轉移的行為;

(3)特別需要注意的是,變量綁定不僅發生在塊邏輯中,也發生在函數調用過程中;因此,在函數調用時,需要謹慎處理入參和返回,需要注意是否發生了所有權移動;另外,函數傳遞時,根據需要,可以通過引用傳遞,來避免所有權轉移。

 

堆變量和棧變量

棧(zhan)和堆是(shi)編(bian)程語言最核心的(de)數據(ju)結(jie)構,但是(shi)在很多語言中,你(ni)并不(bu)需要(yao)深入了解(jie)(jie)棧(zhan)與(yu)堆(例如golang,你(ni)并不(bu)需要(yao)了解(jie)(jie)變量(liang)是(shi)在堆上分配(pei)還是(shi)在棧(zhan)上分配(pei),因為golang會(hui)自(zi)動轉換)。 但對于(yu) Rust 這(zhe)樣的(de)系統編(bian)程語言,值(zhi)是(shi)位于(yu)棧(zhan)上還是(shi)堆上非(fei)常重要(yao), 因為這(zhe)會(hui)影響程序的(de)行為和性能。

如果讀者(zhe)對(dui)堆和棧相關的基礎知識(shi)不夠牢固,小編強烈建(jian)議您去補習一下相關知識(shi),這對(dui)您了解rust相關機制大(da)有裨益!

Rust的變量綁定在處理堆(dui)變量和棧變量時,也是(shi)不一(yi)樣的,如下圖:

由圖可見:

(1) b=a; 變量(liang)綁定后,a依然可用;

(2) 而 str_d= str_c; 變量綁定(ding)后,str_c就(jiu)不可用了。

這是(shi)(shi)因為 a,b都是(shi)(shi)棧(zhan)(zhan)變(bian)量(liang),rust在處理棧(zhan)(zhan)變(bian)量(liang)“賦(fu)值”時(shi),采用了copy方(fang)式(由于棧(zhan)(zhan)變(bian)量(liang)通常很小(xiao),copy數據量(liang)不(bu)大;而且棧(zhan)(zhan)復制效率也高,所有rust選擇(ze)通過(guo)clone的方(fang)式處理棧(zhan)(zhan)變(bian)量(liang)的綁定)。而String類型是(shi)(shi)堆變(bian)量(liang),所以需(xu)要(yao)通過(guo)所有權(quan)轉移來(lai)滿足內(nei)存值的所有權(quan)規則(ze),使得(de)系(xi)統(tong)能自動(dong)管理內(nei)存分配和回收!

因此,可以理解為rust的所有權規則其實就是針對堆分配的內存管理!

另外補充一點,rust在函數傳遞時,是值傳遞。這就可以解釋,為何堆變量在傳遞到函數后,所有權發生了轉移(值傳遞的函數調用,入參s 傳入后,在內部生成了 一個同名的局部變量s',函數體內用的其實是這個局部變量;等效于,函數調用時 發生 s'=s的變量綁定)!

引用與借用

前(qian)兩章節,我(wo)們詳細解釋了Rust的(de)(de)(de)所(suo)有(you)權機(ji)制(zhi)的(de)(de)(de)原(yuan)理和實現,闡述了所(suo)有(you)權機(ji)制(zhi)如何使(shi)Rust在沒有(you)垃圾回收(shou)機(ji)制(zhi)的(de)(de)(de)前(qian)提下,實現了自動化內存管(guan)理,從而提高了內存安全。然而,如果僅僅支持通過轉移所(suo)有(you)權的(de)(de)(de)方式獲(huo)取一個(ge)值,那(nei)會讓程(cheng)序(xu)變得復雜(就(jiu)像上面示(shi)例的(de)(de)(de)打(da)(da)印(yin)字符(fu)串函數(shu)一樣,只(zhi)是(shi)調用了一個(ge)打(da)(da)印(yin),字符(fu)串變量就(jiu)變得不可用了)。 Rust 能(neng)否像其它(ta)編程(cheng)語言一樣,使(shi)用某個(ge)變量的(de)(de)(de)指針或者引(yin)用呢?答案是(shi)可以。

Rust 也支持引用,是一(yi)個指向變量的地址(zhi),與其他(ta)語言(yan)(C,Golang)含義一(yi)致(zhi)。

Rust 通過 借用(Borrowing) 這個概念來達成上述的目的,獲取變量的引用,稱之為借用(borrowing)。正如現實生活(huo)中,如果一個人擁有某樣東西,你可以從他那里借(jie)來,當使用(yong)完畢(bi)后,也必須要物歸(gui)原(yuan)主。

改下print_stringx函數(shu)如(ru)下:

函(han)數入參是一個字符(fu)串引用(而(er)不是字符(fu)串實例),這樣函(han)數調(diao)用就不會發生(sheng)所有權(quan)轉移。在(zai)rust中(zhong),這是常用的方式!

如上圖,pstr就是對vstr 的借用(不可變借用),此時pstr不擁有(you)內存數據(ju),但(dan)可以共(gong)享(xiang)vstr的值。

可變引用

上面的例子我(wo)(wo)們(men)(men)(men)可(ke)以看(kan)見,通過借用,我(wo)(wo)們(men)(men)(men)可(ke)以共享(xiang)變量的值。但這似(si)乎還(huan)不夠,因為(wei)我(wo)(wo)們(men)(men)(men)需要一些函(han)數和(he)接(jie)口來(lai)對(dui)數據(ju)進行(xing)加工(gong)(修改)。我(wo)(wo)們(men)(men)(men)期望(wang)有這樣的代(dai)碼(ma):

fn append_string(dst: &String, data: &str) {
    dst.push_str(data);
}

但上面的(de)代碼編譯(yi)時就會報錯:無(wu)法將dst引(yin)(yin)用(yong)(yong),借用(yong)(yong)為可(ke)寫(xie)的(de)借用(yong)(yong)!建議將dst引(yin)(yin)用(yong)(yong)改變為可(ke)寫(xie)引(yin)(yin)用(yong)(yong)(Rust的(de)編譯(yi)器(qi)(qi)是(shi)真(zhen)的(de)保(bao)姆(mu)級編譯(yi)器(qi)(qi))。

調整如下

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);
}

測試程序嘗試對vstr進行:1打印初(chu)始值,2,修改(gai)內容(rong);3,打印修改(gai)后的值。

見截(jie)圖,程(cheng)序(xu)編譯時(shi)報錯:嘗試將vstr 進(jin)行(xing)可(ke)變借(jie)用(yong)到(dao)mstr時(shi)異常,原因(yin)是vstr在這(zhe)之前(qian)已經被pstr進(jin)行(xing)了(le)不可(ke)變借(jie)用(yong)!

Rust在編譯時會進行(xing)數(shu)(shu)據(ju)競態(tai)檢(jian)查,確保(bao)流(liu)程(cheng)中(zhong)沒有明(ming)顯的(de)數(shu)(shu)據(ju)競爭。例子中(zhong),pstr對(dui)vstr進行(xing)了(le)不可變借用(yong),其含義為(wei)在pstr的(de)生(sheng)命(ming)周期(qi)(qi)內,期(qi)(qi)望(wang)vstr保(bao)持(chi)不變,以保(bao)證pstr對(dui)vstr數(shu)(shu)據(ju)引用(yong)的(de)一致(zhi)性;但是mstr嘗(chang)試對(dui)vstr進行(xing)可變借用(yong),那(nei)么意味著其生(sheng)命(ming)周期(qi)(qi)內可能(neng)對(dui)vstr進行(xing)修(xiu)改,并且mstr的(de)生(sheng)命(ming)周期(qi)(qi)與pstr的(de)生(sheng)命(ming)周期(qi)(qi)存(cun)在重疊;基于(yu)此(ci),Rust編譯檢(jian)查器,可以判定(ding)mstr的(de)可變借用(yong)會破壞pstr的(de)數(shu)(shu)據(ju)一致(zhi)性要求(qiu),因而被禁止!

Rust對(dui)于(yu)借用是數據競態的檢(jian)查要求為(wei):

(1)確保不存在多個可變借用同時對數據進行同時修改的情況;即,同一時間,最多只能存在一個可變借用。

(2)當有不可變借用存在的時候,不允許進行可變借用;反之依然。

(3)可以同時存在多個不可變借用。

(4)借用期間,引用必須總是有效的(在)。

 

關于Rust如何保(bao)障在(zai)借用期(qi)間,引(yin)用必須總是有(you)效,我們(men)將開(kai)一個新的(de)主題——rust的(de)生命(ming)周期(qi) 進行詳細說明。

那就下(xia)回分(fen)解了老鐵!

文章來自個人專欄
文(wen)章 | 訂閱
0條評論
0 / 1000
請輸入你的評論
2
1