星期日, 11月 26, 2023

Rust - String

String

Rust 中 String 的標準類型是 String

編碼是 UTF-8,這個特性給其他語言轉換過來的使用者比較疑惑的性質。

  • 無法使用 [] 取得字元。
  • .len() 取得的是 Bytes 長度,而非字數。
無法使用 [] 取得字元。
let mut st = String::from("您好,Rust");
println!("{}", st[0]);
會得到編譯錯誤
`String` cannot be indexed by `{integer}`
Rust 不支援 [] 的原因
  • Rust 中 String 是以 byte 為單位儲存
  • UTF-8 每個字不定長度
  • 計算字必須由第一個 Bytes 開始解析,時間複雜度為 O(n),而非 O(1)
那如何取得字數與特定位置的字呢?
let hello = "您好";
    
println("The second character is {}", hello.chars().nth(1)); // Get 好
println("The length of string is {}", hello.chars().count()); // Get 2
.chars() 可以取得對應字串的 iterator 來進行 parse 的動作。 但 Rust 不支援 grapheme
let hello = "Z̫̫̳̰̦͙͙̒̇̽̒͗̑̂̋̚ạ͈̳̟̙͉̥̦̙̒̔̃͋̅͛̐l͕̯̟͖̣͊̓̒̂̑̽̊̃͑̃̓̽ḡ̟̲͚̞͆͊̋̏̽̏̉ͅô̤͈̩̖̞̙̲͚͍͓̘͉͂́̆";
    
println("The second character is {}", hello.chars().nth(0)); // Get Z
println("The length of string is {}", hello.chars().count()); // Get 82
Rust 中的 char 為了能夠表示 Unicode 字元。其大小為 4 bytes
let c = '您'; // 
println!("{}", get_type_size(&c)); // get 4
每次 .chars 都會重新 Parse,想要保留結果可以將 String 轉為 Vec
let re = "您好, Rust。這是一個嘗試";
let re_v: Vec<char> = re.chars().collect();
print!("{}{}\n", re_v[0], re_v[1]); // Get 您好

String 與 str

在 Rust 中 Function 常常可以看到以下寫法
fn first_word(s: &str) -> &str {
}
&str
  • 有 & 所以是一個 Reference
  • str 是 String Slice, 相比一般 Reference 多了 "長度" 的資料
以下得範例
let p_i32: i32 = 0;
let p_String: String = format!("Hello world");

let r_i32 = &p_i32;
let r_String = &p_String;
let r_slice = &p_String[..]; // Full Size String Slice
型別 大小
&p_i32 &i32 8
&p_String &String 8
&p_String[..] &str 16
&str 相比 &String 多出的 8 bytes 就是紀錄長度需要的大小。

星期六, 11月 18, 2023

Rust 的基礎 - Option

在日常的程式開發中,有一種意外中止的情況:對 Null Pointer 進行了 Dereference。在 Java 中會丟出 Null Pointer Exception,在 C/C++ 則是觸發 Segmentation Fault。

那麼在 Rust 當中呢?

我們來看以下的範例

fn get_bigger<'a>(s1: &'a String, s2: &'a String) -> Option<&'a String> {
   if s1.len() > s2.len() {
      return Some(s1);
   } else if s1.len() < s2.len() {
      return Some(s2);
   } else {
      return None;
   }  
}
在這個範例中
  1. 定義了一個 get_bigger function
  2. 接受兩個 Reference
  3. 回傳 Option<&String>
    • 回傳較長字串的 Reference
    • 若一樣長,回傳 None

如果想要取得較長字串的長度

fn main() {
   let buf1 = "Hello".to_string();
   let buf2 = "World".to_string();

   let bigger = get_bigger(&buf1, &buf2);

   println!("{}", bigger.len());
}
會得到編譯錯誤
println!("{}", bigger.len());
                      ^^ method not found in `Option<&String>`
如果想要取得回傳的長度,必須對 Option 作拆封的動作
   if let Some(value) = bigger {
      println!("The bigger {}", value);
   } else {
      println!("Same");
   }

在使用 Option 的時候由於強制拆封,以此避免了對 None 進行操作的可能性。在以後的 Code 我們陸續可以看到這種強制拆封避免了開發者對不正常的值作操作的可能。

以下是完整的 Code

fn get_bigger<'a>(s1: &'a String, s2: &'a String) -> Option<&'a String> {
   if s1.len() > s2.len() {
      return Some(s1);
   } else if s1.len() < s2.len() {
      return Some(s2);
   } else {
      return None;
   }  
}

fn main() {
   let buf1 = "Hello".to_string();
   let buf2 = "World".to_string();

   let bigger = get_bigger(&buf1, &buf2);

   if let Some(value) = bigger {
      println!("The bigger {}", value);
   } else {
      println!("Same");
   }
}

星期六, 11月 11, 2023

Rust 的基礎 - Borrow

Rust 的基礎 - Reference 中,透過 reference 來存取到了 value。

Rust 的基礎- 所有權規則 中,有提到每個 Value 都有一個所有者 (owner)

在 Rust 中把創建一個 reference 的動作稱為借用 (borrowing)

  let hello = "hello".to_string();
  let hello_ref = &hello; // 借用 hello

為了確保 Reference 的安全,Rust 有一項編譯期檢查機制 "Borrow Checker",其用意是確保兩方面的事情

  • 所有者與 Reference 之間
  • Reference 與 Reference 之間
所有者與 Reference 之間

要確認的事項是 Rerference 不可以活得比 Owner 久,比如以下 code

   let hello = "hello".to_string();
   let hello_ref = &hello;

   drop(hello);
   println!("{}", hello_ref);

