與 Rust 勾心鬥角 · 字串

garfileo發表於2022-04-10

由三個字元構成的字串

OFF

是「Object File Format」的簡寫。在 Rust 語言裡,字串對應的型別是什麼呢?

&str

若用 C 語言,可使用 char * 型別,例如

char *s = "OFF";

Rust 語言有類似的型別 &str,例如

let s: &str = "OFF";

或省略 s 的型別宣告

let s = "OFF";

rustc 認為雙引號包圍的符號串便是 str 型別的引用形式 &str 的字面量,因而能夠推斷出上例中 s 的型別。

C 語言能夠通過指標或下標形式遍歷字串,例如

char *s = "OFF";
for (char *p = s; *p != '\0'; p++) {
        printf("%c\n", *p);
}

size_t n = strlen(s);
for (size_t i = 0; i < n; i++) {
        printf("%c\n", s[i]);
}

Rust 語言能夠通過下標形式遍歷字串,但過程有些曲折,例如

let s = "OFF";
let t = s.as_bytes();
for i in 0 .. s.len() {
    let a: u8 = t[i];
    let b: char = a as char;
    println!("{}", b);
}

首先需要將字串轉化為位元組陣列,然後在遍歷陣列的過程中將位元組資料轉換為字元型別。倘若使用字串切片引用的方法,程式碼會優雅一些,例如

let s = "OFF";
for i in 0 .. s.len() {
    println!("{}", &s[i .. i + 1]);
}

不過,Rust 對字串給出了簡潔的語法糖,例如

let s = "OFF";
for c in s.chars() {
    println!("{}", c);
}

字串例項方法 chars 返回的是迭代器。使用字串的 chars 方法的好處是便於遍歷 UTF-8 編碼的字串,例如

let s = "OFF 格式";
for c in s.chars() {
    println!("{}", c);
}

倘若在遍歷每個字元時希望能夠獲得字元的下標,則可使用 char_indices 方法,例如

let hello = "你好,Rust!";
for x in hello.char_indices() {
    let (a, b) = x;
    println!("{}, {}", a, b);
}

let hello = "你好,Rust!";
for x in hello.char_indices() {
    println!("{}, {}", x.1, x.2);
}

輸出結果為

0, 你
3, 好
6, ,
9, R
10, u
11, s
12, t
13, !

char_indices 返回的迭代器產生的結果是元組(Turple)。上述程式碼展示了元組的基本用法。

字串比較

對於一個字串,如何確定它的值是否為 "OFF" 呢?只需寫一個能夠比較兩個字串是否相等的函式即可解決該問題。

fn str_eq(a: &str, b: &str) -> bool {
    let a_n = a.len();
    let b_n = b.len();
    if a_n != b_n {
        return false;
    } else {
        let a_bytes = a.as_bytes();
        let b_bytes = b.as_bytes();
        for i in 0 .. a_n {
            let a_i = a_bytes[i];
            let b_i = b_bytes[i];
            if a_i != b_i {
                return false;
            }
        }
    }
    return true;
}

我的 str_eq 寫得應該是有些醜陋,不過沒關係,本意就是想證明我有多麼不會 Rust。

下面測試 str_eq 能否正確工作:

let s = "OFF";
println!("{}", str_eq(s, "OFF"));
println!("{}", str_eq(s, "off"));
println!("{}", str_eq(s, "O F F"));

輸出

true
false
false

即使 str_eq 寫得醜陋也沒關係,在實際的程式碼裡,我並不會使用它,因為 str 型別實現了一個叫作 Eq 的 Trait,可直接用 eq 函式進行字串比較。例如

let s = "OFF";
println!("{}", s.eq("OFF"));

文字 -> 數字

有一個字串,表達一個小數,例如

let s = "0.618";

如何將其中的數字解析為 f64 型別的值呢?

寫一個將小數的字面值轉化為小數的函式並不是很難,所以就作為無聊時打發時間的練習題吧!在實際的專案裡,通常可以使用 str 的例項方法 parse 方法解決該問題。例如

let a: f64 = "0.618".parse().unwrap();

str 的例項方法 parse 返回值的型別為 Result,通常情況下需要基於模式匹配對其進行解構處理方能獲得所需的值,例如:

let a: f64 = match "0.618".parse() {
    Ok(v) => v,
    Err(e) => panic!("Error: {}", e)
};

上述針對 Result 型別的值的解構過程較為普遍,因此 Result 型別將上述過程定義為例項方法 unwrap

字串集

有一個字串,表達三個小數,以空格作為間隔,例如

let s = "0.618 2.718 3.141";

如何將其分割為三個字串,分別表達一個小數?

我知道 str 型別有一個 split_whitespace 方法能夠解決這個問題,但是現在為了近距離接觸 Rust,我應該為此寫一個簡單的狀態機,而不是用現成的方法。在寫這個狀態機之前,先確定如何表達字串的分割結果,亦即如何表示字串集。不妨以 Vec<(usize, usize)> 型別表示字串集,即以二元組為元素的向量,每個二元組用於記錄待分割字串中一段子字串的起止下標。

