字串

字串是每個軟體工程師都應該精通的重要概念。 Rust 因為著重在系統程式,所以它的字串處理系統與其他的程式語言有些不同。 每當你有大小不固定的資料結構時,事情就會變得很複雜,不巧的是字串最好是一個可以改變大小的資料結構。 Rust 的字串與其他系統程式語言,例如 C,也有所不同。

讓我們深入的研究字串。 一個字串是一個編碼成 UTF-8 串流的萬國碼(Unicode)數值序列。 所有的字串都保證是用合法的 UTF-8 編碼。 此外不同於其他的系統程式語言,字串並非由空字串結尾(null-terminated)而且可以含有空字串位元。

Rust 有兩種主要的字串類型: &strString。 讓我們先講講 &str。 它們叫做字串切片(string slices)。 一個字串切片有固定的大小,而且是不可變的。 他是一個指向 UTF-8 位元序列的參照。

let greeting = "Hello there."; // greeting: &'static str

"Hello there."是一個字串常值(string literial),其型態是&'static str。 一個字串常值是一個靜態分配的字串切片,也就是說它儲存在我們編譯過的程式內,而且在整個運行的過程中都存在。 greeting 綁定是一個知道這個靜態字串的參照。 任何一個接受字串切片的函數也同樣會接受字串常值。

字串常值可以橫跨多行。 有兩種方法來表示多行的字串。 第一種會包含換行符號還有每行開頭的空白:

let s = "foo
    bar";

assert_eq!("foo\n        bar", s);

第二種使用 \ 符號,會把空白與換行符號移除:

let s = "foo\
    bar";

assert_eq!("foobar", s);

但是 Rust 不只有 &str。 一個 String是分配在堆積上的一種字串。 這種字串可以長大,而且也保證是 UTF-8 編碼。 String 的產生方式常常是呼叫字串切片的 to_string 函式轉換而來。

let mut s = "Hello".to_string(); // mut s: String
println!("{}", s);

s.push_str(", world.");
println!("{}", s);

使用 & 符號會把 String 強制轉換成 &str

fn takes_slice(slice: &str) {
    println!("Got: {}", slice);
}

fn main() {
    let s = "Hello".to_string();
    takes_slice(&s);
}

當一個函數接受 &str 的一種特徵(trait)作為參數而不是單純的 &str 時,這種自動轉換並不會發生。 例如, TcpStream::connect 有一個型態為 ToSocketAddrs 的參數。 這時傳入 &str 是可以的,但是 String 就必須要明確的使用 &* 轉換後才能傳入。

use std::net::TcpStream;

TcpStream::connect("192.168.0.1:3000"); // &str parameter

let addr_string = "192.168.0.1:3000".to_string();
TcpStream::connect(&*addr_string); // convert addr_string to &str

把一個 String 轉換成 &str 來讀取不需要什麼成本的,但是把 &str 轉換為一個 String 就必須牽涉到分配記憶體。 若是沒有強烈的理由千萬不要這樣做!

索引

因為字串是 UTF-8 串流,所以並不支援使用索引來存取:

let s = "hello";

println!("The first letter of s is {}", s[0]); // ERROR!!!

通常使用 [] 來存取向量是非常快的。 但是因為字串是使用 UTF-8 編碼,每個字符可能有多個位元組,要找到第 N 個字符必須要從頭開始循序尋找。 這是一個相對非常耗費資源的程序,所以我們不想誤導你。 另外,字符(letter)並不是萬國碼的一個標準定義。 我們可以選擇把字串看做單獨的位元組,或單獨的編碼位置(codepoints):

let hachiko = "忠犬ハチ公";

for b in hachiko.as_bytes() {
    print!("{}, ", b);
}

println!("");

for c in hachiko.chars() {
    print!("{}, ", c);
}

println!("");

就會印出:

229, 191, 160, 231, 138, 172, 227, 131, 143, 227, 131, 129, 229, 133, 172,
忠, 犬, ハ, チ, 公,

你可以看出,它所含的位元組遠多於 char 的數量。

你可以使用以下的方法達到類似於索引的功能:

# let hachiko = "忠犬ハチ公";
let dog = hachiko.chars().nth(1); // kinda like hachiko[1]

這種方法強調我們必須從頭開始循序的尋找一連串的 chars

切片(Slicing)

你可以用切片語法得到一個字串的切片:

let dog = "hachiko";
let hachi = &dog[0..5];

但是請注意這是 位元 的位置而非 字母 的位置。所以以下的程式會在執行時失敗:

let dog = "忠犬ハチ公";
let hachi = &dog[0..2];

你會看到以下錯誤訊息:

thread '<main>' panicked at 'index 0 and/or 2 in `忠犬ハチ公` do not lie on
character boundary'

接續(Concatenation)

如果你有一個 String, 你可以把一個 &str 接續到它後面:

let hello = "Hello ".to_string();
let world = "world!";

let hello_world = hello + world;

但是如果你有兩個 String, 你必須使用 &

let hello = "Hello ".to_string();
let world = "world!".to_string();

let hello_world = hello + &world;

這是因為 &String 可以自動強迫轉型(coerce)成一個 &str。這個功能被稱作「取值強迫轉型」(Deref coercisons)。

commit 310ab5e