99re热这里只有精品视频,7777色鬼xxxx欧美色妇,国产成人精品一区二三区在线观看,内射爽无广熟女亚洲,精品人妻av一区二区三区

Rust RefCell<T> 和內(nèi)部可變性模式

2023-03-22 15:13 更新
ch15-05-interior-mutability.md
commit 74edb8dfe07edf8fdae49c6385c72840c07dd18f

內(nèi)部可變性Interior mutability)是 Rust 中的一個設(shè)計模式,它允許你即使在有不可變引用時也可以改變數(shù)據(jù),這通常是借用規(guī)則所不允許的。為了改變數(shù)據(jù),該模式在數(shù)據(jù)結(jié)構(gòu)中使用 unsafe 代碼來模糊 Rust 通常的可變性和借用規(guī)則。我們還未講到不安全代碼;第十九章會學(xué)習(xí)它們。當(dāng)可以確保代碼在運行時會遵守借用規(guī)則,即使編譯器不能保證的情況,可以選擇使用那些運用內(nèi)部可變性模式的類型。所涉及的 unsafe 代碼將被封裝進安全的 API 中,而外部類型仍然是不可變的。

讓我們通過遵循內(nèi)部可變性模式的 RefCell<T> 類型來開始探索。

通過 RefCell<T> 在運行時檢查借用規(guī)則

不同于 Rc<T>,RefCell<T> 代表其數(shù)據(jù)的唯一的所有權(quán)。那么是什么讓 RefCell<T> 不同于像 Box<T> 這樣的類型呢?回憶一下第四章所學(xué)的借用規(guī)則:

  1. 在任意給定時刻,只能擁有一個可變引用或任意數(shù)量的不可變引用 之一(而不是兩者)。
  2. 引用必須總是有效的。

對于引用和 Box<T>,借用規(guī)則的不可變性作用于編譯時。對于 RefCell<T>,這些不可變性作用于 運行時。對于引用,如果違反這些規(guī)則,會得到一個編譯錯誤。而對于 RefCell<T>,如果違反這些規(guī)則程序會 panic 并退出。

在編譯時檢查借用規(guī)則的優(yōu)勢是這些錯誤將在開發(fā)過程的早期被捕獲,同時對運行時沒有性能影響,因為所有的分析都提前完成了。為此,在編譯時檢查借用規(guī)則是大部分情況的最佳選擇,這也正是其為何是 Rust 的默認行為。

相反在運行時檢查借用規(guī)則的好處則是允許出現(xiàn)特定內(nèi)存安全的場景,而它們在編譯時檢查中是不允許的。靜態(tài)分析,正如 Rust 編譯器,是天生保守的。但代碼的一些屬性不可能通過分析代碼發(fā)現(xiàn):其中最著名的就是 停機問題(Halting Problem),這超出了本書的范疇,不過如果你感興趣的話這是一個值得研究的有趣主題。

因為一些分析是不可能的,如果 Rust 編譯器不能通過所有權(quán)規(guī)則編譯,它可能會拒絕一個正確的程序;從這種角度考慮它是保守的。如果 Rust 接受不正確的程序,那么用戶也就不會相信 Rust 所做的保證了。然而,如果 Rust 拒絕正確的程序,雖然會給程序員帶來不便,但不會帶來災(zāi)難。RefCell<T> 正是用于當(dāng)你確信代碼遵守借用規(guī)則,而編譯器不能理解和確定的時候。

類似于 Rc<T>,RefCell<T> 只能用于單線程場景。如果嘗試在多線程上下文中使用RefCell<T>,會得到一個編譯錯誤。第十六章會介紹如何在多線程程序中使用 RefCell<T> 的功能。

如下為選擇 Box<T>Rc<T> 或 RefCell<T> 的理由:

  • ?Rc<T>? 允許相同數(shù)據(jù)有多個所有者;?Box<T>? 和 ?RefCell<T>? 有單一所有者。
  • ?Box<T>? 允許在編譯時執(zhí)行不可變或可變借用檢查;?Rc<T>?僅允許在編譯時執(zhí)行不可變借用檢查;?RefCell<T>? 允許在運行時執(zhí)行不可變或可變借用檢查。
  • 因為 ?RefCell<T>? 允許在運行時執(zhí)行可變借用檢查,所以我們可以在即便 ?RefCell<T>? 自身是不可變的情況下修改其內(nèi)部的值。

在不可變值內(nèi)部改變值就是 內(nèi)部可變性 模式。讓我們看看何時內(nèi)部可變性是有用的,并討論這是如何成為可能的。

