資料型別

每個數值在 Rust 中都屬於某種資料型別,這告訴 Rust 何種資料被指定,好讓它能妥善處理資料。我們將討論兩種資料型別子集:純量(scalar)與複合(compound)。

請記住 Rust 是一門靜態型別語言,這代表它必須在編譯時知道所有變數的型別。編譯器通常能依據數值與我們使用的方式推導出我們想使用的型別。但有時候如果多種型別都有可能時,像是第二章的「將猜測的數字與祕密數字做比較」用到的 parseString 轉換成數字時,我們就需要像這樣加上型別詮釋:


#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("這不是數字!");
}

如果我們沒有加上型別詮釋的話,Rust 將會顯示以下錯誤訊息。這表示編譯器需要我們給予更多資訊才能夠知道我們想用何種型別:

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0282]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("這不是數字!");
  |         ^^^^^ consider giving `guess` a type

error: aborting due to previous error

For more information about this error, try `rustc --explain E0282`.
error: could not compile `no_type_annotations`.

To learn more, run the command again with --verbose.

你將會看到其他資料型別的各種型別詮釋。

純量型別

純量型別代表單一數值。Rust 有四種主要純量型別:整數、浮點數、布林以及字元。你應該在其他程式語言就看過它們了,讓我們來看看它們在 Rust 是怎麼使用的:

整數型別

整數是沒有小數點的數字。我們在第二章用到了一個整數型別 u32,此型別表示其擁有的數值應該是一個佔 32 位元大小的非帶號整數(帶號整數的話則是用 i 起頭而非 u)。表格 3-1 展示了 Rust 中內建的整數型別。帶號(signed)與非帶號(unsigned)的每一變體(比如 i16)都可以用來宣告一個整數數值。

表格 3-1:Rust 中的整數型別

長度帶號非帶號
8 位元i8u8
16 位元i16u16
32 位元i32u32
64 位元i64u64
128 位元i128u128
系統架構isizeusize

每個變體都可以是帶號或非帶號的,並且都有明確的大小。帶號非帶號的區別是數字能不能有負數,換句話說就是數字能否帶有正負符號,如果沒有的話那就只會出現正整數而已。就像在紙上寫數字一樣:當我們需要考慮符號時,我們就會在數字前面加上正負號;但如果我們只在意正整數的話,那它可以不帶符號。帶號數字是以二補數的方式儲存。

每一帶號變體可以儲存的數字範圍包含從 -(2n - 1) 到 2n - 1 - 1 以內的數字,n 就是該變體佔用的位元大小。所以一個 i8 可以儲存的數字範圍就是從 -(27) 到 27 - 1,也就是 -128 到 127。而非帶號可以儲存的數字範圍則是從 0 到 2n - 1,所以 u8 可以儲存的範圍是從 0 到 28 - 1,也就是 0 到 255。

另外,isizeusize 型別則是依據你程式運行的電腦架構來決定大小:如果你在 64 位元架構上的話就是 64 位元;如果你是 32 位元架構的話就是 32 位元。

你可以用表格 3-2 列的格式來寫出整數字面值(literals)。注意除了位元組(byte)的方式以外,所有的數字字面值都允許在最後面加上型別,比如說 57u8。另外也可以加上底線 _ 分隔方便閱讀,比如說 1_000

表格 3-2:Rust 中的整數字面值

數字字面值範例
十進制98_222
十六進制0xff
八進制0o77
二進制0b1111_0000
位元組(僅限u8b'A'

所以你該用哪些整數型別呢?如果你不確定的話,Rust 預設的型別通常就很好了,整數型別預設是 i32:此型別通常是最快的,甚至在 64 位元系統上也是。而你會用到 isizeusize 的主要時機是作為某些集合的索引。

整數溢位

假設你有個變數型別是 u8 可以儲存 0 到 255 的數值。如果你想要改變變數的值超出這個範圍的話,比方說像是 256,那麼就會發生整數溢位。Rust 有一些有趣的規則來處理這項行為。如果你是在除錯模式編譯的話,Rust 會包含整數溢位的檢查,造成你的程式在運行時恐慌panic)。Rust 使用恐慌來表示程式因錯誤而結束,我們會在第九章的「對無法復原的錯誤使用 panic!段落討論更多造成恐慌的細節。

