# Rust - 介面設計建議之靈活(flexible) ## 靈活(flexible) ### 代碼的契約(Contract) - 你寫的代碼包含契約 - 契約: - 要求:代碼使用的限制 - 承諾:代碼使用的保證 - 設計介面時(經驗法則): - 避免施加不必要的限制,只做能夠兌現的承諾 - 增 ...
Rust - 介面設計建議之靈活(flexible)
靈活(flexible)
代碼的契約(Contract)
- 你寫的代碼包含契約
- 契約:
- 要求:代碼使用的限制
- 承諾:代碼使用的保證
- 設計介面時(經驗法則):
- 避免施加不必要的限制,只做能夠兌現的承諾
- 增加限制 或 取消承諾:
- 重大的語義版本更改
- 可導致其他代碼出問題
- 放寬限制 或 提供額外的承諾:
- 通常是向後相容的
- 增加限制 或 取消承諾:
- 避免施加不必要的限制,只做能夠兌現的承諾
限制(Restrictions)與承諾(Promises)
- Rust中,限制的常見形式:
- Trait 約束(Trait Bound)
- 參數類型(Argument Types)
- 承諾的常見形式:
- Trait 的實現
- 返回類型
fn frobnicate1(s: String) -> String
- 契約:調用者進行記憶體分配,承諾返回擁有的 String -> 無法改為 “無需記憶體分配” 的函數
fn frobnicate2(s: &str) -> Cow<'_, str>
- 放寬了契約:只接收字元串的引用,承諾返回字元串的引用或一個擁有的 String
fn frobnicate3(s: impl AsRef<str>) -> impl AsRef<str>
- 進一步放寬契約:要求傳入能產生字元串引用的類型,承諾返回值可產生字元串引用
例子一
use std::borrow::Cow;
fn frobnicate3<T: AsRef<str>>(s: T) -> T {
s
}
fn main() {
let string = String::from("example");
let borrowed: &str = "hello";
let cow: Cow<str> = Cow::Borrowed("world");
let result1: &str = frobnicate3::<&str>(string.as_ref());
let result2: &str = frobnicate3::<&str>(borrowed);
let result3 = frobnicate3(cow);
println!("Result1: {:?}", result1);
println!("Result2: {:?}", result2);
println!("Result3: {:?}", result3);
}
- 都傳入字元串,返回字元串,但契約不同
- 沒有更好。要仔細規劃契約,否則改變契約會引起破壞
泛型參數(Generic Arguments)
- 通過泛型放寬對函數的要求
- 大多數情況下值得使用泛型代替具體類型
例子二
// 你有一個函數,它接受一個實現了 AsRef<str> trait 的參數
fn print_as_str<T: AsRef<str>>(s: T) {
println!("{}", s.as_ref());
}
// 這個函數是泛型的,它對 T 進行了泛型化,
// 這意味著它會對你使用它的每一種實現了 AsRef<str> 的類型進行單態化。
// 例如,如果你用一個 String 和一個 &str 來調用它,
// 你就會在你的二進位文件中有兩份函數的拷貝:
fn main() {
let s = String::from("hello");
let r = "world";
print_as_str(s); // 調用 print_as_str::<String>
print_as_str(r); // 調用 print_as_str::<&str>
}
例子三
// 為了避免這種重覆,你可以把函數改成接受一個 &dyn AsRef<str>:
fn print_as_str(s: &dyn AsRef<str>) {
println!("{}", s.as_ref());
}
// 這個函數不再是泛型的,它接受一個 trait 對象,
// 它可以是任何實現了 AsRef<str> 的類型
// 這意味著它會在運行時使用動態分發來調用 as_ref 方法,
// 並且你只會在你的二進位文件中有一份函數的拷貝:
fn main() {
let s = String::from("hello");
let r = "world";
print_as_str(&s); // 傳遞一個類型為 &dyn AsRef<str> 的 trait 對象
print_as_str(&r); // 傳遞一個類型為 &dyn AsRef<str> 的 trait 對象
}
- 不要走極端
- 經驗法則:
- 用戶合理、頻繁的使用其他類型代替你最初選定的類型,那麼參數定義為泛型更合適
- 問題:通過單態化(monomorphization),會為每個使用泛型代碼的類型組合生成泛型代碼的副本
- 擔心:讓很多參數變成泛型 --> 二進位文件過大
- 解決:動態分發(dynamic dispatch),以忽略不計的性能成本來緩解這個問題
- 對於以引用方式獲取的參數(dyn Trait 不是 Sized 的,需要使用寬指針來使用它們),可以使用動態分發代替泛型參數
例子四
// 假設我們有一個名為 process 的泛型函數,它接受一個類型參數 T 並對其執行某些操作:
fn process<T>(value: T) {
// 處理 value 的代碼
println!("處理 T");
}
// 上述函數使用靜態分發,這意味著在編譯時將為每個具體類型 T 生成相應的實現。
// 現在,假設調用者想要提供動態分發的方式,允許在運行時選擇實現。
// 它們可以通過傳遞 Trait 對象作為參數,
// 使用 dyn 關鍵字來實現。以下是一個例子:
trait Processable {
fn process(&self);
}
struct TypeA;
impl Processable for TypeA {
fn process(&self) {
println!("處理 TypeA");
}
}
struct TypeB;
impl Processable for TypeB {
fn process(&self) {
println!("處理 TypeB");
}
}
fn process_trait_object(value: &dyn Processable) {
value.process();
}
// 如果調用者想要使用動態分發併在運行時選擇實現,
// 它們可以調用 process_trait_object 函數,並傳遞 Trait 對象作為參數。
// 調用者可以根據需求選擇要提供的具體實現:
fn main() {
let a = TypeA;
let b = TypeB;
process_trait_object(&a);
process_trait_object(&b);
process(&a);
process(&b);
process(&a as &dyn Processable);
process(&b as &dyn Processable);
}
- 使用動態分發(dynamic dispatch):
- 代碼不會對性能敏感:可以接受
- 在高性能應用中:在頻繁調用的熱迴圈中使用動態分發可能會成為一個致命問題
- 在撰寫本文時,只有在簡單的 Trait 約束時,才能使用動態分發
- 如
T: AsRef<str>
或impl AsRef<str>
- 如
- 對於更複雜的約束,Rust 無法構造動態分發的虛函數表(vtable)
- 因此無法使用類似
&dyn Hash + Eq
這樣的組合約束。
- 因此無法使用類似
- 使用泛型時,調用者始終可以通過傳遞一個 Trait 對象來選擇動態分發
- 反過來不成立:如果你接受一個 Trait 對象作為參數,那麼調用者必須提供 Trait 對象,而無法選擇使用靜態分發
- 從具體類型開始編寫介面,然後逐漸將它們轉換為泛型
- 可行,但不一定是向下相容
例子五
fn foo(v: &Vec<usize>) {
// 處理 v 的代碼
// ...
}
// 現在,我們決定將函數改為使用 Trait 限定 AsRef<[usize]>,
// 即 impl AsRef<[usize]>:
// fn foo(v: impl AsRef<[usize]>) {
// // 處理 v 的代碼
// // ...
// }
fn main() {
let iter = vec![1, 2, 3].into_iter();
foo(&iter.collect());
}
// 在原始版本中,編譯器可以推斷出 iter.collect() 應該收集為一個 Vec<usize> 類型,
// 因為我們將其傳遞給了接受 &Vec<usize> 的 foo 函數。
// 然而,在更改為使用特質限定後,編譯器只知道 foo 函數
// 接受一個實現了 AsRef<[usize]> 特質的類型。
// 這裡有多個類型滿足這個條件,例如 Vec<usize> 和 &[usize]。
// 因此,編譯器無法確定應該將 iter.collect() 的結果解釋為哪個具體類型。
// 這樣的更改將導致編譯器無法推斷類型,並且調用者的代碼將無法通過編譯。
// 為瞭解決這個問題,調用者可能需要顯示指定期望的類型,例如:
// let iter = vec![1, 2, 3].into_iter();
// foo(&iter.collect::<Vec<usize>>());
泛型的優點
-
可復用:泛型函數能應用在廣泛的類型上,同時明確給出了這些類型的必須滿足的關係。
-
靜態分派和編譯器優化: 每個泛型函數都被專門用於實現了 trait bounds 的具體的類型 (即 單態化 monomorphized ),這意味著:
- 調用的 trait 方法是靜態生成的,因此是直接對 trait 實現的調用
- 編譯器能對這些調用做內聯 (inline) 和其他優化
-
內聯式佈局:如果結構體和枚舉體類型具有某個泛型參數
T
,T
的值將在結構體和枚舉體里以內聯方式排列,不產生任何間接調用。 -
可推斷:由於泛型函數的類型參數通常是推斷出來的, 泛型函數可以減少複雜的代碼,比如顯式轉換、通常必須的一些方法調用。
-
精確的類型:因為泛型給實現了某個 trait 的具體類型一個名稱, 從而有可能清楚這個類型需要或創建的地方在哪。比如這個函數:
fn binary<T: Trait>(x: T, y: T) -> T
會保證消耗和創建具有相同類型
T
的值;不可能傳入實現了Trait
的但不同名稱的兩個類型。
泛型的缺點
- 增加代碼大小:單態化泛型函數意味著函數體會被覆制。 增加代碼大小和靜態分派的性能優勢之間必須做出衡量。
- 類型同質化:這是 “精確的類型” 帶來的另一面: 如果
T
是類型參數,那麼它代表一個單獨的實際類型。 對於像Vec<T>
這樣具體的單獨的元素類型也是一樣, 而且Vec
實際上為了內聯這些元素,進行了專門的處理。 有時候,不同的類型會更有用,參考 trait objects 。 - 簽名冗餘:過度使用泛型會造成閱讀和理解函數簽名更困難。
The Rust RFC Book:https://rust-lang.github.io/rfcs/introduction.html
對象安全(Object Safety)
- 定義 Trait 時,它是否對象安全,也是契約未寫明的一部分
- 如果 Trait 是對象安全的:
- 可使用 dyn Trait 將實現該 Trait 的不同類型視為單一通用類型
- 如果 Trait 不是對象安全的:
- 編譯器會禁止使用 dyn Traie
- 建議 Trait 是對象安全的(即使稍微降低使用的便利程度):
- 提供了使用的新方式和靈活性
對象安全:描述一個 Trait 可否安全的包裝成 Trait Object
對象安全的 Trait 是滿足以下條件的 Trait(RFC 255):
- 所有的 supertrait 必須是對象安全的
- Sized 不能作為 supertrait(不能要求 Self: Sized)
- 不能有任何關聯常量
- 不能有任何帶有泛型的關聯類型
- 所有的關聯函數必須滿足以下條件之一:
- 可以從 Trait 對象分發的函數(Dispatchable functions):
- 沒有任何類型參數(生命周期參數是允許的)
- 是一個方法,只在接收器類型中使用 Self
- 接收器是以下類型之一:
&Self
(即 &self)&mut Self
(即&mut self
)Box<Self>
Rc<Self>
Arc<Self>
Pin<P>
,其中 P 是上述類型之一
- 沒有
where Self: Sized
約束(Self 的接收器類型(即 self)暗含了這一點)
- 顯示不可分發的函數(non-dispatchable functions)要求:
- 具有
where Self: Sized
約束(Self 的接收器類型(即 self)暗含了這一點)
- 具有
- 可以從 Trait 對象分發的函數(Dispatchable functions):
例子六
// 假設我們有一個 Animal 特征,它有兩個方法:name 和 speak。
// name 方法返回一個&str,表示動物的名字;
// speak 方法列印出動物發出的聲音。
// 我們可以為 Dog 和 Cat 類型實現這個特征:
trait Animal {
fn name(&self) -> &str;
fn speak(&self);
}
struct Dog {
name: String,
}
impl Animal for Dog {
fn name(&self) -> &str {
&self.name
}
fn speak(&self) {
println!("Woof!");
}
}
struct Cat {
name: String,
}
impl Animal for Cat {
fn name(&self) -> &str {
&self.name
}
fn speak(&self) {
println!("Meow!");
}
}
// 這個 Animal 特征是 object-safe 的,因為它沒有返回 Self 類型或使用泛型參數。
// 所以我們可以用它來創建一個 trait object:
fn main() {
let dog = Dog {
name: "Fido".to_string(),
};
let cat = Cat {
name: "Whiskers".to_string(),
};
let animals: Vec<&dyn Animal> = vec![&dog, &cat];
for animal in animals {
println!("This is {}", animal.name());
animal.speak();
}
}
// 這樣我們就可以用一個統一的類型 Vec<&dyn Animal> 來存儲不同類型的動物,
// 並且通過 trait object 來調用它們的方法。
例子七
// 但是如果我們給 Animal 特征添加一個新的方法 clone,它返回一個 Self 類型:
trait Animal {
fn name(&self) -> &str;
fn speak(&self);
fn clone(&self) -> Self;
}
// 那麼這個特征就不再是 object-safe 的了,
// 因為 clone 方法違反了規則:返回類型不能是 Self。
// 這樣我們就不能用它來創建 trait object 了,
// 因為編譯器無法知道 Self 具體指代哪個類型
struct Dog {
name: String,
}
impl Animal for Dog {
fn name(&self) -> &str {
&self.name
}
fn speak(&self) {
println!("Woof!");
}
fn clone(&self) -> Self
where
Self: Sized,
{
todo!()
}
}
struct Cat {
name: String,
}
impl Animal for Cat {
fn name(&self) -> &str {
&self.name
}
fn speak(&self) {
println!("Meow!");
}
fn clone(&self) -> Self
where
Self: Sized,
{
todo!()
}
}
fn main() {
let dog = Dog {
name: "Fido".to_string(),
};
let cat = Cat {
name: "Whiskers".to_string(),
};
let animals: Vec<&dyn Animal> = vec![&dog, &cat]; // 報錯 the trait `Animal` cannot be made into an object consider moving `clone` to another trait
for animal in animals {
println!("This is {}", animal.name());
animal.speak();
}
}
例子八
// 如果我們想讓 Animal 特征保持 object-safe,
// 我們就不能給它添加返回 Self 類型的方法。
// 或者,我們可以給 clone 方法添加一個 where Self: Sized 的特征界定,
// 這樣他就只能在具體類型上調用,而不是在 trait object 上:
trait Animal {
fn name(&self) -> &str;
fn speak(&self);
fn clone(&self) -> Self
where
Self: Sized;
}
struct Dog {
name: String,
}
impl Animal for Dog {
fn name(&self) -> &str {
&self.name
}
fn speak(&self) {
println!("Woof!");
}
fn clone(&self) -> Self
where
Self: Sized,
{
todo!()
}
}
struct Cat {
name: String,
}
impl Animal for Cat {
fn name(&self) -> &str {
&self.name
}
fn speak(&self) {
println!("Meow!");
}
fn clone(&self) -> Self
where
Self: Sized,
{
todo!()
}
}
// 這樣我們就可以繼續用 Animal 特征來創建 trait object 了,
// 但是我們不能用 trait object 來調用 clone 方法
fn main() {
let dog = Dog {
name: "Fido".to_string(),
};
let cat = Cat {
name: "Whiskers".to_string(),
};
cat.clone(); // 只能在具體的類型上調用
let animals: Vec<&dyn Animal> = vec![&dog, &cat];
for animal in animals {
println!("This is {}", animal.name());
animal.speak();
animal.clone(); // 報錯 the `clone` method cannot be invoked on a trait object
}
}
- 如果 Trait 必須有泛型方法,考慮:
- 泛型參數放在 Trait 上
- 泛型參數可否使用動態分發,來保證Trait 的對象安全
例子九
use std::collections::HashSet;
use std::hash::Hash;
// 將泛型參數放在 Trait 本身上
trait Container<T> {
fn contains(&self, item: &T) -> bool;
}
// 我們可以為不同的容器類型實現 Container Trait,每個實現都具有自己特定的元素類型。
// 例,我們可以為 Vec<T> 和 HashSet<T> 實現 Container Trait:
impl<T> Container<T> for Vec<T>
where
T: PartialEq,
{
fn contains(&self, item: &T) -> bool {
self.iter().any(|x| x == item)
}
}
impl<T> Container<T> for HashSet<T>
where
T: Hash + Eq,
{
fn contains(&self, item: &T) -> bool {
self.contains(item)
}
}
fn main() {
// 創建一個 Vec<T> 和 HashSet<T> 的實例
let vec_container: Box<dyn Container<i32>> = Box::new(vec![1, 2, 3]);
let hashset_container: Box<dyn Container<i32>> = Box::new(vec![4, 5, 6].into_iter().collect::<HashSet<_>>());
// 調用 contains 方法
println!("Vec contains 2: {}", vec_container.contains(&2));
println!("HashSet contains 6: {}", hashset_container.contains(&6));
}
例子十
use std::fmt::Debug;
// 假設我們有一個 Trait Foo,它有一個泛型方法 bar,它接受一個泛型參數 T:
// trait Foo {
// fn bar<T>(&self, x: T);
//}
// 這個 Trait 是不是 object-safe 的呢?答案是:取決於 T 的類型。 註意:它不是對象安全的
// 如果 T 是一個具體類型,比如 i32或 String,那麼它就不是 object-safe 的,
// 因為它需要在運行時知道 T 的具體類型才能調用 bar 方法。
// 但如果 T 也是一個 trait object,比如 &dyn Debug 或 &dyn Display,
// 那麼這個 Trait 就是 object-safe 的,因為它可以用動態分發的方式來調用 T 的方法。
// 所以我們可以這樣寫:
trait Foo {
fn bar(&self, x: &dyn Debug);
}
// 定義一個結構體 A,它實現了 Foo 特征
struct A {
name: String,
}
impl Foo for A {
fn bar(&self, x: &dyn Debug) {
println!("A {} says {:?}", self.name, x);
}
}
// 定義一個結構體 B,它也實現了 Foo 特征
struct B {
id: i32,
}
impl Foo for B {
fn bar(&self, x: &dyn Debug) {
println!("B {} says {:?}", self.id, x);
}
}
// 這樣我們就可以用 Foo 特征來創建 trait object 了,比如:
fn main() {
// 創建兩個不同類型的值,它們都實現了 Foo 特征
let a = A {
name: "Alice".to_string(),
};
let b = B { id: 42};
// 創建一個 Vec,它存儲了 Foo 的 trait object
let foos: Vec<&dyn Foo> = vec![&a, &b];
// 遍歷 Vec,並用 trait object 調用 bar 方法
for foo in foos {
foo.bar(&"Hello"); // "Hello" 實現了 Debug 特征
}
}
- 為實現對象安全,需要做出多大犧牲?
- 考慮你的 Trait 會被怎樣使用,用戶是否想把它當做 Trait 對象
- 用戶想使用你的 Trait 的多種不同實例 -> 努力實現對象安全
- 考慮你的 Trait 會被怎樣使用,用戶是否想把它當做 Trait 對象
借用 VS 擁有(Borrowed vs Owned)
- 針對 Rust 中幾乎每個函數、Trait 和類型,須決定:
- 是否應該擁有數據
- 僅持有對數據的引用
- 如果代碼需要數據的所有權:
- 它必須存儲擁有的數據
- 當你的代碼必須擁有數據時:
- 必須讓調用者提供擁有的數據,而不是引用或克隆
- 這樣可讓調用者控制分配,並且可清楚地看到使用相關介面的成本
- 如果代碼不需擁有數據:
- 應操作於引用
- 例外:
- 像 i32、bool、f64 等 “小類型”
- 直接存儲和複製的成本與通過引用存儲的成本相同
- 並不是所有 Copy 類型都適用:
- 例:[u8; 8192] 是 Copy 類型,但在多個地方存儲和複製它會很昂貴
- 像 i32、bool、f64 等 “小類型”
- 無法確定代碼是否需要擁有數據,因為它取決於運行時情況
- Cow 類型:
- 允許在需要時持有引用或擁有值
- 如果只有引用的情況下要求生成擁有的值:
- Cow 將使用 ToOwned trait 在後臺創建一個,通常是通過克隆
- 通常在返回類型中使用 Cow 來表示有時會分配記憶體的函數
例子十一
use std::borrow::Cow;
// 假設我們有一個函數 process_data,它接收一個字元串參數,
// 並根據一些條件對其進行處理。有時,我們需要修改輸入字元串,
// 並擁有對修改後的字元串的所有權。
// 然而,大多數情況下,我們只是對輸入字元串進行讀取操作,而不需要修改它。
fn process_data(data: Cow<str>) {
if data.contains("invalid") {
// 如果輸入字元串包含 “invalid”,我們需要修改它
let owned_data: String = data.into_owned();
// 進行一些修改操作
println!("Processed data: {}", owned_data);
} else {
// 如果輸入字元串不包含 “invalid”,我們只需要讀取它
println!("Data: {}", data);
}
}
// 在這個例子中,我們使用了 Cow<str> 類型作為參數類型。
// 當調用函數時,我們可以傳遞一個普通的字元串引用(&str)
// 或一個擁有所有權的字元串(String)作為參數。
fn main() {
let input1 = "This is valid data.";
process_data(Cow::Borrowed(input1));
let input2 = "This is invalid data.";
process_data(Cow::Owned(input2.to_owned()));
}
- 有時,引用生命周期會讓介面複雜,難以使用
- 如果用戶使用介面時遇到編譯問題,這表明您可能需要(即使不必要)擁有某些數據的所有權
- 這樣做的話,建議首先考慮容易克隆或不涉及性能敏感性的數據,而不是直接對大塊數據的內容進行堆分配
- 這樣做可以避免性能問題並提高介面的可用性
- 如果用戶使用介面時遇到編譯問題,這表明您可能需要(即使不必要)擁有某些數據的所有權
可失敗和阻塞的析構函數(Fallible and Blocking Destructors)
- 析構函數(Destructor):在值被銷毀時執行特定的清理操作
- 析構函數由 Drop trait 實現:它定義了一個 drop 方法
- 析構函數通常是不允許失敗的,並且是非阻塞執行的。但有時:
- 例如釋放資源時,可能需要關閉網路連接或寫入日誌文件,這些操作都有可能發生錯誤
- 可能需要執行阻塞操作,例如等待一個線程的結束或等待一個非同步任務的完成
- 針對 I/O 操作的類型,在丟棄時需要執行清理
- 例:將寫入的數據刷新到磁碟、關閉打開的文件、斷開網路連接
- 這些清理操作應在類型的 Drop 實現中完成
- 問題:一旦值被丟棄,就無法向用戶傳遞錯誤信息,除非通過 panic
- 非同步代碼也有類似問題:希望在清理過程中完成這些工作,但有其他工作處於 pending 狀態
- 可嘗試啟動另一個執行器,但這會引入其他問題,例如在非同步代碼中阻塞
- 沒有完美解決方案:需要通過 Drop 儘力清理
- 如果清理出錯了,至少我們嘗試了 —— 忽略錯誤並繼續
- 如果還有可用的執行器,可嘗試生成一個 future 來做清理,但如果 future 永不會運行,我們也儘力了
- 若用戶不想留下“鬆散” 線程:提供顯式的析構函數
- 這通常是一個方法,它獲得 self 的所有權並暴露任何錯誤(使用 -> Result<_, _>)或非同步性(使用 async fn),這些都是與銷毀相關的
例子十二
use std::os::fd::AsRawFd;
// 一個表示文件句柄的類型
struct File {
// 文件名
name: String,
// 文件描述符
fd: i32,
}
// File 類型的方法實現
impl File {
// 一個構造函數,打開一個文件並返回一個 File 實例
fn open(name: &str) -> Result<File, std::io::Error> {
// 使用 std::fs::OpenOptions 打開文件,具有讀寫許可權
let file = std::fs::OpenOptions::new().read(true).write(true).open(name)?;
// 使用 std::os::unix::io::AsRawFd 獲取文件描述符
let fd = file.as_raw_fd();
// 返回一個 File 實例,包含 name 和 fd 欄位
Ok(File {
name: name.to_string(),
fd,
})
}
// 一個顯式的析構器,關閉文件並返回任何錯誤
fn close(self) -> Result<(), std::io::Error> {
// 使用 std::os::unix::io::FromRawFd 將 fd 轉換回 std::fs::File
let file: std::fs::File = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(self.id) };
// 使用 std::fs::File::sync_all 將任何掛起的寫入刷新到磁碟
file.sync_all()?;
// 使用 std::fs::File::set_len 將文件截斷為零位元組
file.set_len(0)?;
// 再次使用 std::fs::File::sync_all 刷新截斷
file.sync_all()?;
// 丟棄 file 實例,它會自動關閉
drop(file);
// 返回 Ok(())
Ok(())
}
}
// 一個測試 File 類型的主函數
fn main() {
// 創建一個名為 "test.txt" 的文件,包含一些內容
std::fs::write("test.txt", "Hello, world!").unwrap();
// 打開文件並獲取一個 File 實例
let file = File::open("test.txt").unwrap();
// 列印文件名和 fd
println!("File name: {}, fd: {}", file.name, file.fd);
// 關閉文件並處理任何錯誤
match file.close() {
Ok(()) => println!("File closed successfully"),
Err(e) => println!("Error closing file: {}", e),
}
// 檢查關閉後的文件大小
let metadata = std::fs::metadata("test.txt").unwrap();
println!("File size: {} bytes", metadata.len());
}
註意:顯式的析構函數需要在文檔中突出顯示
- 添加顯式析構函數時會遇問題:
- 當類型實現了 Drop,在析構函數中無法將該類型的任何欄位移出
- 因為在顯式析構函數運行後,Drop::drop 仍會被調用,它接收 &mut self,要求 self 的所有部分都沒有被移動
- Drop 接受的是 &mut self,而不是 self,因此 Drop 無法實現簡單地調用顯式析構函數並忽略其結果(因為 Drop 不擁有 self)
- 當類型實現了 Drop,在析構函數中無法將該類型的任何欄位移出
例子十三
use std::os::fd::AsRawFd;
// 一個表示文件句柄的類型
struct File {
// 文件名
name: String,
// 文件描述符
fd: i32,
}
// File 類型的方法實現
impl File {
// 一個構造函數,打開一個文件並返回一個 File 實例
fn open(name: &str) -> Result<File, std::io::Error> {
// 使用 std::fs::OpenOptions 打開文件,具有讀寫許可權
let file = std::fs::OpenOptions::new().read(true).write(true).open(name)?;
// 使用 std::os::unix::io::AsRawFd 獲取文件描述符
let fd = file.as_raw_fd();
// 返回一個 File 實例,包含 name 和 fd 欄位
Ok(File {
name: name.to_string(),
fd,
})
}
// 一個顯式的析構器,關閉文件並返回任何錯誤
fn close(self) -> Result<(), std::io::Error> {
// 移出 name 欄位並列印它
let name = self.name; // 報錯 不能從 `self.name` 中移出值,因為它位於 `&mut` 引用後面
println!("Closing file {}", name);
// 使用 std::os::unix::io::FromRawFd 將 fd 轉換回 std::fs::File
let file: std::fs::File = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(self.id) };
// 使用 std::fs::File::sync_all 將任何掛起的寫入刷新到磁碟
file.sync_all()?;
// 使用 std::fs::File::set_len 將文件截斷為零位元組
file.set_len(0)?;
// 再次使用 std::fs::File::sync_all 刷新截斷
file.sync_all()?;
// 丟棄 file 實例,它會自動關閉
drop(file);
// 返回 Ok(())
Ok(())
}
}
// Drop trait 的實現,用於在值離開作用域時運行一些代碼
impl Drop for File {
// drop 方法,接受一個可變引用到 self 作為參數
fn drop(&mut self) {
// 調用 close 方法並忽略它的結果
let _ = self.close(); // 報錯 不能從 `*self` 中移出值,因為它位於 `&mut` 引用後面
// 列印一條消息,表明文件被丟棄了
println!("Dropping file {}", self.name);
}
}
// 一個測試 File 類型的主函數
fn main() {
// 創建一個名為 "test.txt" 的文件,包含一些內容
std::fs::write("test.txt", "Hello, world!").unwrap();
// 打開文件並獲取一個 File 實例
let file = File::open("test.txt").unwrap();
// 列印文件名和 fd
println!("File name: {}, fd: {}", file.name, file.fd);
// 關閉文件並處理任何錯誤
match file.close() {
Ok(()) => println!("File closed successfully"),
Err(e) => println!("Error closing file: {}", e),
}
// 檢查關閉後的文件大小
let metadata = std::fs::metadata("test.txt").unwrap();
println!("File size: {} bytes", metadata.len());
}
- 解決辦法(沒有完美的),方法之一 :
- 將頂層類型作為包裝了 Option 的新類型,Option 持有一個內部類型,該類型包含所有的欄位
- 在兩個析構函數中使用 Option::take;當內部類型還沒有被取走時,調用內部類型的顯式析構函數
- 由於內部類型沒有實現 Drop,你可以獲取所有欄位的所有權
- 缺點:想在頂層類型上提供所有的方法,都必須包含通過 Option 來獲取內部類型上欄位的代碼
例子十四
use std::os::fd::AsRawFd;
// 一個表示文件句柄的類型
struct File {
// 一個包裝在 Option 中的內部類型
inner: Option<InnerFile>,
}
// 一個內部類型,持有文件名和文件描述符
struct InnerFile {
// 文件名
name: String,
// 文件描述符
fd: i32,
}
// File 類型的方法實現
impl File {
// 一個構造函數,打開一個文件並返回一個 File 實例
fn open(name: &str) -> Result<File, std::io::Error> {
// 使用 std::fs::OpenOptions 打開文件,具有讀寫許可權
let file = std::fs::OpenOptions::new().read(true).write(true).open(name)?;
// 使用 std::os::unix::io::AsRawFd 獲取文件描述符
let fd = file.as_raw_fd();
// 返回一個 File 實例,包含一個 Some(InnerFile) 的 inner 欄位
Ok(File {
inner: Some(InnerFile {
name: name.to_string(),
fd,
}),
})
}
// 一個顯式的析構器,關閉文件並返回任何錯誤
fn close(mut self) -> Result<(), std::io::Error> {
// 使用 Option::take 取出 inner 欄位的值,並檢查是否是 Some(InnerFile)
if let Some(inner) = self.inner.take() {
// 移出 name 和 fd 欄位並列印它們
let name = inner.name;
let fd = inner.fd;
println!("Closing file {} with fd {}", name, fd);
// 使用 std::os::unix::io::FromRawFd 將 fd 轉換回 std::fs::File
let file: std::fs::File = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(self.id) };
// 使用 std::fs::File::sync_all 將任何掛起的寫入刷新到磁碟
file.sync_all()?;
// 使用 std::fs::File::set_len 將文件截斷為零位元組
file.set_len(0)?;
// 再次使用 std::fs::File::sync_all 刷新截斷
file.sync_all()?;
// 丟棄 file 實例,它會自動關閉
drop(file);
// 返回 Ok(())
Ok(())
} else {
// 如果 inner 欄位是 None,說明文件已經被關閉或丟棄,返回一個錯誤
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"File already closed or dropped",
))
}
}
}
// Drop trait 的實現,用於在值離開作用域時運行一些代碼
impl Drop for File {
// drop 方法,接受一個可變引用到 self 作為參數
fn drop(&mut self) {
// 使用 Option::take 取出 inner 欄位的值,並檢查是否是 Some(InnerFile)
if let Some(inner) = self.inner.take() {
// 移出 name 和 fd 欄位並列印它們
let name = inner.name;
let fd = inner.id;
println!("Dropping file {} with fd {}", name, fd);
// 使用 std::os::unix::io::FromRawFd 將 fd 轉換回 std::fs::File
let file: std::fs::File = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(fd) };
// 丟棄 file 實例,它會自動關閉
drop(file);
} else {
// 如果 inner 欄位是 None,說明文件已經被關閉或丟棄,不做任何操作
}
}
}
// 一個測試 File 類型的主函數
fn main() {
// 創建一個名為 "test.txt" 的文件,包含一些內容
std::fs::write("test.txt", "Hello, world!").unwrap();
// 打開文件並獲取一個 File 實例
let file = File::open("test.txt").unwrap();
// 列印文件名和 fd
println!(
"File name: {}, fd: {}",
file.inner.as_ref().unwrap().name,
file.inner.as_ref().unwrap().fd
);
// 關閉文件並處理任何錯誤
match file.close() {
Ok(()) => println!("File closed successfully"),
Err(e) => println!("Error closing file: {}", e),
}
// 檢查關閉後的文件大小
let metadata = std::fs::metadata("test.txt").unwrap();
println!("File size: {} bytes", metadata.len());
}
- 方法二:
- 所有欄位都可以 take
- 如果類型具有合理的 ”空“ 值,那麼效果很好
- 如果您必須將幾乎每個欄位都包裝在 Option 中,然後對這些欄位的每次訪問都進行匹配的 unwrap,很繁瑣
例子十五
use std::os::fd::AsRawFd;
// 一個表示文件句柄的類型
struct File {
// 文件名,包裝在一個 Option 中
name: Option<String>,
// 文件描述符,包裝在一個 Option 中
fd: Option<i32>,
}
// File 類型的方法實現
impl File {
// 一個構造函數,打開一個文件並返回一個 File 實例
fn open(name: &str) -> Result<File, std::io::Error> {
// 使用 std::fs::OpenOptions 打開文件,具有讀寫許可權
let file = std::fs::OpenOptions::new().read(true).write(true).open(name)?;
// 使用 std::os::unix::io::AsRawFd 獲取文件描述符
let fd = file.as_raw_fd();
// 返回一個 File 實例,包含一個 Some(name) 和一個 Some(fd) 的欄位
Ok(File {
name: Some(name.to_string()),
fd: Some(fd),
})
}
// 一個顯式的析構器,關閉文件並返回任何錯誤
fn close(mut self) -> Result<(), std::io::Error> {
// 使用 std::mem::take 取出 name 欄位的值,並檢查是否是 Some(name)
if let Some(name) = std::mem::take(&mut self.name) {
// 使用 std::mem::take 取出 fd 欄位的值,並檢查是否是 Some(fd)
if let Some(fd) = std::mem::take(&mut self.fd) {
// 列印文件名和文件描述符
println!("Closing file {} with fd {}", name, fd);
// 使用 std::os::unix::io::FromRawFd 將 fd 轉換回 std::fs::File
let file: std::fs::File = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(fd) };
// 使用 std::fs::File::sync_all 將任何掛起的寫入刷新到磁碟
file.sync_all()?;
// 使用 std::fs::File::set_len 將文件截斷為零位元組
file.set_len(0)?;
// 再次使用 std::fs::File::sync_all 刷新截斷
file.sync_all()?;
// 丟棄 file 實例,它會自動關閉
drop(file);
// 返回 Ok(())
Ok(())
} else {
// 如果 fd 欄位是 None,說明文件已經被關閉或丟棄,返回一個錯誤
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"File descriptor already taken or dropped",
))
}
} else {
// 如果 name 欄位是 None,說明文件已經被關閉或丟棄,返回一個錯誤
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"File name already taken or dropped",
))
}
}
}
// Drop trait 的實現,用於在值離開作用域時運行一些代碼
impl Drop for File {
// drop 方法,接受一個可變引用到 self 作為參數
fn drop(&mut self) {
// 使用 std::mem::take 取出 name 欄位的值,並檢查是否是 Some(name)
if let Some(name) = std::mem::take(&mut self.name) {
// 使用 std::mem::take 取出 fd 欄位的值,並檢查是否是 Some(fd)
if let Some(fd) = std::mem::take(&mut self.fd) {
// 列印文件名和文件描述符
println!("Dropping file {} with fd {}", name, fd);
// 使用 std::os::unix::io::FromRawFd 將 fd 轉換回 std::fs::File
let file: std::fs::File = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(fd) };
// 丟棄 file 實例,它會自動關閉
drop(file);
} else {
// 如果 fd 欄位是 None,說明文件已經被關閉或丟棄,不做任何操作
}
} else {
// 如果 name 欄位是 None,說明文件已經被關閉或丟棄,不做任何操作
}
}
}
// 一個測試 File 類型的主函數
fn main() {
// 創建一個名為 "test.txt" 的文件,包含一些內容
std::fs::write("test.txt", "Hello, world!").unwrap();
// 打開文件並獲取一個 File 實例
let file = File::open("test.txt").unwrap();
// 列印文件名和 fd
println!(
"File name: {}, fd: {}",
file.inner.as_ref().unwrap().name,
file.inner.as_ref().unwrap().fd
);
// 關閉文件並處理任何錯誤
match file.close() {
Ok(()) => println!("File closed successfully"),
Err(e) => println!("Error closing file: {}", e),
}
// 檢查關閉後的文件大小
let metadata = std::fs::metadata("test.txt").unwrap();
println!("File size: {} bytes", metadata.len());
}
- 方法三:
- 將數據持有在 ManuallyDrop 類型內,它會解引用內部類型,不必再 unwrap
- 在 drop 中銷毀時,可用 ManuallyDrop::take 來獲取所有權
- 缺點:ManuallyDrop::take 是 unsafe 的
例子十六
// 引入 std 庫中的一些模塊
use std::{mem::ManuallyDrop, os::fd::AsRawFd};
// 定義一個表示文件句柄的結構體
struct File {
// 文件名,包裝在一個 ManuallyDrop 中
name: ManuallyDrop<String>,
// 文件描述符,包裝在一個 ManuallyDrop 中
fd: ManuallyDrop<i32>,
}
// 為 File 結構體實現一些方法
impl File {
// 一個構造函數,打開一個文件並返回一個 File 實例
fn open(name: &str) -> Result<File, std::io::Error> {
// 使用 std::fs::OpenOptions 打開文件,具有讀寫許可權
let file = std::fs::OpenOptions::new().read(true).write(true).open(name)?;
// 使用 std::os::unix::io::AsRawFd 獲取文件描述符
let fd = file.as_raw_fd();
// 返回一個 File 實例,包含一個 ManuallyDrop(name) 和一個 ManuallyDrop(fd) 的欄位
Ok(File {
name: ManuallyDrop::new(name.to_string()),
fd: ManuallyDrop::new(fd),
})
}
// 一個顯式的析構器,關閉文件並返回任何錯誤
fn close(mut self) -> Result<(), std::io::Error> {
// 使用 std::mem::replace 將 name 欄位替換為一個空字元串,並獲取原來的值
if let name = std::mem::replace(&mut self.name, ManuallyDrop::new(String::new()));
// 使用 std::mem::replace 將 fd 欄位替換為一個無效的值,並獲取原來的值
if let fd = std::mem::replace(&mut self.fd, ManuallyDrop::new(-1));
// 列印文件名和文件描述符
println!("Closing file {:?} with fd {:?}", name, fd);
// 使用 std::os::unix::io::FromRawFd 將 fd 轉換回 std::fs::File
let file: std::fs::File = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(*fd) };
// 使用 std::fs::File::sync_all 將任何掛起的寫入刷新到磁碟
file.sync_all()?;
// 使用 std::fs::File::set_len 將文件截斷為零位元組
file.set_len(0)?;
// 再次使用 std::fs::File::sync_all 刷新截斷
file.sync_all()?;
// 丟棄 file 實例,它會自動關閉
drop(file);
// 返回 Ok(())
Ok(())
}
}
// 為 File 結構體實現 Drop trait,用於在值離開作用域時運行一些代碼
impl Drop for File {
// drop 方法,接受一個可變引用到 self 作為參數
fn drop(&mut self) {
// 使用 ManuallyDrop::take 取出 name 欄位的值,並檢查是否是空字元串
let name = unsafe { ManuallyDrop::take(&mut self.name) };
// 使用 ManuallyDrop::take 取出 fd 欄位的值,並檢查是否是無效的值
let fd = unsafe { ManuallyDrop::take(&mut self.id) };
// 列印文件名和文件描述符
println!("Dropping file {:?} with fd {:?}", name, fd);
// 如果 fd 欄位不是無效的值,說明文件還沒有被關閉或丟棄,需要執行一些操作
if fd != -1 {
// 使用 std::os::unix::io::FromRawFd 將 fd 轉換回 std::fs::File
let file: std::fs::File = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(fd) };
// 丟棄 file 實例,它會自動關閉
drop(file);
}
}
}
// 一個測試 File 類型的主函數
fn main() {
// 創建一個名為 "test.txt" 的文件,包含一些內容
std::fs::write("test.txt", "Hello, world!").unwrap();
// 打開文件並獲取一個 File 實例
let file = File::open("test.txt").unwrap();
// 列印文件名和 fd
println!(
"File name: {}, fd: {}",
*file.name,
*file.fd
);
// 關閉文件並處理任何錯誤
match file.close() {
Ok(()) => println!("File closed successfully"),
Err(e) => println!("Error closing file: {}", e),
}
// 檢查關閉後的文件大小
let metadata = std::fs::metadata("test.txt").unwrap();
println!("File size: {} bytes", metadata.len());
}
- 根據實際情況選擇方案
- 傾向於選擇第二個方案
- 只有發現自己處於一堆 Option 中時才切換到其他選項
- 如果代碼足夠簡單,可輕鬆檢查代碼安全性,那麼 ManuallyDrop 方案也挺好
- 傾向於選擇第二個方案
本文來自博客園,作者:尋月隱君,轉載請註明原文鏈接:https://www.cnblogs.com/QiaoPengjun/p/17470483.html