Rust全部權語義模型

  編程語言的內存管理,大概能夠分爲自動和手動兩種。程序員

  自動管理就是用 GC(垃圾回收)來自動管理內存,像 Java、Ruby、Golang、Elixir 等語言都依賴於 GC。而 C/C++ 倒是依賴於手工管理內存,程序員使用 malloc 和 free 函數來分配釋放內存。編程

  GC技術通過這麼多年的發展,是相對安全的內存管理,也解放了程序員,可是在一些系統級編程領域,其實是須要避免 GC,由於 GC 會引發「世界暫停」,這將帶來性能問題,因此在系統級編程領域C/C++佔絕對的霸主地位。數組

  可是,有C/C++就夠了嗎?靠手工來管理內存,會帶來不少安全問題,好比懸垂指針,誠然有最佳實踐引導,就算經驗豐富的熟手也難以免相似內存安全的錯誤。安全

  Rust 的出現,就是爲了解決這個痛點,它強大的全部權系統,就像是黑暗中的明燈。編程語言

  我曾經也對其感到疑慮,這憑空產生的 Rust 的全部權系統是否是拍腦殼發明的,這真的是解決內存安全問題的「銀彈」嗎?函數

  其實歷史上也曾經有過解決內存安全問題的努力,好比 Cyclone 語言,它是一門對 C 語言進行安全升級的語言,基於區域(region,有點和 Rust 全部權系統中的生命週期相相似)的內存管理,避免一些潛在的內存安全問題,可是,功能極其有限,相似的嘗試還有ML Kit。性能

  就是這些早期的方案,給了Rust語言靈感,才造就如今的全部權系統,因此Rust的全部權系統並不是憑空產生。至因而不是「銀彈」,還不敢下結論,至少,Rust的全部權系統是迄今爲止最精妙最科學的方案了。學習

  語義模型spa

  什麼叫語義模型?語義,顧名思義,是指語言的含義。咱們在學習一個新概念的時候,首先就要搞明白它的語義。而語義模型,是指語義構建的心智模型,由於概念點不是孤立存在的,彼此之間必然有緊密的聯繫,咱們經過挖掘其語義之間的關聯規則,在你的認知中造成一顆「語義樹」,這樣的理解纔是通透的。全部權的「語義樹」,以下圖所示:翻譯

  上圖中的語義樹,主要是想表達下面幾層意思:

  ● 全部權是有好多個概念系統性組成的一個總體概念。

  ● let綁定,綁定了什麼?變量 + 做用域 + 數據(內存)。

  ● move、lifetime、RAII都是和做用域相關的,因此想理解它們就先要理解做用域。

  全部權

  全部權,顧名思義,至少應該包含兩個對象:「全部者」和「全部物」。在Rust中,「全部者」就是變量,「全部物」是數據,抽象來講,就是指某一片內存。let關鍵字,容許你綁定「全部者」和「全部物」,好比下面代碼:

  let num = String::from("42");

  let 關鍵字,讓 num 綁定了42,那麼能夠說,num擁有42的全部權。但這個全部權,是有範圍限制的,這個範圍就是做用域(scope),準確來講是擁有域(owner scope)。換句話說,num在當前做用域下,擁有42的全部權。若是它要進入別的做用域,就必須交出全部權。好比下面的代碼:

  let num = String::from("42");

  let num2 = num;

  let關鍵字會開啓一個隱藏做用域,咱們能夠藉助於MIR來查看,編譯這兩行代碼,查看其MIR:

  scope 1 {

  let _1: std::string::String; // "num" in scope 1 at :4:8: 4:11

  scope 2 {

  let _2: std::string::String; // "num2" in scope 2 at :5:8: 5:12

  }

  }

  Scope 1就是num所在的做用域,scope 2是num2所在的做用域。當你此時想像下面這樣使用num的時候:

  let num = String::from("42");let num2 = num;println!("{:?}", num);

  編譯器會報錯:error[E0382]: use of moved value: `num`。由於num變量的全部權已經轉移了。

  移動(move)語義

  移動,是指全部權的轉移。何時全部權會轉移呢?就是當變量切換做用域的時候,所謂移動,固然是從一個地方挪到另外一個地方。其實你也能夠這樣認爲,當變量切換到另外一個做用域,它在當前做用域的綁定將會失效,它擁有的數據則會在另外一個做用域被從新綁定。

  可是對於實現了Copy Trait的類型來講,當移動發生的時候,它們能夠Copy的副本代替本身去移動,而自身還保留着全部權。好比,Rust中的基本數字類型都默認實現了Copy Trait,好比下面示例:

  let num = 42;

  let num2 = num;

  println!("{:?}", num);

  此時,咱們打印num,編譯器不會報錯。num已經move了,可是由於數字類型是默認實現Copy Trait,因此它move的是自身的副本,其全部權還在,並未發生轉移,經過編譯。不過須要注意的是,Rust 不容許自身或其任何部分實現了Drop trait 的類型使用Copy trait。

  當move發生的時候,全部權被轉移的變量,將會被釋放。

  做用域(Scope)

  沒有GC幫助咱們自動管理內存,咱們只能依賴全部權這套規則來手工管理內存,這就增長了咱們的心智負擔。而全部權的這套規則,是依賴於做用域的,因此咱們須要對Rust中的做用域有必定了解。

  咱們在以前的描述中已經見過了隱式做用域,也就是在當前做用域中由let開啓的做用域。在Rust中,也有一些特殊的宏,好比println!(),也會產生一個默認的scope,而且會隱式借用變量。除此以外,更明顯的做用域 範圍則是函數,也就是說,一個函數自己,就是一個顯式的做用域。你也可使用一對花括號({})來建立顯式的做用域。

  除此以外,一個函數自己就顯式的開闢了一個獨立的做用域。好比:

  fn sum(a: u32, b: u32) -> u32 {

  a + b

  }

  fn main(){

  let a = 1;

  let b = 2;

  sum(a, b);

  }

  上面的代碼中,當調用sum函數的時候,a和b看成參數傳遞過去,此時就會發生全部權move的行爲,可是由於a和b都是基本數據類型,實現了Copy Trait,因此它們的全部權沒有被轉移。若是換了是沒有實現Copy Trait的變量,全部權就會被轉移。

  做用域在Rust中的做用就是製造一個邊界,這個邊界是全部權的邊界。變量走出其所在做用域,全部權會move。若是不想讓全部權move,則可使用「引用」來「出借」變量,而此時做用域的做用就是保證被「借用」的變量準確歸還。

  引用和借用

  有的時候,咱們並不想讓變量的全部權轉移,好比,我寫一個函數,該函數只是給某個數組插入一個固定的值:

  fn push(vec: &mut Vec) {

  vec.push(1);

  }

  fn main(){

  let mut vec = vec![0, 1, 3, 5];

  push(&mut vec);

  println!("{:?}", vec);

  }

  此時,咱們把數組vec傳給push函數,就不但願把全部權轉移,因此,只須要傳入一個可變引用&mut vec,由於咱們須要修改vec,這樣push函數就得了vec變量的可變借用,讓咱們去修改。push函數修改完,會將借用的全部權歸還給vec,而後println!函數就能夠順利使用vec來輸出打印。

  引用很是方便咱們使用,可是若是濫用的話,會引發安全問題,好比懸垂指針。看下面示例:

  let r;

  {

  let a = 1;

  r = &a;

  }

  println!("{}", r);

  上面代碼中,當a離開做用域的時候會被釋放,但此時r還持有一個a的借用,編譯器中的借用檢查器就會告訴你:`a` does not live long enough。翻譯過來就是:`a`活的不夠久。這表明着a的生命週期過短,而沒法借用給r,不然&a就指向了一個曾經存在但如今已再也不存在的對象,這就是懸垂指針,也有人將其稱爲野指針。

  生命週期

  上面的示例中,是在同一個函數做用域下,編譯器能夠識別出生命週期的問題,可是當咱們在函數之間傳遞引用的時候,編譯器就很難自動識別出這些問題了,因此Rust要求咱們爲這些引用顯式的指定生命週期標記,若是你不指定生命週期標記,那麼編譯器將會「鞭策」你。

  struct Foo {

  x: &i32,

  }

  fn main() {

  let y = &5;

  let f = Foo { x: y };

  println!("{}", f.x);

  }無錫婦科醫院哪家好 http://www.wxbhnkyy39.com/

  上面這段代碼,編譯器會提示你:missing lifetime specifier。這是由於,y這個借用被傳遞到了 let f = Foo { x: y }所在做用域中。因此須要確保借用y活得比Foo結構體實例長才行,不然,若是借用y被提早釋放,Foo結構體實例就會形成懸垂指針了。因此咱們須要爲其增長生命週期標記:

  struct Foo<'a> {

  x: &'a i32,

  }

  fn main() {

  let y = &5;

  let f = Foo { x: y };

  println!("{}", f.x);

  }

  加上生命週期標記之後,編譯器中的借用檢查器就會幫助咱們自動比對參數變量的做用域長度,從而確保內存安全。

  再來看一個例子:

  fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {

  if x.len() > y.len() {

  x

  } else {

  y

  }

  }

  fn main() {

  let a = "hello";

  let result;

  {

  let b = String::from("world");

  result = longest(a, b.as_str());

  }

  println!("The longest string is {}", result);

  }

  此段代碼,編譯器會報錯:`b` does not live long enough。這是由於result在外部做用域定義的,result的生命週期是和main函數同樣長的,也就是說,在main函數做用域結束以前,result都必須存活。而此時,變量b在花括號定義的做用域中,出了做用域b就會被釋放。而根據longest函數簽名中的生命週期標註,參數b的生命週期必須和返回值的生命週期一致,因此,借用檢查器果斷的判斷出`b` does not live long enough。

  「顯式的指定」,這是Rust的設計哲學之一。這對於新手,尤爲是習慣了動態語言的人來講,多是一個心智負擔。顯式的指定方便了編譯器,可是對於程序員來講略顯繁瑣。不過爲了安全考慮,咱們就欣然接受這套規則吧。

  首發於知乎專欄

相關文章
相關標籤/搜索