當你是在發佈模式下用 --release 來編譯的話,Rust 則不會加上整數溢位的檢查而造成恐慌。相反地,如果發生整數溢位的話,Rust 會作出二補數包裝的動作。簡單來說,超出最大值的數值可以被包裝成該型別的最低數值。以 u8 為例的話,256 會變成 0、257 會變成 1,以此類推。程式不會恐慌,但是該變數可能會得到一個不是你原本預期的數值。通常依靠整數溢位的行為仍然會被視為邏輯錯誤,如果你想要安全顯式表達這種行為的話,你可以使用標準函式庫的型別 Wrapping

浮點數型別

Rust 還有針對有小數點的浮點數提供兩種基本型別:f32f64,分別佔有 32 位元與 64 位元的大小。而預設的型別為 f64,因為現代的電腦處理的速度幾乎和 f32 一樣卻還能擁有更高的精準度。

以下為展示浮點數的範例:

檔案名稱:src/main.rs

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

浮點數是依照 IEEE-754 所定義的,f32 型別是單精度浮點數,而 f64 是倍精度浮點數。

數值運算

Rust 支援你所有想得到的數值型別基本運算:加法、減法、乘法、除法和取餘。以下程式碼展示出如何在 let 陳述式使用這些運算:

檔案名稱:src/main.rs

fn main() {
    // 加法
    let sum = 5 + 10;

    // 減法
    let difference = 95.5 - 4.3;

    // 乘法
    let product = 4 * 30;

    // 除法
    let quotient = 56.7 / 32.2;

    // 取餘
    let remainder = 43 % 5;
}

每一個陳述式中的表達式都使用了一個數學運算符號並計算出一個數值出來,賦值給該變數。附錄 B 有提供列表列出 Rust 所提供的所有運算子。

布林型別

如同其他多數程式語言一樣,Rust 中的布林型別有兩個可能的值:truefalse。布林值的大小為一個位元組。要在 Rust 中定義布林型別的話用 bool,如範例所示:

檔案名稱:src/main.rs

fn main() {
    let t = true;

    let f: bool = false; // 型別詮釋的方式
}

布林值最常使用的方式之一是作為條件判斷,像是在 if 表達式中使用。我們將會在「控制流程」段落介紹如何在 Rust 使用 if 表達式。

字元型別

目前我們只討論了數字,但是 Rust 一樣也有支援字母。Rust 的 char 型別是最基本的字母型別,以下程式碼顯示了使用它的方法。(請注意到 char 字面值是用單撇號賦值,宣告字串字面值時才是用雙撇號)

檔案名稱:src/main.rs

fn main() {
    let c = 'z';
    let z = 'ℤ';
    let heart_eyed_cat = '😻';
}

Rust 的 char 型別大小為四個位元組並表示為一個 Unicode 純量數值,這代表它能擁有的字元比 ASCII 還來的多。舉凡標音字母(Accented letters)、中文、日文、韓文、表情符號以及零長度空格都是 Rust char 的有效字元。Unicode 純量數值的範圍包含從 U+0000U+D7FF 以及 U+E000U+10FFFF。但是一個「字元」並不是真正的 Unicode 概念,所以你對於什麼是一個「字元」的看法可能會和 Rust 的 char 不一樣。我們將會在第八章的「透過字串儲存 UTF-8 編碼的文字」來討論此議題。

複合型別

複合型別可以組合數個數值為一個型別,Rust 有兩個基本複合型別:元組(tuples)和陣列(arrays)。

元組型別

元組是個將許多不同型別的數值合成一個複合型別的常見方法。元組擁有固定長度:一旦宣告好後,它們就無法增長或縮減。

我們建立一個元組的方法是寫一個用括號囊括起來的數值列表,每個值再用逗號分隔開來。元組的每一格都是一個獨立型別,不同數值不必是相同型別。以下範例我們也加上了型別詮釋,平時不一定要加上:

檔案名稱:src/main.rs

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

此變數 tup 就是整個元組,因為一個元組就被視為單一複合元素。要拿到元組中的每個獨立數值的話,我們可以用模式配對(pattern matching)來解構一個元組的數值,如以下所示:

檔案名稱:src/main.rs

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("y 的數值為:{}", y);
}

此程式先是建立了一個元組然後賦值給 tup,接著它用模式配對和 lettup 拆成三個個別的變數 xyz。這就叫做解構(destructuring),因為它將單一元組拆成了三個部分。最後程式將 y 的值印出來,也就是 6.4

除了用模式配對解構元組以外,我們也可以直接用句號(.)再加上數值的索引來取得元組內的元素。舉例來說:

檔案名稱:src/main.rs

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

此程式建立了元組 x,然後用它們個別的索引產生新的變數。和多數程式語言一樣,元組的第一個索引是 0。

陣列型別

另一種取得數個數值集合的方法是使用陣列。和元組不一樣的是,陣列中的每個型別必須是一樣的。Rust 中的陣列和一些其他語言的陣列會有點不同,因為 Rust 的陣列是固定長度的,就和元組一樣。

在 Rust 中,陣列的寫法是將數值寫在中括號內,每個數值再用逗號區隔開來:

檔案名稱:src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];
}

當你想要你的資料被分配在堆疊(stack)而不是堆積(heap)的話,使用陣列是很好的選擇(我們會在第四章討論堆疊與堆積的內容)。或者當你想確定你永遠會取得固定長度的元素時也是。所以陣列不像向量(vector)型別那麼有彈性,向量是標準函式庫提供的集合型別,類似於陣列但允許變更長度大小。如果你不確定該用陣列或向量的話,通常你應該用向量就好。第八章將會討論更多向量的細節。

你會想用到陣列而非向量的時機,可能會像是以下要取得一年之中的每個月份的例子這樣。這樣的列表很可能永遠不需要新增或刪除月份,所以你可以選擇用陣列宣告,因為永遠只會有 12 個月份:


#![allow(unused)]
fn main() {
let months = ["一月", "二月", "三月", "四月", "五月", "六月", "七月",
              "八月", "九月", "十月", "十一月", "十二月"];
}

要詮釋陣列型別的話,你可以在中括號寫出型別和元素個數,並用分號區隔開來,如以下所示:


#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}

i32 在此是每個元素的型別,在分號後面的數字 5 指的是此陣列有五個元素。

這種寫法和初始化陣列數值的另一種寫法很像:如果你想建立的陣列中每個元素數值都一樣的話,你可以指定一個數值後加上分號,最後寫出元素個數。如以下所示:


#![allow(unused)]
fn main() {
let a = [3; 5];
}

陣列 a 會包含 5 個元素,然後每個元素的初始化數值均為 3。這樣寫與 let a = [3, 3, 3, 3, 3]; 的寫法一樣,但比較簡潔。

獲取陣列元素

一個陣列是被分配在堆疊上的一整塊記憶體,你可以用索引來取得陣列的元素,比如:

檔案名稱:src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

在此範例中,變數 first 會得到數值 1,因為這是陣列索引 [0] 的數值。變數 second 則會從陣列索引 [1] 得到數值 2

無效的陣列元素存取

如果我們存取陣列之後的元素會發生什麼事呢?假設你修改範例成以下程式碼的話,雖然可以編譯通過但是在執行時則會出現錯誤:

檔案名稱:src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];
    let index = 10;

    let element = a[index];

    println!("元素的數值為:{}", element);
}

使用 cargo run 執行程式會產生以下結果:

$ cargo run
   Compiling arrays v0.1.0 (file:///projects/arrays)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/arrays`
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:5:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

編譯期間沒有產生任何錯誤,但程式會產生執行時runtime)錯誤並無法正確離開。當你嘗試使用索引存取元素時,Rust 會檢查你的索引是否小於陣列長度,如果索引大於或等於陣列長度的話,Rust 就會恐慌。

這是你第一個在實例中看到 Rust 安全原則給予的保障。在許多低階語言並不會提供這樣的檢查,所以當你提供不正確的索引時,無效的記憶體可能會被存取。Rust 會保護你免於這樣的錯誤風險,並立即離開程式,而不是允許記憶體存取並繼續。第九章將會討論更多有關 Rust 的錯誤處理方式。