CRust 學習筆記:丟棄檢查 -Drop Check--1

本系列文章是 Jon Gjengset 發佈的 CRust of Rust 系列視頻的學習筆記,CRust of Rust 是一系列持續更新的 Rust 中級教程。

在這篇文章中,我們講解丟棄檢查。在程序中我們定義的變量都有其作用域,當變量超出作用域時,就會被丟棄。多個變量則按其定義的相反順序丟棄,結構體和元組中的字段按其定義的順序丟棄。

正常情況下,編譯器的借用檢查器會做相應的檢查。那我們爲什麼還需要研究丟棄檢查,是因爲如果類型系統不小心,它可能會意外地產生懸垂指針。

下面我們通過一些實際的例子來深入學習丟棄檢查,新建一個項目:

cargo new boks

在 main.rs 中寫入如下代碼:

pub struct Boks<T> {
    p: *mut T,
}

impl<T> Boks<T> {
    pub fn ny(t: T) -> Self {
        Boks {
            p: Box::into_raw(Box::new(t)),
        }
    }
}

fn main() {
    let x = 42;
    let b = Boks::ny(x);
}

Boks 的成員 p 是一個可變原生指針,在 ny(t: T) 函數中通過 Box::into_raw() 獲取了 t 的原生指針。

在 main 函數中,當變量 b 超出作用域後,並不會自動釋放原生指針指向的內存,會產生內存泄漏。因此需要我們手動去實現 Drop trait,來釋放內存。

impl<T> Drop for Boks<T> {
    fn drop(&mut self) {
        // unsafe { std::ptr::drop_in_place(self.p) };
        unsafe { Box::from_raw(self.p) };
    }
}

要想使用 p,我們還需要實現解引用的兩個 trait:Deref 和 DerefMut。

impl<T> std::ops::Deref for Boks<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        unsafe { &*self.p }
    }
}

impl<T> std::ops::DerefMut for Boks<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        unsafe { &mut *self.p }
    }
}

我們測試一下:

fn main() {
    let x = 42;
    let b = Boks::ny(x);
    println!("{:?}", *b);
}

結果打印出 42。

現在我們有了可以工作的基礎代碼,下一步我們繼續研究丟棄檢查。看如下代碼:

fn main() {
    ......
    let mut y = 42;
    let b = Boks::ny(&mut y);
    println!("{y}");
}

運行 cargo check:

error[E0502]: cannot borrow `y` as immutable because it is also borrowed as mutable
  --> src/main.rs:44:16
   |
42 |     let b = Boks::ny(&mut y);
   |                      ------ mutable borrow occurs here
43 |
44 |     println!("{y}");
   |                ^ immutable borrow occurs here
45 | }
   |  - mutable borrow might be used here, when `b` is dropped and runs the `Drop` code for type `Boks`

我們看最後一行的錯誤信息,提示當 b 超出作用域時執行 Drop 特徵的 drop 方法時,編譯器會認爲可能會在 drop 方法中有可變借用的情況,與上一行的不可變借用衝突。這就是編譯器的丟棄檢查 (Drop Check)。

由於 drop 方法是我們自己寫的,難免會在 drop 方法中訪問內部變量的可變引用,如:

impl<T> Drop for Boks<T> {
    fn drop(&mut self) {
        let _ = unsafe { std::ptr::read(self.p as *const u8) };

        // unsafe { std::ptr::drop_in_place(self.p) };
        unsafe { Box::from_raw(self.p) };
    }
}

但是編譯器不知道,所以編譯器會進行丟棄檢查,即無論何種情況,都假設認爲訪問了內部變量的可變引用。

如果換成標準庫的 Box 就不會報錯:

fn main() {
    ......
    let mut y = 42;
    // let b = Boks::ny(&mut y);
    let b = Box::new(&mut y);
    println!("{y}");
}

執行 cargo run,結果爲 42。

這是因爲編譯器知道標準庫的 Box 超出作用域執行 drop 方法時,會釋放潛在的內部變量內存,不會訪問內部變量的可變引用。

如果不實現 Drop 特徵的 drop 方法,編譯器就不會報錯,但是會造成內存泄漏。在下一篇文章中我們將解決這個問題。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vT3fczBF6G8G0nmsEqbqPg