Skip to content

Instantly share code, notes, and snippets.

@KmolYuan
Created September 12, 2022 05:07
Show Gist options
  • Save KmolYuan/fd62b15f8ef18a24596c43aa8f0701a0 to your computer and use it in GitHub Desktop.
Save KmolYuan/fd62b15f8ef18a24596c43aa8f0701a0 to your computer and use it in GitHub Desktop.

Copy-On-Write in Rust

Rust 中常使用「移動 (move)」語意,為使複製成本降低,大部分的函式會採用 pass-by-reference 的方式 &T,但是移動語意的好處在於可以提前刪除物件。

標準庫提供的 enum 類型 {std, alloc}::borrow::Cow 同時允許「借出的 (Borrowed)」與「擁有的 (Owned)」兩種資料型態,並且會在借出可變引用 &mut T 時複製,亦可單純借出唯讀引用。

pub enum Cow<'a, B>
where
    B: 'a + ToOwned + ?Sized,
{
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

要能複製必須先實作 Clone trait,所以這邊加入了一個輔助的 ToOwned trait,它協助一般類型調用 Clone::clone() 與幫助無大小類型建立它們的有大小類型,無大小類型將在下面的小節介紹。另外,這邊用了一個 Borrow trait,跟 AsRef 非常類似,也在下面的小節介紹。

pub trait ToOwned {
    type Owned: Borrow<Self>;

    fn to_owned(&self) -> Self::Owned;

    fn clone_into(&self, target: &mut Self::Owned) { ... }
}

impl<T: Clone> ToOwned for T { ... }

當一個函式想同時允許 T&T 輸入時,可以使用 Into trait 轉成 Cow

fn foo<'a, C>(c: C)
where
    C: Into<Cow<'a, str>>,
{
    let s = c.into();
    s.as_ref(); // 唯讀借用
    s.to_mut(); // 可變借用
    let s: String = s.into_owned(); // 複製 (借出) or 移動 (擁有),產生擁有類型
}

foo("abc"); // &str
foo("abc".to_string()); // String

Unsized Type !Sized

無大小類型是一個抽象的概念,通常基於動態長度設計--也就是 slice [T] 類型,雖然知道 T 的大小,但是長度是 runtime 期間儲存的,能用 <[T]>::len(&self) -> usize 函式查詢。有了上述概念,&[T] 是一個指在第一項的指標 &T,和一個長度 usize 組成的。

設計無大小類型的目的在於,可以區分「記憶體內的資料」和「與程式綁定的資料」。例如我們可以使用 const 建立定值,程式會將定值資料複製到記憶體中開始執行,但是使用「指標」的定值,就好像只複製指標到記憶體一樣,由於資料是程式的一部份,指標會指向程式本身的區段,不用額外增加記憶體。

// 定值資料、定值指標
const SLICE: &[u8] = &[1, 2, 3];
const TITLE: &str = "Hello!";

// 複製定值指標到動態指標
let slice = SLICE;
// 複製定值指標到動態可變指標
let mut slice = SLICE;

// 定值資料、動態指標
let s = &[1, 2, 3];
let title = "Hello!";

只要自訂類型包含無大小類型,該類型就會是 !Sized。除了 trait 自身的定義,一般泛型參數都預設為 Sized,使用 ?Sized 則可以同時允許無大小類型。

Rust 標準庫包含下列無大小類型:

!Sized 「擁有的」類型 用途
[T] Vec<T> 動態陣列,比靜態陣列 [T; N] 靈活
str String UTF-8 合格字串,底層為 [u8]
std::ffi::OsStr std::ffi::OsString 系統預設字串,底層為 [u8],可能不相容 UTF-8
std::path::Path std::path::PathBuf 檔案路徑,底層為 OsStr

Borrow/AsRef/Deref

Rust 本身包含三種指標轉換的 trait,並且各自有其可變版本,如下表所列:

唯讀 可變 關係 用途
std::borrow::Borrow BorrowMut A: Borrow<B>, &A -> &B 底層類型 B 的指標轉換
std::convert::AsRef AsMut A: AsRef<B>, &A -> &B 任意指標的輕量級轉換
std::ops::Deref DerefMut A: Deref<Target=B>, &A -> &B 自動指標轉換,可實現類似繼承的效果

雖然 BorrowAsRef 的效果幾乎一樣,但是 Borrow 比較專注於保持底層類型的相容性,而 AsRef 則類似於指標界的 Into

由於 Deref 是自動的,因此 &A -> &B 的關係是唯一的,不過可以連鎖呼叫 &A -> &B -> &C

另外注意它們都是函式,所以結構體 struct 仍會受到借用限制,借出後不能夠將其他欄位借出。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment