1. 程式人生 > 其它 >rust漫遊 - 寫時拷貝 Cow<'_, B>

rust漫遊 - 寫時拷貝 Cow<'_, B>

rust漫遊 - 寫時拷貝 Cow<'_, B>

Cow 是一個寫時複製功能的智慧指標,在資料需要修改或者所有權發生變化時使用,多用於讀多寫少的場景。

pub enum Cow<'a, B: ?Sized + 'a>
where
    B: ToOwned,
{
    /// Borrowed data.
    Borrowed(&'a B),

    /// Owned data.
    Owned(<B as ToOwned>::Owned),
}

資料在寫入的情況下 Cow 才有存在的意義。當借用的資料被修改時,在不破壞原有資料的情況下,克隆一個副本並且在副本上進行修改;這是一種惰性的策略,在真正需要修改時才產生克隆的操作,而並不預先克隆。

關鍵函式

  • to_mut(), 獲取所有權資料的可變引用,無所有權時從借用資料中克隆
  • into_owned(), 提取所有權資料。

使用

官方示例

官方描述了三種情況

  1. 借用資料,但是未呼叫 to_mut(),故不存在 clone 操作
  2. 借用資料,呼叫 to_mut(), 發生 clone 操作
  3. 所有權資料,呼叫 to_mut(), 不存在 clone 操作,因為具有該資料的所有權
use std::borrow::Cow;

fn abs_all(input: &mut Cow<[i32]>) {
    for i in 0..input.len() {
        let v = input[i];
        if v < 0 {
            // Clones into a vector if not already owned.
            input.to_mut()[i] = -v;
        }
    }
}

// No clone occurs because `input` doesn't need to be mutated.
let slice = [0, 1, 2];
let mut input = Cow::from(&slice[..]);
abs_all(&mut input);

// Clone occurs because `input` needs to be mutated.
let slice = [-1, 0, 1];
let mut input = Cow::from(&slice[..]);
abs_all(&mut input);

// No clone occurs because `input` is already owned.
let mut input = Cow::from(vec![-1, 0, 1]);
abs_all(&mut input);

觀察所有權的變化

寫了個地址列印函式,以此來觀察所有權的變化。

fn print_addr(s: &str) {
    println!("{}", s);
    let mut p = s.as_ptr();
    for ch in s.chars() {
        println!("\t{:p}\t{}", p, ch);
        p = p.wrapping_add(ch.len_utf8());
    }
}

借用修改取出所有權

對借用的資料進行修改操作(有可能不會修改,見下文),操作完成後取出所有權 是最常見的用法

以下是一般的借用資料從修改至獲取所有權資料的過程,通過新產生的地址可以看出來存在 clone 操作

{
    let s = String::from("AB");
    print_addr(&s);
    let mut cow = Cow::Borrowed(&s);
    cow.to_mut().insert_str(1, "cd");
    let sr = cow.into_owned();
    print_addr(&sr);
}

// AB
//         0x7fb694c05af0  A
//         0x7fb694c05af1  B
// AcdB
//         0x7fb694c05b00  A
//         0x7fb694c05b01  c
//         0x7fb694c05b02  d
//         0x7fb694c05b03  B

上面的程式碼註釋 to_mut() 行後,相當於不會有獲取所有權的需求,這個時候是不應該做修改的,into_owned() 應該棄用轉而使用 as_str() 這類沒有所有權變化的操作.

{
    let s = String::from("AB");
    print_addr(&s);
    let mut cow = Cow::Borrowed(&s);
    // cow.to_mut().insert_str(1, "cd"); // 這一行是執行時決定的
    let sr = cow.as_str(); // 看後續的使用,若是後續也只是讀操作可以使用 as_str()
    print_addr(&sr);
}

在所有權資料上進行修改

在對已具有所有權資料上操作時,字串的地址未發生改變,未發生 clone 操作

insert_str 為兩個 memcpy 操作,故首地址不會發生變化

{
    let s1 = String::from("cd");
    print_addr(&s1);
    let mut cow1: Cow<'_, String> = Cow::Owned(s1);
    cow1.to_mut().insert_str(0, "AB");
    let sr1 = cow1.into_owned();
    print_addr(&sr1);
}

// cd
//         0x7fb694c05b10  c
//         0x7fb694c05b11  d
// ABcd
//         0x7fb694c05b10  A
//         0x7fb694c05b11  B
//         0x7fb694c05b12  c
//         0x7fb694c05b13  d

實現

Cow 是一個列舉值,包含了一個借用和所有。可以使用 Cow 的型別需要實現了 ToOwned trait。

ToOwned trait 同樣包含了所有權或借用的操作。

  1. 需要實現 Borrow 借用 trait
  2. 可以從借用的資料中建立所有權資料或者克隆

相關 trait

pub trait ToOwned {
    type Owned: Borrow<Self>;  // 需要實現 Borrow triat

    pub fn to_owned(&self) -> Self::Owned;  // 所有權建立
    pub fn clone_into(&self, target: &mut Self::Owned) { ... }
}

pub trait Borrow<Borrowed> where
    Borrowed: ?Sized, {
    pub fn borrow(&self) -> &Borrowed;
}

Borrow 借用 triat 泛型實現

impl<T: ?Sized> Borrow<T> for T {
    fn borrow(&self) -> &T {
        self
    }
}

to_owned 建立所有權的泛型實現如下,取決該型別是否實現 Clone tait

impl<T> ToOwned for T
where
    T: Clone,
{
    type Owned = T;
    fn to_owned(&self) -> T {
        self.clone()
    }
}

方法

to_mut()

獲取所有權的可變引用

  1. 已獲取所有權直接返回引用
  2. 借用資料的情況先呼叫 to_owned() 獲取克隆副本的所有權,並且做一個檢查

ref 關鍵字指示模式匹配為借用而不是移動。
<B as ToOwned>::Owned 表示 B 型別實現了 ToOwned trait,現使用該 trait 中的 Owned 型別,本質就是B型別本身,但是限制了實現 trait

pub fn to_mut(&mut self) -> &mut <B as ToOwned>::Owned {
    match *self {
        Borrowed(borrowed) => {
            *self = Owned(borrowed.to_owned());
            match *self {
                Borrowed(..) => unreachable!(),
                Owned(ref mut owned) => owned,
            }
        }
        Owned(ref mut owned) => owned,
    }
}

into_owned()

取出 Cow 中的所有權資料,當為獲取所有權時,進行 clone 操作

pub fn into_owned(self) -> <B as ToOwned>::Owned {
    match self {
        Borrowed(borrowed) => borrowed.to_owned(),
        Owned(owned) => owned,
    }
}

deref

由於 Cow 也實現了 Deref trait, 支援解引用強制多型,實現如下。

impl<B: ?Sized + ToOwned> Deref for Cow<'_, B> {
    type Target = B;

    fn deref(&self) -> &B {
        match *self {
            Borrowed(borrowed) => borrowed,
            Owned(ref owned) => owned.borrow(),
        }
    }
}

上面程式碼的 print_addr 的引數既可以是 &str, 也可以為 &Cow.

參考

官方文件