返回文章列表

Rust 實用特性深入解析

本文探討 Rust 語言中重要的實用特性,包含 `Drop`、`Sized`、`Clone` 和 `Copy` 等,並詳細說明 `Deref` 和 `DerefMut` 特性如何實作解參照強制轉換,以及使用這些特性時需要注意的事項。文章將透過程式碼範例和原理剖析,幫助讀者理解這些特性的作用和使用方法,進而提升

程式語言 Rust

Rust 提供了許多實用特性,讓開發者能更有效地管理資源和控制型別。Drop 特性允許自定義值的清理邏輯,確保資源正確釋放。Sized 特性區分固定大小和動態大小型別,影響變數儲存和函式引數傳遞方式。CloneCopy 特性則提供複製機制,Clone 適用於需要深複製的型別,而 Copy 適用於淺複製的型別。理解這些特性對於編寫高效且安全的 Rust 程式碼至關重要。DerefDerefMut 特性則允許自定義解參照運算元的行為,常被用於智慧指標的實作,讓開發者能像使用原生指標一樣操作智慧指標,同時享有資源自動管理的優勢。然而,使用這些特性時需要注意解參照強制轉換的限制,避免在泛型程式碼中產生錯誤。

實用特性(Utility Traits)

科學的本質不過是在自然界的紛繁多樣性中,或者更準確地說,在我們經驗的多樣性中,尋找統一性。詩歌、繪畫和藝術同樣是在尋找這種統一性,用柯勒律治的話來說,就是在多樣性中尋找統一。 —— 雅各布·布羅諾夫斯基(Jacob Bronowski)

除了運算元多載(operator overloading),我們在前一章中已經討論過,Rust 語言和標準函式庫中還有其他幾個內建的特性(traits),讓你可以與語言的某些部分掛鉤:

  • 你可以使用 Drop 特性來在值超出範圍時清理它們,就像 C++ 中的解構函式(destructors)。
  • Box<T>Rc<T> 這樣的智慧型指標型別可以實作 Deref 特性,使指標反映被包裹值的函式方法。
  • 透過實作 From<T>Into<T> 特性,你可以告訴 Rust 如何將一個型別的值轉換成另一個型別。

本章是 Rust 標準函式庫中有用的特性的集合。我們將介紹表 13-1 中顯示的每個特性。

表 13-1. 實用特性總結

特性描述
Drop解構函式。當值被丟棄時,Rust 自動執行的清理程式碼。
Sized標記特性,用於在編譯時具有固定大小的型別,而不是像切片(slices)這樣的動態大小的型別。
Clone支援複製值的型別。
Copy標記特性,用於可以透過簡單地對包含值的記憶體進行位元組對位元組複製來克隆的型別。
DerefDerefMut用於智慧型指標型別的特性。
Default具有合理的「預設值」的型別。
AsRefAsMut用於從另一種型別借用參考的轉換特性。
BorrowBorrowMut轉換特性,像 AsRef/AsMut,但額外保證一致的雜湊、排序和平等性。
FromInto用於將一種型別的值轉換成另一種的轉換特性。
ToOwned用於將參考轉換成擁有值的轉換特性。

Drop

當一個值的擁有者消失時,我們說 Rust 丟棄了這個值。丟棄一個值意味著釋放該值擁有的其他值、堆積疊儲存和系統資源。當變數超出範圍時;當表示式的值被;運算元丟棄時;當你截斷一個向量,從其末端移除元素時;等等,都會發生丟棄。

在大多數情況下,Rust 會自動為你處理值的丟棄。例如,假設你定義了以下型別:

struct Appellation {
    name: String,
    nicknames: Vec<String>
}

一個 Appellation 擁有字串內容和向量緩衝區的堆積疊儲存。Rust 會在 Appellation 被丟棄時清理所有這些,而無需你進一步編寫程式碼。但是,如果你願意,你可以透過實作 std::ops::Drop 特性來自定義 Rust 如何丟棄你的型別的值:

trait Drop {
    fn drop(&mut self);
}

內容解密:

  • Drop 特性允許你定義當一個值被丟棄時應該執行的程式碼。
  • Rust 在丟棄值之前會呼叫其 drop 方法,然後再正常地丟棄其欄位或元素所擁有的值。
  • 你不能顯式呼叫 drop 方法;Rust 會在適當的時候自動呼叫它。

