主頁 > 後端開發 > Rust - 介面設計建議之靈活(flexible)

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

2023-06-10 07:56:43 後端開發

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 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)暗含了這一點)

例子六

// 假設我們有一個 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 = 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)

例子十三

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語言回圈列印空心正方形代碼實作

下一篇:返回列表

標籤雲
其他(160680) Python(38219) JavaScript(25489) Java(18216) C(15237) 區塊鏈(8270) C#(7972) AI(7469) 爪哇(7425) MySQL(7241) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5873) 数组(5741) R(5409) Linux(5347) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4589) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2435) ASP.NET(2404) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1984) 功能(1967) HtmlCss(1956) Web開發(1951) C++(1933) python-3.x(1918) 弹簧靴(1913) xml(1889) PostgreSQL(1880) .NETCore(1863) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust - 介面設計建議之靈活(flexible)

    # Rust - 介面設計建議之靈活(flexible) ## 靈活(flexible) ### 代碼的契約(Contract) - 你寫的代碼包含契約 - 契約: - 要求:代碼使用的限制 - 承諾:代碼使用的保證 - 設計介面時(經驗法則): - 避免施加不必要的限制,只做能夠兌現的承諾 - 增 ......

    uj5u.com 2023-06-10 07:56:43 more
  • C語言回圈列印空心正方形代碼實作

    ```c #include int main() { int w, i, j; printf("輸入正方形邊長\n"); scanf_s("%d", &w); if (w <= 0) { printf("正方形邊長要大于0\n"); return 0; } // 外層回圈控制行數 for (i = ......

    uj5u.com 2023-06-10 07:56:38 more
  • JavaWeb準備作業和部分環境配置

    # JavaWeb ## 基本概念 web開發: - web:網頁的意思 - 靜態web:提供的資料資源不會發生改變 - 動態web:資料是不斷變化的,現代網站幾乎都是動態網站技術堆疊: Servlet/JSP:基于java,主推B/S架構,可以 ? 承載高并發,語法類似ASP ? ASP:微軟,國內 ......

    uj5u.com 2023-06-10 07:56:00 more
  • 切片比陣列好用在哪

    # 1. 引言 在Go語言中,陣列和切片都是常見的資料結構,它們經常被用于存盤資料,可以相互替換。本文將介紹Go語言中陣列和切片的基本概念,同時詳細探討切片的優勢。從而能夠充分的理解切片相對于陣列的優點,更好得對切片進行使用。 # 2. 基本介紹 ### 2.1 陣列 陣列是一種固定長度、具有相同類 ......

    uj5u.com 2023-06-10 07:55:41 more
  • java集成chatGpt完整案例代碼(效果和官網一樣逐字輸出)

    背景 要集成chatGpt參考我上一篇文章即可。但是,如果要實作官網一樣的效果,逐字輸出,難度就提升了不少了。經過在官網的研究發現它應該是采用了SSE技術,這是一種最新的HTTP互動技術。SSE(Server-Sent Events):通俗解釋起來就是一種基于HTTP的,以流的形式由服務端持續向客戶 ......

    uj5u.com 2023-06-10 07:55:33 more
  • Java物件中轉換空值的欄位

    在后端回傳資料給前端時,公司的框架會把值為null的資料去掉,方便前端取值。 如:Person物件為:{name:"浩二", age:24, weight:null, height:114},那回傳給前端的就為{name:"浩二", age:24, height:114}。 如果這個時候有個需求: ......

    uj5u.com 2023-06-10 07:54:49 more
  • 構建高可用、高效、安全和可靠的分布式系統:Spring Cloud為開發人

    Spring Cloud是一個基于Spring框架的云應用開發工具箱,可以快速構建分布式應用程式。它為開發人員提供了許多有用的功能,例如服務發現、負載均衡、配置管理和應用監控等,這些功能可以使分布式系統更加穩定和可靠。 如果您還沒有了解過Spring Cloud,我強烈建議您觀看以下這個視頻鏈接: ......

    uj5u.com 2023-06-10 07:54:43 more
  • Java 集合框架體系簡介

    陣列可以通過索引快速訪問和操作元素,在許多場景下仍然是非常有用的,但如果需要動態調整大小或保存不同型別的元素,則可以考慮使用集合類來代替陣列。集合類還提供了一系列增加、洗掉、修改和查找元素的方法。集合框架中還提供了多種優化和封裝好的實作類,通過使用合適的集合類可以更高效地組織和操作資料。 ......

    uj5u.com 2023-06-10 07:54:37 more
  • BS和CS

    # BS和CS - **CS:**客戶端服務器架構模式 - **優點:**充分利用客戶端機器的資源,減輕服務器的負荷(一部分安全要求不高的計算任務存盤任務放在客戶端執行,從而能夠減輕服務器的壓力,也能夠減輕網路負荷); - **缺點:**需要安裝;升級維護成本較高; - **BS:**瀏覽器服務器架 ......

    uj5u.com 2023-06-10 07:54:30 more
  • 百度人臉識別_SpringBoot整合離線SDK

    ### 一、前言 1. 建議使用低版本 SDK : [Baidu_Face_Offline_SDK_Windows_Java_6.1.3](https://www.123pan.com/s/AvoiVv-d7aud.html) - 目前已知8.x版本對服務端不兼容,存在運行程序中,第一次呼叫sdk能 ......

    uj5u.com 2023-06-10 07:54:26 more