Rust入坑指南:智慧指針

  • 2020 年 3 月 10 日
  • 筆記

在了解了Rust中的所有權、所有權借用、生命周期這些概念後,相信各位坑友對Rust已經有了比較深刻的認識了,今天又是一個連環坑,我們一起來把智慧指針刨出來,一探究竟。

智慧指針是Rust中一種特殊的數據結構。它與普通指針的本質區別在於普通指針是對值的借用,而智慧指針通常擁有對數據的所有權。在Rust中,如果你想要在堆記憶體中定義一個對象,並不是像Java中那樣直接new一個,也不是像C語言中那樣需要手動malloc函數來分配記憶體空間。Rust中使用的是Box::new來對數據進行封箱,而Box<T>就是我們今天要介紹的智慧指針之一。除了Box<T>之外,Rust標準庫中提供的智慧指針還有Rc<T>Ref<T>RefCell<T>等等。在詳細介紹之前,我們還是先了解一下智慧指針的基本概念。

基本概念

我們說Rust的智慧指針是一種特殊的數據結構,那麼它特殊在哪呢?它與普通數據結構的區別在於智慧指針實現了DerefDrop這兩個traits。實現Deref可以使智慧指針能夠解引用,而實現Drop則使智慧指針具有自動析構的能力。

Deref

Deref有一個特性是強制隱式轉換:如果一個類型T實現了Deref<Target=U>,則該類型T的引用在應用的時候會被自動轉換為類型U

use std::rc::Rc;  fn main() {      let x = Rc::new("hello");      println!("{:?}", x.chars());  }

如果你查看Rc的源碼,會發現它並沒有實現chars()方法,但我們上面這段程式碼卻可以直接調用,這是因為Rc實現了Deref。

#[stable(feature = "rust1", since = "1.0.0")]  impl<T: ?Sized> Deref for Rc<T> {      type Target = T;        #[inline(always)]      fn deref(&self) -> &T {          &self.inner().value      }  }

這就使得智慧指針在使用時被自動解引用,像是不存在一樣。

Deref的內部實現是這樣的:

#[lang = "deref"]  #[doc(alias = "*")]  #[doc(alias = "&*")]  #[stable(feature = "rust1", since = "1.0.0")]  pub trait Deref {      /// The resulting type after dereferencing.      #[stable(feature = "rust1", since = "1.0.0")]      type Target: ?Sized;        /// Dereferences the value.      #[must_use]      #[stable(feature = "rust1", since = "1.0.0")]      fn deref(&self) -> &Self::Target;  }    #[lang = "deref_mut"]  #[doc(alias = "*")]  #[stable(feature = "rust1", since = "1.0.0")]  pub trait DerefMut: Deref {      /// Mutably dereferences the value.      #[stable(feature = "rust1", since = "1.0.0")]      fn deref_mut(&mut self) -> &mut Self::Target;  }

DerefMut和Deref類似,只不過它是返回可變引用的。

Drop

Drop對於智慧指針非常重要,它是在智慧指針被丟棄時自動執行一些清理工作,這裡所說的清理工作並不僅限於釋放堆記憶體,還包括一些釋放文件和網路連接等工作。之前我總是把Drop理解成Java中的GC,隨著對它的深入了解後,我發現它比GC要強大許多。

Drop的內部實現是這樣的:

#[lang = "drop"]  #[stable(feature = "rust1", since = "1.0.0")]  pub trait Drop {      #[stable(feature = "rust1", since = "1.0.0")]      fn drop(&mut self);  }

這裡只有一個drop方法,實現了Drop的結構體,在消亡之前,都會調用drop方法。

use std::ops::Drop;  #[derive(Debug)]  struct S(i32);    impl Drop for S {      fn drop(&mut self) {          println!("drop {}", self.0);      }  }    fn main() {      let x = S(1);      println!("create x: {:?}", x);      {          let y = S(2);          println!("create y: {:?}", y);      }  }

上面程式碼的執行結果為

結果

可以看到x和y在生命周期結束時都去執行了drop方法。

對智慧指針的基本概念就先介紹到這裡,下面我們進入正題,具體來看看每個智慧指針都有什麼特點吧。

Box

前面我們已經提到了Box在Rust中是用來在堆記憶體中保存數據使用的。它的使用方法非常簡單:

fn main() {      let x = Box::new("hello");      println!("{:?}", x.chars())  }

我們可以看一下Box::new的源碼

#[stable(feature = "rust1", since = "1.0.0")]  #[inline(always)]  pub fn new(x: T) -> Box<T> {    box x  }

可以看到這裡只有一個box關鍵字,這個關鍵字是用來進行堆記憶體分配的,它只能在Rust源碼內部使用。box關鍵字會調用Rust內部的exchange_malloc和box_free方法來管理記憶體。

#[cfg(not(test))]  #[lang = "exchange_malloc"]  #[inline]  unsafe fn exchange_malloc(size: usize, align: usize) -> *mut u8 {      if size == 0 {          align as *mut u8      } else {          let layout = Layout::from_size_align_unchecked(size, align);          let ptr = alloc(layout);          if !ptr.is_null() {              ptr          } else {              handle_alloc_error(layout)          }      }  }    #[cfg_attr(not(test), lang = "box_free")]  #[inline]  pub(crate) unsafe fn box_free<T: ?Sized>(ptr: Unique<T>) {      let ptr = ptr.as_ptr();      let size = size_of_val(&*ptr);      let align = min_align_of_val(&*ptr);      // We do not allocate for Box<T> when T is ZST, so deallocation is also not necessary.      if size != 0 {          let layout = Layout::from_size_align_unchecked(size, align);          dealloc(ptr as *mut u8, layout);      }  }