內(nèi)部可變性:不可變值的可變借用

借用規(guī)則的一個推論是當(dāng)有一個不可變值時,不能可變地借用它。例如,如下代碼不能編譯:

fn main() {
    let x = 5;
    let y = &mut x;
}

如果嘗試編譯,會得到如下錯誤:

$ cargo run
   Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
 --> src/main.rs:3:13
  |
2 |     let x = 5;
  |         - help: consider changing this to be mutable: `mut x`
3 |     let y = &mut x;
  |             ^^^^^^ cannot borrow as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` due to previous error

然而,特定情況下,令一個值在其方法內(nèi)部能夠修改自身,而在其他代碼中仍視為不可變,是很有用的。值方法外部的代碼就不能修改其值了。RefCell<T> 是一個獲得內(nèi)部可變性的方法。RefCell<T> 并沒有完全繞開借用規(guī)則,編譯器中的借用檢查器允許內(nèi)部可變性并相應(yīng)地在運行時檢查借用規(guī)則。如果違反了這些規(guī)則,會出現(xiàn) panic 而不是編譯錯誤。

讓我們通過一個實際的例子來探索何處可以使用 RefCell<T> 來修改不可變值并看看為何這么做是有意義的。

內(nèi)部可變性的用例:mock 對象

測試替身test double)是一個通用編程概念,它代表一個在測試中替代某個類型的類型。mock 對象 是特定類型的測試替身,它們記錄測試過程中發(fā)生了什么以便可以斷言操作是正確的。

雖然 Rust 中的對象與其他語言中的對象并不是一回事,Rust 也沒有像其他語言那樣在標(biāo)準(zhǔn)庫中內(nèi)建 mock 對象功能,不過我們確實可以創(chuàng)建一個與 mock 對象有著相同功能的結(jié)構(gòu)體。

如下是一個我們想要測試的場景:我們在編寫一個記錄某個值與最大值的差距的庫,并根據(jù)當(dāng)前值與最大值的差距來發(fā)送消息。例如,這個庫可以用于記錄用戶所允許的 API 調(diào)用數(shù)量限額。

該庫只提供記錄與最大值的差距,以及何種情況發(fā)送什么消息的功能。使用此庫的程序則期望提供實際發(fā)送消息的機制:程序可以選擇記錄一條消息、發(fā)送 email、發(fā)送短信等等。庫本身無需知道這些細節(jié);只需實現(xiàn)其提供的 Messenger trait 即可。示例 15-20 展示了庫代碼:

文件名: src/lib.rs

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

示例 15-20:一個記錄某個值與最大值差距的庫,并根據(jù)此值的特定級別發(fā)出警告

這些代碼中一個重要部分是擁有一個方法 send 的 Messenger trait,其獲取一個 self 的不可變引用和文本信息。這個 trait 是 mock 對象所需要實現(xiàn)的接口庫,這樣 mock 就能像一個真正的對象那樣使用了。另一個重要的部分是我們需要測試 LimitTracker 的 set_value 方法的行為??梢愿淖儌鬟f的 value 參數(shù)的值,不過 set_value 并沒有返回任何可供斷言的值。也就是說,如果使用某個實現(xiàn)了 Messenger trait 的值和特定的 max 創(chuàng)建 LimitTracker,當(dāng)傳遞不同 value 值時,消息發(fā)送者應(yīng)被告知發(fā)送合適的消息。

我們所需的 mock 對象是,調(diào)用 send 并不實際發(fā)送 email 或消息,而是只記錄信息被通知要發(fā)送了。可以新建一個 mock 對象實例,用其創(chuàng)建 LimitTracker,調(diào)用 LimitTracker 的 set_value 方法,然后檢查 mock 對象是否有我們期望的消息。示例 15-21 展示了一個如此嘗試的 mock 對象實現(xiàn),不過借用檢查器并不允許:

文件名: src/lib.rs

#[cfg(test)]
mod tests {
    use super::*;

    struct MockMessenger {
        sent_messages: Vec<String>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: vec![],
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.len(), 1);
    }
}

示例 15-21:嘗試實現(xiàn) MockMessenger,借用檢查器不允許這么做

測試代碼定義了一個 MockMessenger 結(jié)構(gòu)體,其 sent_messages 字段為一個 String 值的 Vec 用來記錄被告知發(fā)送的消息。我們還定義了一個關(guān)聯(lián)函數(shù) new 以便于新建從空消息列表開始的 MockMessenger 值。接著為 MockMessenger 實現(xiàn) Messenger trait 這樣就可以為 LimitTracker 提供一個 MockMessenger。在 send 方法的定義中,獲取傳入的消息作為參數(shù)并儲存在 MockMessenger 的 sent_messages 列表中。

在測試中,我們測試了當(dāng) LimitTracker 被告知將 value 設(shè)置為超過 max 值 75% 的某個值。首先新建一個 MockMessenger,其從空消息列表開始。接著新建一個 LimitTracker 并傳遞新建 MockMessenger 的引用和 max 值 100。我們使用值 80 調(diào)用 LimitTracker 的 set_value 方法,這超過了 100 的 75%。接著斷言 MockMessenger 中記錄的消息列表應(yīng)該有一條消息。

然而,這個測試是有問題的:

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
  --> src/lib.rs:58:13
   |
2  |     fn send(&self, msg: &str);
   |             ----- help: consider changing that to be a mutable reference: `&mut self`
...
58 |             self.sent_messages.push(String::from(message));
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` due to previous error
warning: build failed, waiting for other jobs to finish...
error: build failed

