資料型別

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

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

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

如果我們沒有像上列程式碼這樣加上型別詮釋 : u32 的話,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("這不是數字!");
  |         ^^^^^
  |
help: consider giving `guess` an explicit type
  |
2 |     let guess: _ = "42".parse().expect("這不是數字!");
  |              +++

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

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

純量型別

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

整數型別

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

表格 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)。能適用於數種數字型別的數字字面值都允許在最後面加上型別,比如說用 57u8 來指定型別。數字字面值也可以加上底線 _ 分隔方便閱讀,比如說 1_000 其實就和指定 1000 的數值一樣。

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

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

所以你該用哪些整數型別呢?如果你不確定的話,Rust 預設的型別是很好的起始點:整數型別預設是 i32。而你會用到 isizeusize 的主要時機是作為某些集合的索引。

整數溢位

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

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

要顯式處理可能的溢位的話,你可以使用以下標準函式庫中基本型別提供的一系列方法:

  • 將所有操作用 wrapping_* 方法包裝,像是 wrapping_add
  • 使用 checked_* 方法,如果有溢位的話其會回傳 None 數值。
  • 使用 overflowing_* 方法,其會回傳數值與一個布林值來顯示是否有溢位發生。
  • 屬於 saturating_* ,讓數值溢位時保持在最小或最大值。

浮點數型別

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

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

檔案名稱: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 truncated = -5 / 3; // 結果爲 -1

    // 取餘
    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 的 char 型別是最基本的字母型別,以下程式碼顯示了使用它的方法:

檔案名稱:src/main.rs

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // 明確標註型別的寫法
    let heart_eyed_cat = '😻';
}

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

複合型別

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

元組(Tuple)型別

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

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

檔案名稱: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。

沒有任何數值的元組有一種特殊的名稱叫做單元型別(Unit),其數值與型別都寫作 (),通常代表一個空的數值或空的回傳型別。表達式要是沒有回傳任何數值的話,它們就會隱式回傳單元型別。

陣列型別

另一種取得數個數值集合的方法是使用陣列。和元組不一樣的是,陣列中的每個型別必須是一樣的。和其他語言的陣列不同,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

use std::io;

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

    println!("請輸入陣列索引");

    let mut index = String::new();

    io::stdin()
        .read_line(&mut index)
        .expect("讀行失敗");

    let index: usize = index
        .trim()
        .parse()
        .expect("輸入的索引並非數字");

    let element = a[index];

    println!(
        "索引 {index} 元素的數值爲:{element}"
    );
}

此程式碼能編譯成功。如果你透過 cargo run 執行此程式碼並輸入 01234 的話,程式將會印出陣列索引對應的數值。但如果你輸入超出陣列長度的數值,像是 10 的話,你會看到像是這樣的輸出結果:

thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:19:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

此程式會在使用無效數值進行索引操作時產生執行時(runtime)錯誤。程式會退出並回傳錯誤訊息,且不會執行最後的 println!。當你嘗試使用索引存取元素時,Rust 會檢查你的索引是否小於陣列長度,如果索引大於或等於陣列長度的話,Rust 就會恐慌。這樣的檢查必須發生在執行時,尤其是在此例,因爲編譯器無法知道之後的使用者將會輸入哪些數值。

這是 Rust 記憶體安全原則給予的保障。在許多低階語言並不會提供這樣的檢查,所以當你提供不正確的索引時,無效的記憶體可能會被存取。Rust 會保護你免於這樣的錯誤風險,並立即離開程式,而不是允許記憶體存取並繼續。第九章將會討論更多有關 Rust 的錯誤處理方式以及如何讓你寫出易讀且安全的程式碼,而不會恐慌或造成無效的記憶體存取。