下面是一個試驗,表明通過 Vec<(usize, usize)> 能夠表達字串的分割結果。

let s = "0.618 2.718 3.141";
let mut slices: Vec<(usize, usize)> = Vec::new();
slices.push((0, 5));
slices.push((6, 11));
slices.push((12, 17));
println!("({0}, {1}, {2})",
         &s[slices[0].0..slices[0].1],
         &s[slices[1].0..slices[1].1],
         &s[slices[2].0..slices[2].1]);

輸出結果為

(0.618, 2.718, 3.141)

上述程式碼除包含了 Rust 元組的基本用法。,形如 &s[a..b] 的語法稱為字串切片,通過它能夠訪問字串中下標 a 到下標 b 之間的這段內容,若以數學區間的形式表示這段下標區間,可寫為 [a, b),即前閉後開區間。

狀態

要實現用於分割字串的狀態機,還要考慮如何表達狀態。Rust 有列舉型別,可用於表達狀態。例如

enum Status {
    Init,
    Space,
    NonSpace
}

為什麼有 Init 狀態而沒有 Stop 狀態呢?因為字串遍歷過程自身能夠終止,無需顯式給出狀態機的終止狀態。

下面的程式碼可在遍歷字串的過程中根據字元設定狀態:

fn display_status(m: &Status) {
    match m {
        Status::Init => println!("Init"),
        Status::Space => println!("Space"),
        Status::NonSpace => println!("NonSpace")
    }
}

fn main() {
    let s = "0.618 2.718 3.141";
    let mut m = Status::Init;
    display_status(&m);
    for x in s.char_indices() {
        let (_, b) = x;
        if b == ' ' {
            m = Status::Space;
        } else {
            m = Status::NonSpace;
        }
        display_status(&m);
    }
}

結果為

Init
NonSpace
NonSpace
NonSpace
NonSpace
NonSpace
Space
NonSpace
NonSpace
NonSpace
NonSpace
NonSpace
Space
NonSpace
NonSpace
NonSpace
NonSpace
NonSpace

上述程式碼複習了變數所有權借用、條件表示式以及模式匹配等內容。不過,將條件表示式改為模式匹配,程式碼通常會簡潔一些,例如

for x in s.char_indices() {
    match x.1 {
        ' ' => m = Status::Space,
        _   => m = Status::NonSpace
    }
    display_status(&m);
}

狀態機

狀態機由一些在狀態發生變化時觸發的功能構成。下面這個狀態機能夠基於空格對字串進行分割:

let s = "0.618 2.718 3.141";
let mut slices: Vec<(usize, usize)> = Vec::new();
let mut m = Status::Init;
for x in s.char_indices() {
    match m {
        Status::Init => {
            match x.1 {
                ' ' => m = Status::Space,
                _   => {
                    m = Status::NonSpace;
                    slices.push((x.0, x.0 + 1));
                }
            }
        },
        Status::Space => {
            match x.1 {
                ' ' => {},
                _   => {
                    m = Status::NonSpace;
                    slices.push((x.0, x.0 + 1));
                }
            }
        },
        Status::NonSpace => {
            match x.1 {
                ' ' => m = Status::Space,
                _   => {
                    slices.last_mut().unwrap().1 += 1;
                }
            }
        },
    }
}
for slice in slices {
    println!("({}, {})", slice.0, slice.1);
}

向量的 last_mut 方法可以返回 Result 型別,其中包含著向量最後一個元素的指標,且通過該指標可以修改該元素的值。倘若僅僅是訪問向量的最後一個元素,可使用 last 方法。

小結

enum Status {
    Init,
    Space,
    NonSpace
}

fn split_str(s: &str) -> Vec<(usize, usize)> {
    let mut slices: Vec<(usize, usize)> = Vec::new();
    let mut m = Status::Init;
    for x in s.char_indices() {
        match m {
            Status::Init => {
                match x.1 {
                    ' ' => m = Status::Space,
                    _   => {
                        m = Status::NonSpace;
                        slices.push((x.0, x.0 + 1));
                    }
                }
            },
            Status::Space => {
                match x.1 {
                    ' ' => {},
                    _   => {
                        m = Status::NonSpace;
                        slices.push((x.0, x.0 + 1));
                    }
                }
            },
            Status::NonSpace => {
                match x.1 {
                    ' ' => m = Status::Space,
                    _   => {
                        slices.last_mut().unwrap().1 += 1;
                    }
                }
            },
        }
    }
    return slices;
}

fn main() {
    let s = "0.618 2.718 3.141";
    let slices = split_str(s);
    for slice in slices {
        println!("({}, {})", slice.0, slice.1);
    }
}

相關文章