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 = https://www.cnblogs.com/QiaoPengjun/archive/2023/06/09/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 = https://www.cnblogs.com/QiaoPengjun/archive/2023/06/09/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 = https://www.cnblogs.com/QiaoPengjun/archive/2023/06/09/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 = https://www.cnblogs.com/QiaoPengjun/archive/2023/06/09/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 = https://www.cnblogs.com/QiaoPengjun/archive/2023/06/09/std::fs::metadata("test.txt").unwrap();
println!("File size: {} bytes", metadata.len());
}
- 根據實際情況選擇方案
- 傾向于選擇第二個方案
- 只有發現自己處于一堆 Option 中時才切換到其他選項
- 如果代碼足夠簡單,可輕松檢查代碼安全性,那么 ManuallyDrop 方案也挺好
- 傾向于選擇第二個方案
本文來自博客園,作者:尋月隱君,轉載請注明原文鏈接:https://www.cnblogs.com/QiaoPengjun/p/17470483.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/554762.html
標籤:其他
上一篇:C語言回圈列印空心正方形代碼實作
下一篇:返回列表
