字串(String)#

Rust 有兩種主要的字串型別:&strString。 理解它們的差異,對於學習後續的所有權(Ownership)概念至關重要。


1. 字串切片 &str#

&str 是對字串資料的 不可變參考 ,通常稱為「字串切片」。

字串字面值的型別就是 &str,資料直接嵌入在編譯後的執行檔中,因此永遠有效且不可變:

1
let hello: &str = "Hello, World!";

&str 只是指向某段 UTF-8 字串資料的指標,本身不擁有資料。


2. String 型別#

String 是可成長、可變、有所有權的字串,資料儲存在堆積(Heap)上:

1
2
3
4
let mut s = String::from("Hello");
s.push_str(", World!"); // 附加字串
s.push('!');            // 附加單一字元
println!("{s}");        // Hello, World!!

String::from() 是建立 String 最常見的方式,它將字串字面值的內容 複製到堆積 上,並取得所有權。


3. &str 與 String 的比較#

&strString
儲存位置堆疊(指標)+ 唯讀記憶體堆積
可變性不可變可變
所有權借用,不擁有擁有
大小編譯時已知執行時動態
建立方式"literal"String::from("...")

4. 兩者互轉#

&strString

1
2
3
let s1: &str = "hello";
let s2: String = s1.to_string();
let s3: String = String::from(s1); // 同上

String&str

1
2
3
let s: String = String::from("hello");
let slice: &str = &s;       // 自動解參考
let slice2: &str = &s[..];  // 明確取全部切片

5. 字串切片(Slice)#

可以用索引範圍取得字串的一部分,回傳 &str

1
2
3
4
let s = String::from("hello world");
let hello = &s[0..5];  // "hello"
let world = &s[6..11]; // "world"
let all   = &s[..];    // "hello world"

注意 :Rust 字串是 UTF-8 編碼,索引以 位元組 計算,切在多位元組字元中間會 panic。中文字每個字元佔 3 個位元組:

1
2
3
let s = String::from("你好世界");
// let ch = &s[0..1]; // panic!切在字元中間
let ch = &s[0..3]; // "你"(3 個位元組)

6. 常用方法#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn main() {
    let s = String::from("  Hello, Rust!  ");

    println!("{}", s.len());           // 長度(位元組數)
    println!("{}", s.is_empty());      // 是否為空
    println!("{}", s.contains("Rust")); // 是否包含子字串
    println!("{}", s.trim());          // 去除首尾空白
    println!("{}", s.to_lowercase());  // 轉小寫
    println!("{}", s.to_uppercase());  // 轉大寫
    println!("{}", s.replace("Rust", "World")); // 替換
}

7. 字串串接#

使用 + 運算子:

+ 會取走左側 String 的所有權,右側需傳入 &str

1
2
3
4
5
let s1 = String::from("Hello, ");
let s2 = String::from("World!");
let s3 = s1 + &s2; // s1 的所有權移入,s2 仍有效
// println!("{s1}"); // 錯誤!s1 已失效
println!("{s3}");

使用 format! 巨集(較推薦):

format! 不取走任何所有權,使用更直覺:

1
2
3
4
5
let s1 = String::from("Hello");
let s2 = String::from("World");
let s3 = format!("{s1}, {s2}!");
println!("{s3}"); // Hello, World!
// s1、s2 仍然有效

8. 迭代字元#

因為 UTF-8 編碼的關係,不能直接用索引取得字元,要用 .chars() 迭代:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fn main() {
    let s = String::from("你好Rust");

    for c in s.chars() {
        println!("{c}");
    }
    // 你
    // 好
    // R
    // u
    // s
    // t

    println!("字元數:{}", s.chars().count()); // 6
    println!("位元組數:{}", s.len());          // 10(中文各3字節)
}

9. 函數參數的慣例#

函數接受字串時,慣用 &str 而非 &String,因為 &String 可以自動強制轉型(coerce)為 &str,但反過來不行。這樣函數可以同時接受字串字面值和 String

1
2
3
4
5
6
7
8
fn greet(name: &str) {
    println!("你好,{name}!");
}

fn main() {
    greet("Alice");                       // &str 直接傳入
    greet(&String::from("Bob"));          // &String 自動轉為 &str
}

Reference#

https://doc.rust-lang.org/book/ch08-02-strings.html