[為你自己學 Rust] 資料型態(原始型別 - 陣列、元組)
前面章節介紹了有純量型(Scalar)的資料型別,這個章節來看看複合型(Compound)的資料型別。複合型主要有陣列(array)跟元組(tuple)這兩種。
也許各位看到陣列會覺得「啊這個我知道,就是用一個中括號...」,基本上是沒錯啦,但 Rust 的陣列會跟你平常在 JavaScript 裡用的陣列不太一樣。
陣列(Array)
如果要你簡單描述什麼是陣列,你大概會說陣列就是連續的資料,可以新增、修改或刪除裡面的資料,然後可以透過索引值(Index)取用指定的資料。在 JavaScript 我們可能寫過這樣的程式:
const list = [ 1450, "Hello", [], "A"];
console.log(list.length) // 總共 4 個元素
console.log(list[1]) // 印出 "Hello"
list.push({ name: '華安', num: 9527 }) // 加入新元素
console.log(list)
// 印出 [ 1450, 'Hello', [], 'A', { name: '華安', num: 9527 } ]
這應該沒什麼問題,但在 Rust 裡面的陣列就不是這樣用了...
只能放同樣性質的東西
在 JavaScript、Python 或 Ruby 裡的陣列,你想放什麼就放什麼,而且可以數字、字串混著放。但在 Rust 裡的陣列只能規定放相同型別的東西:
let list: [u8; 3] = [1, 2, 3];
println!("{:?}", list);
這裡我宣告了一個名為 list
的陣列,其中 [u8; 3]
的就是表示這個陣列有 3 個格子,然後格子裡面都是 u8
型別的資料。u8
在前面有介紹過,所以如果放的值超過 u8
的上限的話會出問題。因為有規定這些格子都只能放 u8
,所以如果這樣寫:
let list: [u8; 3] = ['a', 2, 3];
一執行 Rust 編譯器馬上就會跟你抱怨型別不對(mismatched types),而且無法執行:
$ cargo run
error[E0308]: mismatched types
|
2 | let list: [u8; 3] = ['a', 2, 3];
| ^^^ expected `u8`, found `char`
|
help: if you meant to write a byte literal, prefix with `b`
|
2 | let list: [u8; 3] = [b'a', 2, 3];
| ~~~~
如果覺得寫 [u8; 3]
太麻煩,也可以讓 Rust 編譯器幫你猜:
let list = [1, 2, 3];
這樣就會幫你宣告一個 [i32; 3]
的陣列。
格子數量是固定的,而且要放好放滿!
Rust 的陣列宣告好之後,大小就是固定的,不能改,所以在 Rust 裡面你不能對陣列進行新增或刪除的行為,改倒是可以改,但需要加上 mut
的宣告:
let mut list = [1450, 9527, 5566];
list[2] = 500;
println!("{:?}", list);
mut
我們會在下個章節跟大家說明。
就是因為陣列宣告好之後不能動態的加入元素,所以一開始就要把值放好放滿,如果多放或少放都會出錯:
let list: [i32; 3] = [9527, 5566]; // 故意少放一個
println!("{:?}", list);
馬上就會抱怨給你看:
$ cargo run
error[E0308]: mismatched types
|
2 | let list: [i32; 3] = [9527, 5566];
| -------- ^^^^^^^^^^^^ expected an array with a fixed size of 3 elements, found one with 2 elements
| | |
| | help: consider specifying the actual array length: `2`
| expected due to this
放同樣的型別有什麼好處?
到這裡,會不會感覺陣列規定要放同樣的型別這件事有點麻煩,宣告了數字陣列就只能放數字,不能像以前一樣想放什麼就放什麼。
事實上,當在 Rust 宣告了一個陣列之後,Rust 會去要一塊連續的記憶體來放指定的資料。例如我宣告了一個 [u8; 6]
的陣列,你可以想像就是要去跟記憶體要 6 個空位,每個格子的寬度是 8 位元:
大家可能已經知道陣列是透過索引值在拿資料的,但可能還不知道拿資料的細節是什麼。Rust 一開始去要這塊記憶體的時候就會知道這一連串的記憶體的「起點」在哪裡,假設我想要取得這個陣列的第 3 個格子的資料,我只要提供索引值 2
,同時也知道每一個格子的寬度是 8 位元,接著只要透過像是「起點 + 2 x 8 bit」簡單加法跟乘法,一下子就能找到第 3 格位置的記憶體位置:
這樣存取資料的效能非常好。Rust 其實也不是第一個這樣做的,只是平常大家可能被 JavaScript 給慣壞了,不會想這麼多細節,反正畫面會動就好。
陣列操作
跟其它程式語言的陣列操作差不多,直接透過索引值就能取用內容:
let list = [1450, 9527, 5566];
println!("{}", list.len()); // 印出 3
println!("{}", list[1]); // 印出 9527
在 JavaScript 如果用 list[100]
這種明顯超過應該有的索引值的寫法,只會默默的得到一個 undefined
而已(這設計其實有點糟糕),但如果在 Rust 裡這樣做,就會給你一個超過邊界的 Panic:
$ cargo run
error: this operation will panic at runtime
|
4 | println!("{}", list[100]);
| ^^^^^^^^^ index out of bounds: the length is 3 but the index is 100
與 JavaScript 選擇妥協相比,我更喜歡 Rust 這種有錯就直說而且還會跟你說哪裡有錯的設計。
如果想要印出陣列裡的每個值,以往大概會用 for
迴圈搭配索引值的寫法一個一個印出來,在 Rust 的 for
迴圈寫起來比較像是迭代器(Iterator),寫起來像是這樣:
let list = [1450, 9527, 5566];
for item in list.iter() {
println!("{}", item);
}
透過 for...in
的寫法可以把陣列裡的元素一個一個拿出來。其中因為 list
本身是陣列,所以需要再透過 .iter()
方法把它轉成 Iterator,才能透過 for ... in
處理。
另外,Rust 也有跟別人借來了「解構(destructuring)」的寫法,像這樣:
let list = [1450, 9527, 5566];
let [_, b, c] = list;
這樣就可以直接把變數 b
跟 c
的值設定成 9527
跟 5566
,前面那個 _
表示「不重要、不在乎」的意思。
陣列感覺很不實用?
在一般開發者的觀念中,陣列就是宣告要來放東西的,放同樣型別這件事可能還可以理解,但元素的個數不能調整就會讓人覺得那我要這陣列幹嘛?
事實上還是可以的,只是那個東西不叫陣列,在 Rust 裡這樣的東西叫做「向量(Vector)」,它用起來跟各位平常在用的陣列比較接近,請待我在後續的章節再跟大家說明。
元組(Tuple)
先不管這是什麼東西,首先,大家覺得 Tuple 這個英文字該怎麼發音?因為在程式語言 Python 裡也有 Tuple 這個資料結構,所以曾經有人在 Twitter 上問 Python 的設計者 Guido 這個字該怎麼唸,結果他很風趣的回答道:
他星期一、三、五唸「吐波」,星期二、四、六唸「塔波」,星期日就不唸它。事實上你想怎麼唸就怎麼唸,只要你自己或你的同事聽的懂就好。至於中文要翻譯成「元組」或「數組」就隨你開心,本書將會使用 Tuple 原文。
參考資料:https://twitter.com/gvanrossum/status/86144775731941376
Tuple 是一種資料結構,在 Rust 裡可以透過小括號來定義:
let point: (i32, i32, i32) = (100, 200, 300);
跟陣列不同,Tuple 裡的元素不一定需要相同型別:
let answer: (char, bool) = ('蛤', false);
同樣,如果不想寫型別宣告,也是能交給 Rust 編譯器猜:
let pet = ('🐈', false);
操作
陣列可以透過索引值存取元素的值,在 Tuple 也是差不多,只是寫起來會有點不太一樣,是透過小數點的方式:
let pet = ('🐈', false, 18);
println!("{} {} {}", pet.0, pet.1, pet.2)
Tuple 透過看起來像索引值的「欄位(field)」來存取資料,我知道它寫起來有點怪。跟陣列一樣,如果超過應該有的範例,例如 pet.5
,Rust 就會說沒有這個欄位:
$ cargo run
error[E0609]: no field `5` on type `(char, bool, {integer})`
|
3 | println!("{} {} {}", pet.5, pet.1, pet.2)
另外,類似陣列的解構,Tuple 也行:
let point = (100, 200, 300);
let (x, y, z) = point;
Tuple 的元素數量沒有規定要幾個,要 1 個、5 個、10 個或 100 個都可以,只是通常不會用到那麼大一包,比較常在函數的回傳值上看到它。
不過有個比較特別的情況,就是空的 Tuple,它有個特別的名字叫「單元(Unit)」。
單元(Unit)
單元不就是一個空的 Tuple,這有什麼好特別拿出來講的,甚至還特別給它一個名字?而且,Tuple 跟陣列一樣,宣告了元素個數之後就不能改變,所以要這空的 Tuple 到底能幹嘛?
我們到現在都還沒開始寫到函數,目前都只有在進入點的 main
函數裡練拳腳而已。通常函數都有回傳值,Rust 需要知道所有的變數、函數的型態,所以如果函數沒有回傳值也要明確的講沒有回傳值,有些程式語言會使用 void
的表示法,表示這個函數沒有回傳值的意思。
照 Rust 的設計,進入點的這個 main
函數照是沒有也不應該有回傳值的,在 Rust 裡面要用來表示沒有回傳值的寫法,就是回傳一個 Unit:
fn main() -> () {
println!("Hello Rust")
}
但這邊可以省略 Unit 不寫就只是 Rust 給的一個糖衣而已。也就是說,如果某個函數說「我的回傳值是一個 Unit」,就是表示「這個函數是沒有回傳值」的。