對於我們的 Appellation 型別,實作 Drop 可以充分利用其欄位:

impl Drop for Appellation {
    fn drop(&mut self) {
        print!("Dropping {}", self.name);
        if !self.nicknames.is_empty() {
            print!(" (AKA {})", self.nicknames.join(", "));
        }
        println!("");
    }
}

給定這個實作,我們可以寫出以下程式碼:

{
    let mut a = Appellation { 
        name: "Zeus".to_string(),
        nicknames: vec!["cloud collector".to_string(),
                        "king of the gods".to_string()] 
    };
    println!("before assignment");
    a = Appellation { 
        name: "Hera".to_string(), 
        nicknames: vec![] 
    };
    println!("at end of block");
}

這段程式碼會輸出:

before assignment
Dropping Zeus (AKA cloud collector, king of the gods)
at end of block
Dropping Hera

我們的 Appellationstd::ops::Drop 實作只是列印了一條訊息,那麼它的記憶體究竟是如何被清理的呢?事實上,Vec 型別實作了 Drop,它會丟棄其每個元素,然後釋放它們佔用的堆積疊分配緩衝區。String 在內部使用 Vec<u8> 來儲存其文字,所以 String 不需要自己實作 Drop;它讓其 Vec 處理字元的釋放。同樣的原理也適用於 Appellation 值:當一個被丟棄時,最終是 VecDrop 實作負責釋放每個字串的內容,最後釋放儲存向量元素的緩衝區。至於儲存 Appellation 值本身的記憶體,它也有某個擁有者,也許是一個區域性變數或某個資料結構,它負責釋放它。

程式碼解析:

  1. struct Appellation:定義了一個名為 Appellation 的結構體,包含一個名稱和一些別名。
  2. impl Drop for Appellation:為 Appellation 實作了 Drop 特性,定義了當一個 Appellation 值被丟棄時要執行的操作。
  3. fn drop(&mut self):在這個方法中,我們列印了正在被丟棄的 Appellation 的名稱和別名(如果有的話)。
  4. let mut a = Appellation { ... }:建立了一個新的 Appellation 值並賦給變數 a
  5. a = Appellation { ... }:將一個新的 Appellation 值賦給 a,導致原來的 Appellation 值被丟棄,並觸發了我們為其定義的 drop 方法。

邏輯解析:

  • 當變數超出範圍或被重新指定時,原有的值會被丟棄。
  • Rust 自動呼叫值的 drop 方法(如果該值實作了 Drop 特性),然後再處理其成員變數的釋放。
  • 在這個例子中,我們利用這一機制列印了相關訊息,但實際應用中可以執行任何必要的清理工作,如關閉檔案、釋放資源等。

透過這種方式,Rust 提供了一種方便且可靠的方法來管理資源和清理工作,使得程式設計更加安全和高效。

Rust 中的 DropSized 特性

Rust 語言提供了多種特性(trait)以便開發者能夠更好地控制和管理資源。其中,DropSized 是兩個非常重要的特性,分別用於資源管理和型別大小的定義。

Drop 特性:資源管理

Drop 特性允許開發者定義當一個值超出其作用域時應該執行的清理操作。這對於管理那些需要明確釋放的資源(如檔案描述符、網路連線等)至關重要。

Drop 的工作原理

當一個變數超出其作用域時,Rust 會自動呼叫其 drop 方法(如果該型別實作了 Drop 特性)。這確保了資源能夠被正確地釋放。

struct FileDesc {
    fd: i32,
}

impl Drop for FileDesc {
    fn drop(&mut self) {
        let _ = unsafe { libc::close(self.fd) };
    }
}

在上述例子中,FileDesc 結構體代表了一個檔案描述符,並實作了 Drop 特性。當 FileDesc 的例項超出作用域時,其 drop 方法會被呼叫,從而關閉檔案描述符。

資源移動與 Drop

Rust 確保每個值只被丟棄一次,即使該值在不同的變數之間移動。

let p;
{
    let q = Appellation { 
        name: "Cardamine hirsuta".to_string(),
        nicknames: vec!["shotweed".to_string(), "bittercress".to_string()] 
    };
    if complicated_condition() {
        p = q;
    }
}
println!("Sproing! What was that?");