不能修改 MockMessenger 來記錄消息,因為 send 方法獲取了 self 的不可變引用。我們也不能參考錯誤文本的建議使用 &mut self 替代,因為這樣 send 的簽名就不符合 Messenger trait 定義中的簽名了(可以試著這么改,看看會出現(xiàn)什么錯誤信息)。

這正是內(nèi)部可變性的用武之地!我們將通過 RefCell 來儲存 sent_messages,然后 send 將能夠修改 sent_messages 并儲存消息。示例 15-22 展示了代碼:

文件名: src/lib.rs

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: RefCell::new(vec![]),
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        // --snip--

        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}

示例 15-22:使用 RefCell<T> 能夠在外部值被認為是不可變的情況下修改內(nèi)部值

現(xiàn)在 sent_messages 字段的類型是 RefCell<Vec<String>> 而不是 Vec<String>。在 new 函數(shù)中新建了一個 RefCell<Vec<String>> 實例替代空 vector。

對于 send 方法的實現(xiàn),第一個參數(shù)仍為 self 的不可變借用,這是符合方法定義的。我們調(diào)用 self.sent_messages 中 RefCell 的 borrow_mut 方法來獲取 RefCell 中值的可變引用,這是一個 vector。接著可以對 vector 的可變引用調(diào)用 push 以便記錄測試過程中看到的消息。

最后必須做出的修改位于斷言中:為了看到其內(nèi)部 vector 中有多少個項,需要調(diào)用 RefCell 的 borrow 以獲取 vector 的不可變引用。

現(xiàn)在我們見識了如何使用 RefCell<T>,讓我們研究一下它怎樣工作的!

RefCell<T> 在運行時記錄借用

當(dāng)創(chuàng)建不可變和可變引用時,我們分別使用 & 和 &mut 語法。對于 RefCell<T> 來說,則是 borrow 和 borrow_mut 方法,這屬于 RefCell<T> 安全 API 的一部分。borrow 方法返回 Ref<T> 類型的智能指針,borrow_mut 方法返回 RefMut<T> 類型的智能指針。這兩個類型都實現(xiàn)了 Deref,所以可以當(dāng)作常規(guī)引用對待。

RefCell<T> 記錄當(dāng)前有多少個活動的 Ref<T> 和 RefMut<T> 智能指針。每次調(diào)用 borrow,RefCell<T> 將活動的不可變借用計數(shù)加一。當(dāng) Ref<T> 值離開作用域時,不可變借用計數(shù)減一。就像編譯時借用規(guī)則一樣,RefCell<T> 在任何時候只允許有多個不可變借用或一個可變借用。

如果我們嘗試違反這些規(guī)則,相比引用時的編譯時錯誤,RefCell<T> 的實現(xiàn)會在運行時出現(xiàn) panic。示例 15-23 展示了對示例 15-22 中 send 實現(xiàn)的修改,這里我們故意嘗試在相同作用域創(chuàng)建兩個可變借用以便演示 RefCell<T> 不允許我們在運行時這么做:

文件名: src/lib.rs

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            let mut one_borrow = self.sent_messages.borrow_mut();
            let mut two_borrow = self.sent_messages.borrow_mut();

            one_borrow.push(String::from(message));
            two_borrow.push(String::from(message));
        }
    }

示例 15-23:在同一作用域中創(chuàng)建兩個可變引用并觀察 RefCell<T> panic

