一起學Rust-理解所有權

  • 2019 年 10 月 6 日
  • 筆記

上期學習回顧:上期學習的結構體的結尾留了一個小的問題,這一期的開始來學習一下。

原問題是這樣的: &str 類型通過mem::size_of::<&str>()進行列印記憶體,始終為16位元組。(這裡不嚴謹了,應該是在64位機器上是16位元組)

為啥呢?其實答案就藏在rustdoc中,位於 std/primitive.str.html#representation.

原文:A &str is made up of two components: a pointer to some bytes, and a length. You can look at these with the as_ptr and len methods

use std::slice;  use std::str;    let story = "Once upon a time...";    let ptr = story.as_ptr();  let len = story.len();    // story has nineteen bytes  assert_eq!(19, len);    // We can re-build a str out of ptr and len. This is all unsafe because  // we are responsible for making sure the two components are valid:  let s = unsafe {      // First, we build a &[u8]...      let slice = slice::from_raw_parts(ptr, len);        // ... and then convert that slice into a string slice      str::from_utf8(slice)  };    assert_eq!(s, Ok(story));

這個原文中的例子就是證明 &str 組成的兩部分,下面進行簡單解析:

ptr通過as_ptr方法獲取,是 *const u8 類型,佔用8位元組,len變數是usize類型在64位機器中是8位元組。slice變數從from_raw_parts中獲取,主要返回的是Repr結構中的rust成員,T指代類型是u8:

#[inline]  #[stable(feature = "rust1", since = "1.0.0")]  pub unsafe fn from_raw_parts<'a, T>(data: *const T, len: usize) -> &'a [T] {      debug_assert!(data as usize % mem::align_of::<T>() == 0, "attempt to create unaligned slice");      debug_assert!(mem::size_of::<T>().saturating_mul(len) <= isize::MAX as usize,                    "attempt to create slice covering half the address space");      Repr { raw: FatPtr { data, len } }.rust  }

下面是union Repr結構,其中rust、rust_mut、raw共用同一塊記憶體空間。

#[repr(C)]  union Repr<'a, T: 'a> {      rust: &'a [T],      rust_mut: &'a mut [T],      raw: FatPtr<T>,  }

下面是FatPtr結構

#[repr(C)]  struct FatPtr<T> {      data: *const T,      len: usize,  }

實際的FatPtr則是一個8 + 8 = 16位元組的結構體,那麼Repr的union就是16位元組。同時由於ptr變數是* const u8類型,所以T為u8,因此from_raw_parts方法返回類型為* const [u8],大小為16位元組。而接下來的方法內僅做來utf8的檢查以及類型轉換的工作,大小未發生變化,所以在64位的機器上 &str 類型是16位元組。

本期正題

所有權的概念,是在Rust初學時需要面對的一個難題,總是在編寫程式碼的過程中出現各種的問題

error[E0382]: use of moved value: `s`    error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable

所有權規則:

  • Each value in Rust has a variable that』s called its owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

所有權規則解釋:

  • 在Rust中,每一個值都會對應一個叫做所有者的變數。
  • 一次運行中的值只能存在一個所有者
  • 當所有者離開作用域,它的值會被釋放掉。