根據 complicated_condition() 的結果,pq 將擁有 Appellation 例項,而另一個將變成未初始化狀態。Rust 內部使用一個不可見的旗標來跟蹤變數的狀態,以確保資源被正確地丟棄。

Sized 特性:型別大小

大多數 Rust 型別都是大小固定的,也就是說它們在記憶體中佔用的大小是固定的。然而,也有一些型別是大小不固定的,例如字串切片(str)和陣列切片([T])。

大小不固定的型別

大小不固定的型別不能直接儲存在變數中或作為函式的引數傳遞。必須透過指標(如 &strBox<dyn Write>)來間接操作它們。

let s: &str = "diminutive";
let slice: &[u8] = b"big";

在上述例子中,sslice 分別是對字串切片和位元組切片的參照。這些參照本身是大小固定的,但它們所指向的資料大小是不固定的。

Trait 物件與大小不固定的型別

Trait 物件(如 &dyn WriteBox<dyn Write>)也是一種大小不固定的型別。它們指向實作了特定 trait 的值,但這些值的大小可能不同。

使用 Sized 特性

預設情況下,泛型型別變數被限制為大小固定的型別。如果需要允許大小不固定的型別,可以使用 ?Sized 約束。

struct S<T: ?Sized> { 
    b: Box<T> 
}

這樣,S 可以用於大小固定和大小不固定的型別。

無大小型別與 Clone、Copy 特性

在 Rust 中,除了切片和特徵物件之外,還有一種無大小(unsized)型別。結構體型別的最後一個欄位(但僅限於最後一個)可以是無大小的,這使得該結構體本身也是無大小的。

內容解密:
  • ref_count 欄位儲存了參照計數,用於追蹤有多少個 Rc<T> 指標指向同一個 T 值。
  • value 欄位儲存了實際的 T 值。
  • T: ?Sized 表示 T 可以是任何大小的型別,包括無大小型別。

你可以使用 RcBox 與有大小的型別,如 RcBox<String>,結果是一個有大小的結構體型別。或者,你也可以使用它與無大小的型別,如 RcBox<std::fmt::Display>,此時 RcBox<Display> 是一個無大小的結構體型別。

從有大小型別轉換到無大小型別

你無法直接建立一個 RcBox<Display> 值,而是必須先建立一個普通的、有大小的 RcBox,其值型別實作了 Display 特徵,如 RcBox<String>。然後,Rust 允許你將一個對 RcBox<String> 的參照轉換為一個對 RcBox<Display> 的胖參照:

let boxed_lunch: RcBox<String> = RcBox {
    ref_count: 1,
    value: "lunch".to_string()
};
use std::fmt::Display;
let boxed_displayable: &RcBox<Display> = &boxed_lunch;

內容解密:

  • 首先建立了一個 RcBox<String> 例項 boxed_lunch
  • 然後將對 boxed_lunch 的參照轉換為對 RcBox<Display> 的參照,因為 String 實作了 Display 特徵。

這種轉換在傳遞值給函式時會隱式發生,因此你可以將一個 &RcBox<String> 傳遞給期望 &RcBox<Display> 的函式:

fn display(boxed: &RcBox<Display>) {
    println!("For your enjoyment: {}", &boxed.value);
}
display(&boxed_lunch);

輸出結果為:

For your enjoyment: lunch

Clone 特性

std::clone::Clone 特性用於可以複製自己的型別。Clone 特性的定義如下:

trait Clone: Sized {
    fn clone(&self) -> Self;
    fn clone_from(&mut self, source: &Self) {
        *self = source.clone()
    }
}

Clone 特性的實作

  • clone 方法應該建立一個獨立的 self 複製並傳回它。由於這個方法的傳回型別是 Self,而函式不能傳回無大小的值,因此 Clone 特性本身擴充套件了 Sized 特性,這使得實作的 Self 型別必須是有大小的。
  • 複製一個值通常需要分配它所擁有的資源的副本,因此複製可能很昂貴,無論是在時間還是記憶體方面。例如,複製一個 Vec<String> 不僅複製了向量,還複製了它的每個 String 元素。

Deref 和 DerefMut 特性