這里為 borrow_mut 返回的 RefMut 智能指針創(chuàng)建了 one_borrow 變量。接著用相同的方式在變量 two_borrow 創(chuàng)建了另一個可變借用。這會在相同作用域中創(chuàng)建兩個可變引用,這是不允許的。當(dāng)運行庫的測試時,示例 15-23 編譯時不會有任何錯誤,不過測試會失?。?br>

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
    Finished test [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests (target/debug/deps/limit_tracker-e599811fa246dbde)

running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED

failures:

---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'main' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::it_sends_an_over_75_percent_warning_message

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

注意代碼 panic 和信息 already borrowed: BorrowMutError。這也就是 RefCell<T> 如何在運行時處理違反借用規(guī)則的情況。

在運行時捕獲借用錯誤而不是編譯時意味著將會在開發(fā)過程的后期才會發(fā)現(xiàn)錯誤,甚至有可能發(fā)布到生產(chǎn)環(huán)境才發(fā)現(xiàn);還會因為在運行時而不是編譯時記錄借用而導(dǎo)致少量的運行時性能懲罰。然而,使用 RefCell 使得在只允許不可變值的上下文中編寫修改自身以記錄消息的 mock 對象成為可能。雖然有取舍,但是我們可以選擇使用 RefCell<T> 來獲得比常規(guī)引用所能提供的更多的功能。

結(jié)合 Rc<T> 和 RefCell<T> 來擁有多個可變數(shù)據(jù)所有者

RefCell<T> 的一個常見用法是與 Rc<T> 結(jié)合。回憶一下 Rc<T> 允許對相同數(shù)據(jù)有多個所有者,不過只能提供數(shù)據(jù)的不可變訪問。如果有一個儲存了 RefCell<T> 的 Rc<T> 的話,就可以得到有多個所有者 并且 可以修改的值了!

例如,回憶示例 15-18 的 cons list 的例子中使用 Rc<T> 使得多個列表共享另一個列表的所有權(quán)。因為 Rc<T> 只存放不可變值,所以一旦創(chuàng)建了這些列表值后就不能修改。讓我們加入 RefCell<T> 來獲得修改列表中值的能力。示例 15-24 展示了通過在 Cons 定義中使用 RefCell<T>,我們就允許修改所有列表中的值了:

文件名: src/main.rs

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {:?}", a);
    println!("b after = {:?}", b);
    println!("c after = {:?}", c);
}

示例 15-24:使用 Rc<RefCell<i32>> 創(chuàng)建可以修改的 List

這里創(chuàng)建了一個 Rc<RefCell<i32>> 實例并儲存在變量 value 中以便之后直接訪問。接著在 a 中用包含 value 的 Cons 成員創(chuàng)建了一個 List。需要克隆 value 以便 a 和 value 都能擁有其內(nèi)部值 5 的所有權(quán),而不是將所有權(quán)從 value 移動到 a 或者讓 a 借用 value。

我們將列表 a 封裝進了 Rc<T> 這樣當(dāng)創(chuàng)建列表 b 和 c 時,他們都可以引用 a,正如示例 15-18 一樣。

一旦創(chuàng)建了列表 a、b 和 c,我們將 value 的值加 10。為此對 value 調(diào)用了 borrow_mut,這里使用了第五章討論的自動解引用功能(“-> 運算符到哪去了?” 部分)來解引用 Rc<T> 以獲取其內(nèi)部的 RefCell<T> 值。borrow_mut 方法返回 RefMut<T> 智能指針,可以對其使用解引用運算符并修改其內(nèi)部值。

當(dāng)我們打印出 a、b 和 c 時,可以看到他們都擁有修改后的值 15 而不是 5:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished dev [unoptimized + debuginfo] target(s) in 0.63s
     Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))

這是非常巧妙的!通過使用 RefCell<T>,我們可以擁有一個表面上不可變的 List,不過可以使用 RefCell<T> 中提供內(nèi)部可變性的方法來在需要時修改數(shù)據(jù)。RefCell<T> 的運行時借用規(guī)則檢查也確實保護我們免于出現(xiàn)數(shù)據(jù)競爭——有時為了數(shù)據(jù)結(jié)構(gòu)的靈活性而付出一些性能是值得的。

標(biāo)準(zhǔn)庫中也有其他提供內(nèi)部可變性的類型,比如 Cell<T>,它類似 RefCell<T> 但有一點除外:它并非提供內(nèi)部值的引用,而是把值拷貝進和拷貝出 Cell<T>。還有 Mutex<T>,其提供線程間安全的內(nèi)部可變性,我們將在第 16 章中討論其用法。請查看標(biāo)準(zhǔn)庫來獲取更多細節(jié)關(guān)于這些不同類型之間的區(qū)別。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號