fn main() {      let arr = vec![1,2];      if arr.len() > 0 {          let arr1 = arr;          println!("{:?}", arr1);  //列印 [1,2]      }      println!("{:?}", arr);  //編譯報錯        println!("{:?}", arr1);  //編譯報錯  }

在上面的例子中,可知以下幾條資訊

arr是vec![1,2]的所有者。

main函數的程式碼塊是一個作用域,if 的語句塊也是一個單獨的作用域。

在if 程式碼塊中 vec![1,2]的所有者變成了arr1。

所以如果注釋掉底部的兩個錯誤語句,第5行是可以列印arr1的值。而下面列印arr失敗的原因就是arr變數已經從記憶體釋放,無法訪問。而列印arr1出現錯誤的原因就是arr1是屬於if程式碼塊的,當離開if 的作用域後,記憶體釋放。

移動和複製

當創建一個不定長的值的情況下會在堆記憶體中申請空間,此類值的變數在重新賦值給另外一個變數時會發生所有權的移動 move ,移動的結果就是原有的變數釋放,新變數指向值的堆記憶體地址,成為此值的唯一所有者,將來在離開作用域後釋放此變數以及其值的記憶體空間。(由於Rust內無垃圾回收機制,如果不是移動所有權,那麼會有兩個或多個變數指向值的堆記憶體,則在離開作用域釋放記憶體時可能會出現多次釋放,可能存在記憶體安全的問題,所以為了防止出現記憶體安全的問題,使用了唯一對應的所有者,釋放記憶體時也僅一次性完成)

let string = String::new();  let new_string = string;  //會發生所有權轉移,string變數釋放。

對於原則1有一點是需要關注的,看以下例子:

let num1 = 5;  //以下均可以正常編譯運行。  // let num1 = "abc";  // let num1 = (2,3);  // let num1 = [23,54];  // let num1 = ("sdk", 3);  // let num1 = true;  // let num2 = num1;  println!("{},{}", num1, num2);  //正常列印

可以正常運行的原因是因為num2賦值時發生了值的複製,觀察可發現num1變數的值均是標量值,固定大小,是存儲在棧記憶體中的,所以複製相對容易很多,所以Rust提供了複製的功能,在離開作用域時分別釋放各自的記憶體,不會出現多次釋放的記憶體安全問題,而且也同樣滿足所有權第一條的規則。

如果變數中包含有需要申請堆記憶體的值就會進行發生所有權移動,而不是複製,因為堆內的數據大小無法確定,複製可能會造成大量資源的消耗:

let var1 = (3, String::from("s"));  let var2 = var1;  //這裡會發生所有權移動。    ------------    struct Demo {      a:char,      b:i32  }  let var1 = Demo{a: '3', b:3};  let var2 = var1;  // 所有權移動

克隆

克隆可用於對堆記憶體的值的拷貝,堆記憶體數據在Rust內不存在深淺拷貝的說法,可以認為克隆就是深拷貝,完全拷貝堆記憶體數據,比如String類型就實現了Clone trait,可以通過調用clone方法拷貝一份數據:

let var1 = String::from("sd");  let var2 = var1.clone();  //克隆,  println!("{},{}", var1, var2);   //編譯成功    -------  #[derive(Debug,Clone)]  //必須定義Clone才能調用clone方法,  struct Demo {      a:char,      b:i32  }  let var1 = Demo{a: '3', b:3};  let var2 = var1.clone();  //克隆堆記憶體  println!("{},{}", var1, var2);   //編譯成功    ------  #[derive(Debug,Copy,Clone)]  //實現Copy和Clone,在賦值時會發生賦值  struct Demo {      a:char,      b:i32  }  let var1 = Demo{a: '3', b:3};  let var2 = var1;  //克隆堆記憶體  println!("{},{}", var1, var2);   //編譯成功    -----** but **----    //這裡是編譯不通過的,  //因為Demo中的b類型實現了Drop trait  #[derive(Debug, Copy, Clone)]  struct Demo {      a:char,      b:Vec<i32>  }  

如上例中,通過對全部是標量類型成員的結構體,實現Copy和Clone trait是可以在賦值時直接發生克隆操作的,不必要顯示調用clone方法。但是對於成員中含有如Vec<T>類型的結構體,則無法實現Copy,因為其本身實現了Drop,與Copy trait互斥。

棧中的數據調用clone和不調用clone的效果是一樣的,因為在重新賦值時就是完全拷貝的,所以可以省略clone的調用。

對於所有權的規則可以通過各種的變數組合進行測試,總結規律,才能印象深刻。

函數作用域

不僅僅是變數重新賦值,當值在不同作用域間傳遞時,也會發生所有權轉移,下面的示例無法成功編譯。

fn main() {      let s = String::from("sd");      test(s);//  s的所有權轉移至test函數內      println!("{}",s);  }    fn test(s:String) {      println!("{}", s);  }

由於第三條規則的原因,離開作用域會釋放記憶體,所以發生所有權的轉移同樣是為了防止發生記憶體安全問題。

fn main() {      let s = String::from("sd");      let a = test(s);  //s所有權轉移,原s失效,a接受test內轉移的數據。      println!("{}", a);  }    fn test(s:String) -> String {      println!("{}", s);      s      // s作為返回值返回,所有權轉移出此方法  }//離開,作用域內變數釋放

上面的例子說明了所有權轉移的變數,只是變數失效,但並不影響值,將值轉移給其他變數,函數的返回值也是同樣可以轉移所有權