Rc

在前面的學習中,我們知道Rust中一個值在同一時間只能有一個變數擁有其所有權,但有時我們可能會需要多個變數擁有所有權,例如在圖結構中,兩個圖可能對同一條邊擁有所有權。

對於這樣的情況,Rust為我們提供了智慧指針Rc(reference counting)來解決共享所有權的問題。每當我們通過Rc共享一個所有權時,引用計數就會加一。當引用計數為0時,該值才會被析構。

Rc是單執行緒引用計數指針,不是執行緒安全類型。

我們還是通過一個簡單的例子來看一下Rc的應用吧。(示例來自the book

如果我們想要造一個「雙頭」的鏈表,如下圖所示,3和4都指向5。我們先來嘗試使用Box實現。

雙頭鏈表

enum List {      Cons(i32, Box<List>),      Nil,  }    use crate::List::{Cons, Nil};    fn main() {      let a = Cons(5,                   Box::new(Cons(10,                                 Box::new(Nil))));      let b = Cons(3, Box::new(a));      let c = Cons(4, Box::new(a));  }

上述程式碼在編譯時就會報錯,因為a綁定給了b以後就無法再綁定給c了。

Box無法共享所有權

enum List {      Cons(i32, Rc<List>),      Nil,  }    use crate::List::{Cons, Nil};  use std::rc::Rc;    fn main() {      let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));      let b = Cons(3, Rc::clone(&a));      let c = Cons(4, Rc::clone(&a));      println!("count a {}", Rc::strong_count(&a));  }

這時我們可以看到a的引用計數是3,這是因為這裡計算的是節點5的引用計數,而a本身也是對5的一次綁定。這種通過clone方法共享所有權的引用稱作強引用

Rust還為我們提供了另一種智慧指針Weak,你可以把它當作是Rc的另一個版本。它提供的引用屬於弱引用。它共享的指針沒有所有權。但他可以幫助我們有效的避免循環引用。

RefCell

前文中我們聊過變數的可變性和不可變性,主要是針對變數的。按照前面所講的,對於結構體來說,我們也只能控制它的整個實例是否可變。實例的具體某個成員是否可變我們是控制不了的。但在實際開發中,這樣的場景也是比較常見的。比如我們有一個User結構體:

struct User {      id: i32,      name: str,      age: u8,  }

通常情況下,我們只能修改一個人的名稱或者年齡,而不能修改用戶的id。如果我們把User的實例設置成了可變狀態,那就不能保證別人不會去修改id。

為了應對這種情況,Rust為我們提供了Cell<T>RefCell<T>。它們本質上不屬於智慧指針,而是可以提供內部可變性的容器。內部可變性實際上是一種設計模式,它的內部是通過一些unsafe程式碼來實現的。

我們先來看一下Cell<T>的使用方法吧。

use std::cell::Cell;  struct Foo {      x: u32,      y: Cell<u32>,  }    fn main() {      let foo = Foo { x: 1, y: Cell::new(3)};      assert_eq!(1, foo.x);      assert_eq!(3, foo.y.get());      foo.y.set(5);      assert_eq!(5, foo.y.get());  }

我們可以使用Cell的set/get方法來設置/獲取起內部的值。這有點像我們在Java實體類中的setter/getter方法。這裡有一點需要注意:Cell<T>中包裹的T必須要實現Copy才能夠使用get方法,如果沒有實現Copy,則需要使用Cell提供的get_mut方法來返回可變借用,而set方法在任何情況下都可以使用。由此可見Cell並沒有違反借用規則。

對於沒有實現Copy的類型,使用Cell<T>還是比較不方便的,還好Rust還提供了RefCell<T>。話不多說,我們直接來看程式碼。

use std::cell::RefCell;  fn main() {      let x = RefCell::new(vec![1, 2, 3]);      println!("{:?}", x.borrow());      x.borrow_mut().push(5);      println!("{:?}", x.borrow());  }

從上面這段程式碼中我們可以觀察到RefCell<T>的borrow_mut和borrow方法對應了Cell<T>中的set和get方法。

RefCell<T>Cell<T>還有一點區別是:Cell<T>沒有運行時開銷(不過也不要用它包裹大的數據結構),而RefCell<T>是有運行時開銷的,這是因為使用RefCell<T>時需要維護一個借用檢查器,如果違反借用規則,則會引起執行緒恐慌。

總結

關於智慧指針我們就先介紹這麼多,現在我們簡單總結一下。Rust的智慧指針為我們提供了很多有用的功能,智慧指針的一個特點就是實現了DropDeref這兩個trait。其中Droptrait中提供了drop方法,在析構時會去調用。Dereftrait提供了自動解引用的能力,讓我們在使用智慧指針的時候不需要再手動解引用了。

接著我們分別介紹了幾種常見的智慧指針。Box<T>可以幫助我們在堆記憶體中分配值,Rc<T>為我們提供了多次借用的能力。RefCell<T>使內部可變性成為現實。

最後再多說一點,其實我們以前見到過的StringVec也屬於智慧指針。

至於它們為什麼屬於智慧指針,Rust又提供了哪些其他的智慧指針呢?這裡就留個坑吧,感興趣的同學可以自己踩一下。