Rust - 介面設計建議之靈活(flexible)

来源:https://www.cnblogs.com/QiaoPengjun/archive/2023/06/09/17470483.html
-Advertisement-
Play Games

# 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 ),這意味著:

    1. 調用的 trait 方法是靜態生成的,因此是直接對 trait 實現的調用
    2. 編譯器能對這些調用做內聯 (inline) 和其他優化
  • 內聯式佈局:如果結構體和枚舉體類型具有某個泛型參數 TT 的值將在結構體和枚舉體里以內聯方式排列,不產生任何間接調用。

  • 可推斷:由於泛型函數的類型參數通常是推斷出來的, 泛型函數可以減少複雜的代碼,比如顯式轉換、通常必須的一些方法調用。

  • 精確的類型:因為泛型給實現了某個 trait 的具體類型一個名稱, 從而有可能清楚這個類型需要或創建的地方在哪。比如這個函數:

    fn binary<T: Trait>(x: T, y: T) -> T
    

    會保證消耗和創建具有相同類型 T 的值;不可能傳入實現了 Trait 的但不同名稱的兩個類型。

泛型的缺點

  • 增加代碼大小:單態化泛型函數意味著函數體會被覆制。 增加代碼大小和靜態分派的性能優勢之間必須做出衡量。
  • 類型同質化:這是 “精確的類型” 帶來的另一面: 如果 T 是類型參數,那麼它代表一個單獨的實際類型。 對於像 Vec<T> 這樣具體的單獨的元素類型也是一樣, 而且 Vec 實際上為了內聯這些元素,進行了專門的處理。 有時候,不同的類型會更有用,參考 trait objects
  • 簽名冗餘:過度使用泛型會造成閱讀和理解函數簽名更困難。

The Rust RFC Bookhttps://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)暗含了這一點)

例子六

// 假設我們有一個 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 的多種不同實例 -> 努力實現對象安全

借用 VS 擁有(Borrowed vs Owned)

  • 針對 Rust 中幾乎每個函數、Trait 和類型,須決定:
    • 是否應該擁有數據
    • 僅持有對數據的引用
  • 如果代碼需要數據的所有權:
    • 它必須存儲擁有的數據
  • 當你的代碼必須擁有數據時:
    • 必須讓調用者提供擁有的數據,而不是引用或克隆
  • 這樣可讓調用者控制分配,並且可清楚地看到使用相關介面的成本
  • 如果代碼不需擁有數據:
    • 應操作於引用
  • 例外:
    • 像 i32、bool、f64 等 “小類型”
      • 直接存儲和複製的成本與通過引用存儲的成本相同
      • 並不是所有 Copy 類型都適用:
        • 例:[u8; 8192] 是 Copy 類型,但在多個地方存儲和複製它會很昂貴
  • 無法確定代碼是否需要擁有數據,因為它取決於運行時情況
  • 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)

例子十三

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # JS語法學習 **Javascript:客戶端的腳本語言** ## **1. JavaScript數據類型** ![](https://img2023.cnblogs.com/blog/3008601/202306/3008601-20230607170622855-1334758269.png ...
  • >我們是[袋鼠雲數棧 UED 團隊](http://ued.dtstack.cn/),致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。。 >本文作者:霽明 # 一、背景 ## 1、業務背景 業務中會有一些需要實現拖拽的場景,尤其是偏視覺方向以及移動端 ...
  • 在前端開發中,最常見的字元編碼方案是 UTF-8。UTF-8是一種可變長度的 Unicode 編碼方案,可以表示幾乎所有的字元,並且與 ASCII 相容。由於互聯網的廣泛應用和多語言的支持,UTF-8成為了前端開發中的首選字元編碼方案。 使用UTF-8編碼的好處: 1. 多語言支持 :UTF-8可以 ...
  • 在現有的大環境下,越來越註重用戶體驗,因此對圖片庫的要求也日益攀升。從成本的角度來看,使用 AVIF 格式可以節省大量的網路帶寬和存儲空間,減少網站載入時間,可以改善用戶體驗,進而提高網站的效率和收益,節約大量的費用。 AVIF 的優點在於它可以提供更好的圖像質量和更小的文件大小。與 JPEG 相... ...
  • Map
    基於electron25+vite4+vue3仿製chatgpt客戶端聊天模板ElectronChatGPT。 electron-chatgpt 使用最新桌面端技術Electron25.x結合Vite4.x全家桶技術開發跨端模仿ChatGPT智能聊天程式模板。支持經典+分欄兩種佈局、暗黑+明亮主題模 ...
  • 在應用層下的文件操作只需要調用微軟應用層下的`API`函數及`C庫`標準函數即可,而如果在內核中讀寫文件則應用層的API顯然是無法被使用的,內核層需要使用內核專有API,某些應用層下的API只需要增加Zw開頭即可在內核中使用,例如本章要講解的文件與目錄操作相關函數,多數ARK反內核工具都具有對文件的... ...
  • # ImageIO的應用 # 一、關於IO流 在講imageio之前,我們先來複習一下IO流的使用。 這裡我建立一個Java類,用來實現讀取文檔中的內容,並且能夠識別換行,話不多說,上代碼: ```java package com.Evan.demo; import java.io.Buffered ...
  • 某日二師兄參加XXX科技公司的C++工程師開發崗位第10面: > 面試官:瞭解`sizeof`操作符嗎? > > 二師兄:略微瞭解(不就是求大小的嘛。。) > > 面試官:請講以下如何使用`sizeof`? > > 二師兄:`sizeof`主要是求變數或者類型的大小。直接使用`sizeof(type ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...