iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 16
1

大家好,今天要來介紹一種全新的資料型態 Slice,Slice 讓你指向一個集合部分的連續資料而且沒有任何的 ownership。

那麼為什麼會需要這個資料型態呢?話不多說就讓我們開始介紹 slice type 吧!

The Problem

首先假如我們要設計一支程式,他的功能是輸入一個字串然後返回第一個字,如果這支程式沒有找到斷字的空格就會把整個字串當成一個字返回。

所以讓我們來想像一下這支程式可能長什麼樣子,

fn first_word(s: &String) -> ?

這支程式傳了一個 reference 近來我們不想要讓他有 ownership 這沒有問題,那麼我們該怎麼找到字然後回傳呢?我們可以先試著用 for 迴圈找到字串結束的 index 如果有遇到空格就結束。

我們來試試看吧,

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()
}

這支程式有些沒看過的方法稍微了解了一下,as_bytes 回傳的是字串的 UTF-8 形式的 array 所以轉換出來之後就不是一般的字串而是 &[u8] 的格式。而根據這裡的說明https://ithelp.ithome.com.tw/upload/images/20191002/20119807CRQXqsmJnA.png

但是因為這個格式都是數字所以我們必需用人類可以懂的方式來寫,b' ' 前面的 b 就是代表 &[u8] 這個格式後面的 ' ' 就是空格,所以他的意思就是 &[u8] 這個格式的空格所以如果我們這樣寫 b'a',他就等於是 &[u8] 這個格式的 a

再來是 iter 這個方法中文就是迭代器是而這是 Rust 用來把 collection 跑過一次的功能,有點類似 JS 的[].map() ,不過 iter 會把值變成 Iter 型態的物件回傳而裡面有 enumerate(枚舉) 的方法可以拿到值。

呼講了很多細節但其實程式很簡單,就是回傳空格前的字元的 index,

fn main() {
  let text = String::from("hello a world");
  println!("first word end index = {}", first_word(&text));
  // first word end index = 5
}

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()
}

花了很多時間講這個程式可是其實他根本不重要!/images/emoticon/emoticon02.gif

這邊要探討的問題其實是這個

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // 拿到 index 為 5

    s.clear(); // 然後我把字串清空

    // word 的值還是 5 可是我們已經沒有 string 可以用了!所以 word 根本就跟垃圾一樣沒用
}

這樣在編譯時並不會報錯,所以如果我們這時候不小心誤用的 word 來寫我們的程式就會發生一堆 Bug!

因此 Rust 為了要讓我們從此不必再擔心害怕 Slice 出現了。

(本章的主角現在才出現...)
/images/emoticon/emoticon06.gif

String Slices

String Slices 就是 String 一部分的 reference 他的寫法如下,

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

各位可以看到他不是整個字串的 reference 而是你指定該字串起始和結束位置(用 index 表示)的 reference,我們同樣借官網的圖來表示,

https://ithelp.ithome.com.tw/upload/images/20191002/20119807aVTlEy3YQN.png

而 Rust 的設計師應該也跟我一樣懶惰,所以起始的 index 預設為 0 因此我們也可以這樣寫,

let s = String::from("hello");

// 這兩個相等
let slice = &s[0..2];
let slice = &s[..2];

而結束的 index 預設就是該字串的長度因此,

let s = String::from("hello");

let len = s.len();

// 這兩個相等
let slice = &s[3..len];
let slice = &s[3..];

所以如果你兩個都不寫就會變成這樣(非常懶)

let slice = &s[..];

Note:最後這邊官網給了貼心小提示,slice 只有在使用我剛剛解釋過的 UTF-8 格式才可以運作,如果不符合格式就會報錯,原文請看到[Note:](https://doc.rust-lang.org/book/ch04-03-slices.html#string-slices)。

有了新學的知識那麼我們就可以來改寫 first_word 的例子,

fn first_word_enhance(s: &String) -> &str {
  let bytes = s.as_bytes();

  for (i, &item) in bytes.iter().enumerate() {
    if item == b' ' {
      return &s[0..i];
    }
  }

  &s[..]
}

基本上就是套用 slice 的方法來取的第一個字或是整個字串,除了完成我們的功能之外還記得我們前面提到的 Bug 嗎?slice 讓他變成不可能發生的事情, 因為如果我們犯了錯誤在編譯期間就會報錯了!

  let mut s = String::from("hello world");

  // 把 s 借給了 word
  let word = first_word_enhance(&s);
                        -- immutable borrow occurs here

  // 把 s 清掉
  s.clear();
  ^^^^^^^^^ mutable borrow occurs here

  // 因為 s 已經被清掉了所以就不可能 reference 的到他因此會報錯。
  println!("the first word is: {}", word); 
                                    ---- immutable borrow later used here

還記得不能同時把值借給 mutable 跟 immutable 的變數嗎?這邊因為我們改寫了 first_word 他現在返回的也是 reference 所以可以很好的避免剛剛的 Bug 發生。

String Literals Are Slices

以下是翻譯官網的說明

早前有說過 string literals 是被存在 binary 裡面的,現在我們了解了 Slice 所以我們可以來了解這個,

let s = "Hello, world!";

這裡的 s 他的型態是 &str 他就是 slice 並且指向特定 binary 的位置,這也是為什麼 string literals 是 immutable 的因為 &str 本身就是 immutable 的 reference。

以下是我的理解

他的意思應該是在說為什麼當初我們在設計讓使用者輸入資料的時候只能使用

let s = String::from("test");

而其原因我猜是 &str 他其實是存放在記憶體位子上的 binary 的 reference,所以既然是存在記憶體上那麼我們就無法在執行期間對其修改。

但其實我也有點搞混搞清楚的話再回來補充/images/emoticon/emoticon01.gif

(求高人指點原文在這拜託了)

String Slices as Parameters

如果是有經驗的 Rust 工程師的話會把剛剛的程式,

fn first_word(s: &String) -> &str {

改寫成這樣

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

因為這樣寫的話就可以同時接受 &String&str 的型態,以下範例

fn main() {
    let my_string = String::from("hello world");

    // first_word works on slices of `String`s
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word works on slices of string literals
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

Other Slices

除了 string 可以 slice 之外我們也可以對 array slice 例如,

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

而且除了這兩種類型之外還有其他 collection 也可以 slice,筆者我稍微喵了一眼應該是 (VectorsHash Maps)。

總結

今天探討的問題其根本原因在於變數的依賴會因為其中一個的改變而造成一連串的變化,因此在設計程式時如果你的變數在改變的時候有可能影響依賴於這個變數的其他變數時,最好的做法是用 reference 的方式來設計。因為這樣在編譯期間有問題就會直接報錯。

最後恭喜各位 Ownership 告一個段落了!/images/emoticon/emoticon01.gif

那麼下一篇開始會來試試看用 rust 架一個 web service,文件也看的夠多了我們來練練刀吧!(其實是私心想要準備跟朋友的讀書會)

那麼我們明天見!

最後一樣有問題歡迎發問

/images/emoticon/emoticon07.gif

參考連結

appendix-02-operators


上一篇
[Day 15] Rust References and Borrowing 參照與借用
下一篇
[Day 17] Rust Actix PART1
系列文
WebAssembly + Rust 的前端應用30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
DanSnow
iT邦好手 1 級 ‧ 2021-04-06 12:07:10

這邊想補充一個小東西 range 省略任何一邊,其實不代表它們就會直接變成什麼預設值,而是省略後,讓接受這個 range 的函式去決定,省略的那邊應該要是什麼值,對 slice 而言,它填入的就是 0 與自己的長度

我要留言

立即登入留言