你可以透過實作 std::ops::Derefstd::ops::DerefMut 特性來指定解參照運算元(如 *.)在你的型別上的行為。像 Box<T>Rc<T> 這樣的指標型別實作了這些特性,以便它們可以像 Rust 的內建指標型別一樣運作。

Copy 特性

在 Rust 中,指定通常會移動值,而不是複製它們。然而,簡單的型別如果不擁有任何資源,可以是 Copy 型別,此時指定會複製源值,而不是移動值並使源值未初始化。

Copy 特性的定義

trait Copy: Clone { }

要使你的型別成為 Copy,需要實作 Copy 特性。但是,由於 Copy 是一個具有特殊含義的標記特性,Rust 只允許那些只需要淺層位元組複製的型別實作 Copy。擁有其他資源(如堆積疊緩衝區或作業系統控制程式碼)的型別不能實作 Copy

任何實作了 Drop 特性的型別都不能是 Copy。Rust 推定,如果一個型別需要特殊的清理程式碼,那麼它也需要特殊的複製程式碼,因此不能是 Copy

Deref 與 DerefMut 特性的應用與實作

在 Rust 程式語言中,DerefDerefMut 特性扮演著非常重要的角色,它們主要用於實作智慧指標(smart pointer)型別,如 BoxRcArc 等。這些特性使得 Rust 能夠自動進行解參照(dereference),進而簡化程式碼的撰寫。

Deref 特性

Deref 特性定義了一個方法 deref,該方法接受一個 &Self 參考並傳回一個 &Self::Target 參考。這裡的 Target 型別應該是 Self 所包含、擁有或參照的型別。例如,對於 Box<Complex>Target 型別就是 Complex

Deref 特性的實作範例

use std::ops::Deref;

struct Selector<T> {
    elements: Vec<T>,
    current: usize,
}

impl<T> Deref for Selector<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.elements[self.current]
    }
}

DerefMut 特性

DerefMut 特性擴充套件了 Deref,它提供了一個 deref_mut 方法,用於傳回一個可變的參考 &mut Self::Target。這使得我們可以修改被參照的值。

DerefMut 特性的實作範例

use std::ops::{Deref, DerefMut};

impl<T> DerefMut for Selector<T> {
    fn deref_mut(&mut self) -> &mut T {
        &mut self.elements[self.current]
    }
}

解參照強制轉換(Deref Coercions)

Rust 利用 DerefDerefMut 特性進行解參照強制轉換。當 Rust 發現需要進行型別轉換以解決型別衝突時,它會自動插入 derefderef_mut 呼叫。這使得我們可以更方便地使用智慧指標。

解參照強制轉換的範例

let mut s = Selector { elements: vec!['x', 'y', 'z'], current: 2 };

assert_eq!(*s, 'z'); // 使用 * 運算子直接存取當前元素
assert!(s.is_alphabetic()); // 直接呼叫 char 的方法
*s = 'w'; // 修改當前元素的值
assert_eq!(s.elements, ['x', 'y', 'w']);

使用 Deref 和 DerefMut 的注意事項

雖然 DerefDerefMut 特性非常有用,但不應該濫用它們來讓某個型別的所有方法自動出現在另一個型別上。這可能會導致混淆和不可預期的行為。

範例:泛型函式中的解參照強制轉換問題

fn show_it(thing: &str) {
    println!("{}", thing);
}

let s = Selector { elements: vec!["good", "bad", "ugly"], current: 2 };
show_it(&s); // 正確,Rust 自動進行了解參照強制轉換

// 但是,如果將 show_it 改為泛型函式
fn show_it_generic<T: Display>(thing: &T) {
    println!("{}", thing);
}

// 則會出現錯誤,因為 Rust 不會對型別變數的限制進行解參照強制轉換
// show_it_generic(&s); // 錯誤!
// 需要手動進行轉換:show_it_generic(&s as &str);

內容解密:

  1. Deref 特性的定義:定義了 deref 方法,用於傳回一個分享參考。
  2. DerefMut 特性的定義:擴充套件了 Deref,提供了 deref_mut 方法,用於傳回一個可變參考。
  3. 解參照強制轉換的作用:簡化了智慧指標的使用,使得程式碼更簡潔。
  4. 實作範例:展示瞭如何為自定義型別 Selector 實作 DerefDerefMut
  5. 使用注意事項:避免濫用這些特性,以防止不可預期的行為。