我們會得到以下錯誤

   let hello = "hello".to_string();
   let hello_ref = &hello;
                   ------ borrow of `hello` occurs here
   drop(hello);
        ^^^^^ move out of `hello` occurs here
   println!("{}", hello_ref);
                  -------- borrow later used here
Reference 與 Reference 之間

在說明 Reference 與 Reference 之間,首先先介紹 Mutable Borrowing,在介紹 Mutable Borrowing 前要先介紹什麼是 Mutable

Rust 相比其他常見的程式語言有一個特點,就是所有變數預設都是唯獨的

底下的這段 code
   let hello = "hello".to_string();
   hello = "world".to_string();
會得到編譯錯誤
   let hello = "hello".to_string();
   hello = "world".to_string();
   ^^^^^ cannot assign twice to immutable variable
想要編譯成功需要將程式改為
   let mut hello = "hello".to_string();
   hello = "world".to_string();

這麼作的思路是預設不能改可以避免潛在問題

而 Borrowing 所創建的 reference 預設是無法修改指向的 Value 的

   let mut hello = "hello".to_string();

   let hello_ref = &hello;

   hello_ref.push_str(" world");

會得到編譯錯誤

   let mut hello = "hello".to_string();

   let hello_ref = &hello;

   hello_ref.push_str(" world");
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `hello_ref` is a `&` reference, \
                                 so the data it refers to cannot be borrowed as mutable

想要修正這個錯誤,需要使用 mutable borrow

   let mut hello = "hello".to_string();

   let hello_ref = &mut hello;

   hello_ref.push_str(" world");

知道了什麼叫做 mutable borrowing 後,我們回到 Reference 與 Reference 之間 的情況,Rust 的規則是

  • borrowing 與 mutable borrowing 的 reference 不可重疊使用
  • 同時間只能有一個 mutable borrwing 的 reference

比如以下 code

   let mut hello = "hello".to_string();

   let hello_mut_ref = &mut hello;
   let hello_ref = &hello;

   hello_mut_ref.push_str(" world");
會得到編譯錯誤
   let mut hello = "hello".to_string();

   let hello_mut_ref = &mut hello;
                       ---------- mutable borrow occurs here
   let hello_ref = &hello;
                   ^^^^^^ immutable borrow occurs here

   hello_mut_ref.push_str(" world");
   -------------------------------- mutable borrow later used here

為什麼需要這種規則,可以看以下的 code

   let hello_mut_ref = &mut hello;
   let hello_ref = &hello;

   for c in hello_ref.chars() {
      hello_mut_ref.push_str(" world");
      // push_str may makes the iterator from hello_ref.chars() invalid.
      print!("{}", c);
   }

在這個範例中我們看到 hello_ref 去透過 chars function 取得了 hello 的 iterator,而在取用的途中,可以看到又透過 hello_mut_ref 在往 hello 內去附加字串。這種附加動作有可能會導致記憶體重分配,導致 hello 的 iterator 在途中變成無用的對象。

總結: Rust 的 Borrow Checker
  • 所有者與 Reference 之間
    • Reference 不可以活得比所有者久
  • Reference 與 Reference 之間
    • mutable borrowing 與 borrowing 不可同時存在
    • mutable borrowing 同時只能存在一個

以上規則可以有效防止 Dangling Pointer 的出現

星期六, 11月 04, 2023

Rust 的基礎 - Reference

在之前的所有權章節有提到以下的 code 行為是 move 而非複製

let hello = "Hello World".to_string(); let hello2 = hello; // hello move to hello2

在 Rust 中可以定義 function

fn add(v1: i32, v2: i32) -> i32{
  return v1 + v2;
}
在 move 的規則之下,在需要保留原本傳入參數的情況下。就得將原本的參數再回傳回來
fn concat(s1: String, s2: String) -> (String, String, String) {
   let result = format!("{}{}", s1, s2);

   return (s1, s2, result);
}

fn main() {
   let hello = "Hello".to_string();
   let world = "World".to_string();

   let (hello, world, hello_world) = concat(hello, world);

   println!("{} = {} + {}", hello_world, hello, world);
}

這種作法顯然會帶來兩個問題

  1. 撰寫麻煩
  2. 額外的 Stack 複製成本

為此 Rust 中引入了 reference 機制,以下為使用 Reference 的機制改寫的 code

fn concat(s1: &String, s2: &String) -> String {
   return format!("{}{}", s1, s2);
}

fn main() {
   let hello = "Hello".to_string();
   let world = "World".to_string();

   let hello_world = concat(&hello, &world);

   println!("{} = {} + {}", hello_world, hello, world);
}

在上述的範例中,主要做了兩項改變

  1. concat 參數的型別由String變為&String
  2. concat 傳入的變數由hello變為&hello

&String翻譯為中文的話是 "一個 Reference,指向的型別是 String"

&hello的話,& 則是一個運算子,對 hello 運算後會得到指向 hello 的 Reference

以下的 code 我們可以得到 String 與 &String 在記憶體中的大小

fn main() {
   println!("{}", std::mem::size_of::<String>());
   println!("{}", std::mem::size_of::<&String>());
}

在 x64 環境下,String 大小是 24 bytes,&String 大小則是 8 bytes,而 x86 環境下則分別是 12 bytes 與 4 bytes

可以發現 reference 的大小規則與所在平台的記憶體空間的所需定位大小一致,基本上就是 C 的指標

一旦有了指標出現,就會想到一些安全問題,比如 Dangling Pointer等,在之後的文章會介紹 Rust 如何防止 Rerference 的問題