107.06.09 rust 之路 11 Slices

除了 borrow 還有一個方式可以避免拿到所有權,就是 slice
slice 也是 borrow 的一種,但是可以 borrow 連續結構(String、array、vector....)的片段
一片檸檬,一口茶,一個章節,一段時光
Photo by Joanna Kosinska on Unsplash


先來看一個簡單的例子
first_word 函式是要尋找傳入字串的第一個單字
所以找到第一個空白就回傳,無空白就回傳整個字串長度
在 main 中拿到第一個單字最後的索引之後就可以透過原始 s 拿到第一個單字
fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }
    s.len()
}

fn main() {
    let mut s = "Hello World".to_string();
    let first_index = first_word(&s);
    s.clear();
    println!("{}", first_index);
    println!("{}", s.as_bytes()[first_index]); // panic!
}
但是有一個危險的事情發生了
上面的程式碼 first_index 會得到 5,不過 s 已經先執行了 clear() 了
所以若是透過 first_index 去拿字串就會 panic (程式異常中斷)
thread 'main' panicked at 'index out of bounds: the len is 0 but the index is 5'

Slices

因此就來介紹 slice
與 borrow 相似,但是 slice 借用的只有片段而不是全部
對於 String、&str 來說 slice 的型別都是 &str
使用 [開始..結束] 來表示範圍,注意,包含開始,不包含結束,使用 usize 表示位置
在 1.26.0 後可以使用 = 表示要包含結束
[..結束] 表示從 0 開始
[開始..] 表示到結尾
[..] 表示整個連續結構
e.g.
let s = "Hello world".to_string();
let slice1 = &s[..=4];  // "Hello" .... 1.26.0 stable
let slice2 = &s[6..];   // "World"
詳細關係如下圖:

右邊的 "Hello World" 是 string literal,會寫死在執行檔中
左邊 s 是 String,可以發現其指向 string literal
另外兩個是 slice 它們指向片段的 string literal

String v.s. &str

這裡就可以知道 String 和 &str 差異的根本了
去看 String 的原始碼可以發現,String 是一個向量 (Vec<u8>)
所以長度可變,而向量也存在 slice 的用法

而也可以知道為何大部分都是使用 &str 而不是 str
因為 str 是寫死在執行檔中,需要透過借用 (&str) 去讀取它的值
此借用是不可變的借用,所以才說 &str 長度固定

修改程式

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[..i]; // 回傳 slice 而不是 index
        }
    }
    &s[..]
}

fn main() {
    let mut s = "Hello World".to_string();
    let first_word = first_word(&s);
    s.clear();
    println!("{}", first_word);
}

這樣 compile 時就會出現錯誤,而不等到執行時期才 panic
錯誤原因很明顯,因為函式內部會使用到 borrow
但因為 borrow 的規則,不能同時可變、不可變
所以這樣寫程式是錯的,必須要改方式去寫
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src\main.rs:45:5
   |
44 |     let first_index = first_word(&s);
   |                                   - immutable borrow occurs here
45 |     s.clear();
   |     ^ mutable borrow occurs here
46 |     println!("{}", first_index);
47 | }
   | - immutable borrow ends here

正確的 main() 裡面應該改成這樣
let mut s = "Hello world".to_string();
{
    let first_index = first_word(&s);
    println!("{}", first_index);
}
s.clear();

習慣

通常用到 String 的 slice 時,函式的寫法傾向於傳入 slice
原因就是寫成 slice 就可以處理 String 和 &str
因為 String 轉成 &str 只需要取 slice
反之 &str 轉成 String 則需要用到 to_string(),就會需要額外的資源

所以大部分不寫成這樣:
fn first_word(s: &String) -> &str {

而是
fn first_word(s: &str) -> &str {

呼叫時:
/* s1 是 String */
first_word(&s1[..]);
first_word(&s1);

/* s2 是 &str */
first_word(&s2[..]);
first_word(&s2);
first_word(s2);

/* 發現不管是 String 或 &str 都可以用 */
first_word(&s);

沒有留言:

張貼留言

^ Top