之前看到一個有趣的圖,在 c 中 string 其實就是 char*,而在 rust 中 string 卻有很多種型別,對寫 Golang 的我來說光是為何要 to_string() 才能 append string 就滿頭問號。
slice, array, vector
講 string 前可以先理解 rust 中 slice, array, vector 的定義
array 為固定長度的序列
[T; N] T 是類型 N 是 長度
let s = [1; 5];
println!("{:?}", s);
// [1, 1, 1, 1, 1]
vector 是可以動態增長的序列
let mut s = vec![1];
s.push(2);
assert_eq!(s, &[1, 2]);
assert_eq!(s.len(), 2);
assert_ne!(s.capacity(), 2);
slice 則是對連續空間的 reference,可以是 vector / array 的一部分參考,可以不需要經過複製就可以對 slice 參照的 vector / array 進行操作
常見的 &str 和 String 的差別
&str
let s = "abc";
在編譯時因為 "abc" 長度是固定的所以被認為是靜態的放 stack 上,所以引用時去參照這段位置,所以 s 為 str 這個類別的 reference &str,而 &str 其實是一個 slice 參照 u8 的 array,
String
let s = "abc".to_string();
s 是 String 其實是一個 u8 的 vector,因為是可以動態增長,compiler 不會知道 s 會長多大,所以必須放在 heap 上,可以做下面的操作來改變 String 本身
let mut s = "abc".to_string();
s.push_str("def");
那為何 rust 不把我定義的 "abc" 值當作 [u8] 就好了呢? 因為 rust 還同時要確保 &str 中的 [u8] 是合法的 UTF-8 字元,就像大部份語言 char 與 string 的區別,這種 pointer 包含了 type 還有其他規則的 pointer 常被稱為 fat pointer。
可以看出來其實 &str 與 String 類型的差別跟 compiler 將 data 放在 heap 還是 stack 有很大的關係,當你對 &str 使用 to_string 就產生的 memory allocation,所以不需要就不要使用 to_string 增加開銷。
其他意外發現的坑
計算字數
let s = "🗻∈🌏";
let byte = s.as_bytes();
assert_eq!(s.len(), 11);
assert_eq!(s.chars().count(), 3);
assert_eq!(byte.len(), 11);
s.len() 竟然是 11 讓我有點傻眼XD...
Precomposed Characters
除此之外,如果是 precomposed characters 會有很神奇的現象 [1]
println!("{}", "é".chars().count()); // 2
println!("{}", "é".chars().count()); // 1
何會得到不一樣的結果?因為實際上這兩個是不一樣的字 程式碼
fn main() {
assert_eq!("é","é")
} thread 'main' panicked at 'assertion failed: `(left == right)`
left: `"e\u{301}"`,
right: `"é"`', src/main.rs:2:5
"""
這讓我理解到數字其實背後也有很多學問,chars count 是數 Unicode Scalar Value,而 "é" 是 e 還有 \u301 這個 Unicode 組成 ,所以直接算就會是兩個字,那解法就是用 graphemes 這個 method 對應 composited char 的問題
use unicode_segmentation::UnicodeSegmentation; // 1.6.0
fn main() {
let s = "é";
println!("{}", s.chars().count()); // 2
println!("{}", "é".graphemes(true).count()); // 1
}
註 [1]: 感謝 Rust-TW Telegram 上 Lo Weihang 勘誤
Updated at 2021/11/28
Top comments (0)