# 初探 Rust 數值型態,史上最精簡的命名
Rust 的數值資料型態名稱,可以說是我在各種程式語言裡頭看過最簡練的。
在許多靜態型別、重視性能的程式語言當中,我們時常會需要給變數顯式地指定數值型態的大小。比方說,當我們使用 Java 寫遊戲,需要宣告一個變數來儲存玩家通關次數的時候,我們可能會考慮到一個玩家不可能累積通關數萬次,因而選擇使用最適合的 `short` 型態,而不是更大的 `int` 或 `long` 型態,以節省更多的記憶體空間。
然而,對於一個程式設計師而言,想分清楚這些資料型態的個別長度,多少還是需要一點記憶成本的。程式設計師難以單從 `short` 這個名字看出它佔 2 bytes 大小,且若遇上了像 C++ 那樣「`int` 長度因編譯器和平台而異」的情況,則更是折磨人。
<br/>
## ► Rust 的數值型態
對於 Rust 使用者而言,這種難題不存在──你只需要記得 Rust 的內建的數值型態分為整數、無號整數、浮點數,接下來就只剩長度大小的事了。
事實上,Rust 不是唯一一個給資料型態採用長度命名的程式語言。就我能查到的資料來看,Nim(2008)、Golang(2009)、Rust(2010)、Swift(2014)、Zig(2016)都原生使用長度來給基本數值型態命名。
以 Golang 作例,它就給整數分成了 `int`、`int8`、`int16`、`int32`、`int64`,正整數則有 `uint`、`uint8`、`uint16`、`uint32`、`uint64`,以及浮點數 `float32` 和 `float64`,而 Rust 在這方面把資料型態的名稱省略得更極致:它的數值型態分為整數的 `isize`、`i8`、`i16`、`i32`、`i64`、`i128`,無號整數的 `usize`、`u8`、`u16`、`u32`、`u64`、`u128`,以及浮點數的 `f32`、`f64`。
i 代表的是整數(integer):
* `isize`:長度大小取決於平台的整數,例如在 64 位元環境時,`isize` 就相當於 `i64`
* `i8`:佔用 8 位元的整數,可表示範圍是 -128 ~ 127
* `i16`:佔用 16 位元的整數 -32768 ~ 32767
* `i32`:佔用 32 位元的整數 -2147483648 ~ 2147483647
* `i64`:佔用 64 位元的整數 -9.2e18 ~ 9.2e18
* `i128`:佔用 128 位元的整數 -1.7e38 ~ 1.7e38
u 代表的是無號整數(unsigned integer):
* `usize`:長度大小取決於平台的無號整數,例如在 32 位元環境時,`usize` 就相當於 `u32`
* `u8`:佔用 8 位元的無號整數,可表示範圍是 0 ~ 255
* `u16`:佔用 16 位元的無號整數,可表示範圍是 0 ~ 65535
* `u32`:佔用 32 位元的無號整數,可表示範圍是 0 ~ 4294967295
* `u64`:佔用 64 位元的無號整數,可表示範圍是 0 ~ 1.8e19
* `u128`:佔用 128 位元的無號整數,可表示範圍是 0 ~ 3.4e38
f 代表的是浮點數(float):
* `f32`:佔用 32 位元的浮點數
* `f64`:佔用 64 位元的浮點數
這樣的命名方式已經短得無法再更短,卻能夠讓有一定底子的程式設計師一眼就曉得它的長度、確定它的範圍,即便是第一次接觸 Rust 語言。例如,我們透過 `u32` 這個名字就可以知道是無號整數 unsigned integer,佔用了 32 個位元,範圍是 0 ~ 4294967295;透過 `i16` 這個名字就可以知道是包含正負號的整數 integer,佔用了 16 個位元,範圍是 -32768 ~ 32767;透過 `f64` 就可以知道是佔用了 64 個位元的浮點數 float。
一般而言,`isize` 和 `usize` 型態是為了底層的指標操作而存在,通常在涉及記憶體分配的時候會比較容易用得到,像是我們給陣列指定長度的時候,編譯器預期輸入的形態就會是 `usize`,以確保在不同平台上存取記憶體位址的時候是安全的。在不涉及底層操作的情況下,我們通常不會使用 `isize` 或 `usize`。
如果你還記得我曾說過 Rust 語言會從各方面逼迫你寫出考慮周詳的程式碼,那你或許已經能從這樣的設計瞥見其中一角──當你需要定義變數欄位(包括函數、結構體)的時候,你必須清楚地寫出你使用的數值型態大小,而不是像其他語言一樣使用 `int` 這種依賴於慣例、由編譯器來決定的名稱,所有的選項細節都應該要是程式設計師自己也能清楚掌握的。
<br/>
## ► 宣告變數
在 Rust 當中,變數和函數都採 lower_snake_case 命名法。透過 let name: type = value 來宣告一個變數:
```rust
let var_1: u64 = 20000000000;
let var_2: u64 = 20_000_000_000;
let var_3: u64 = 0xDEADCAFE; // 十六進制的 DEADCAFE
let var_4: u32 = 0o455; // 八進制的 455
let var_5: u8 = 0b11101011; // 二進制的 11101011
println!("var_1: {var_1}");
println!("var_2: {var_2}");
println!("var_3: {var_3:X} in hexadecimal, {var_3} in decimal");
println!("var_4: {var_4:o} in octal, {var_4} in decimal");
println!("var_5: {var_5:b} in binary, {var_5} in decimal");
if var_1 == var_2 {
println!("`var_1` equals to `var_2`!");
}
```
從「20_000_000_000」可以看見,我們可以利用底線來給較大的數值分位,這樣一來在上述的例子當中,一眼就可以看得出 `var_2` 的值是兩百億了。透過實際執行最後的 if 條件判斷句,我們可以得知分位底線無論加不加都不會對程式本身造成影響,但可以讓程式設計師更易於閱讀。
在一些特殊場合下,我們也許還會用到二進制或十六進制等表示方法(例如加密、檔案讀寫、資料校驗、字串轉碼等),這也可以直接透過在數值前加上 0b、0o、0x 來分別表示二進制、八進制、十六進制,省去自行使用計算機轉換的麻煩。在 `println!` 當中,則可以使用 :X、:x、:o、:b 來指定一個數值應該被以什麼樣的進位方式顯示出來。
一般而言,如果沒有特別指定型態的話,通常編譯器會透過前後文來推斷變數的形態,整數預設是 `i32`,浮點數預設是 `f64`:
```rust
let my_int = 123; // -> i32
let my_float = 456.0; // -> f64
println!("The value of `my_int` is: {my_int}"); // -> 123
println!("The value of `my_int` is: {my_int:?}"); // -> 123
println!("The value of `my_int` is: {}", my_int); // -> 123
println!("The value of `my_int` is: {:?}", my_int); // -> 123
println!("The value of `my_float` is: {my_float}"); // -> 456
println!("The value of `my_float` is: {my_float:?}"); // -> 456.0
println!("The value of `my_float` is: {}", my_float); // -> 456
println!("The value of `my_float` is: {:?}", my_float); // -> 456.0
```
這裡也趁機說說 `println!` 的用法:當你想要使用 `println!` 印出變數內容的時候,你可以選擇直接把變數包在大括號當中,或者是先放入大括號,再隨後把變數排在 format 字串的後面,就像 C 語言當中的 `printf` 一樣。原則上會推薦把變數包在大括號裡頭,會更利於直接閱讀。
在 Rust 當中,資料的顯示行為還分成 `Display` 和 `Debug`。我們通常會在 format 字串的大括號當中加上「:?」來表示我們是依照 `Debug` 特徵來處理,通常用來顯示更詳細的資訊給開發者閱讀。這個實際講起來大概要等到提及特徵(trait)和定義結構體(struct)的時候才會比較明白,就留到之後再特別拿出來講吧。
<br/>
## ► Rust 內建的 overflow 處理機制
Rust 是安全至上的語言──這樣的理念貫穿了 Rust 的原則,極大地反映在內建的函式庫和許多語法的設計上面。
我們都知道溢位可能會帶來預期以外的行為,甚至是災難性的後果:若有一個 `i32` 型態的變數(範圍:-2147483648 ~ 2147483647)值為 2147483647,當電腦試圖把它往上加 1 的時候,它就會從最小值開始循環,變成 -2147483648。如果這變數是發生在遊戲當中角色的持有金錢數上面,這就會導致非常有錢的角色因為太過富有而一瞬間破產,從而引發其他更多潛在的 bug(例如存檔整個廢掉、角色卡關)。
對於一個上古時代以來就這麼具備代表性的常見漏洞,Rust 提供了完整的內建函式庫來處理。像是我們可以使用飽和加法 `saturating_add` 來包裝那些「可能會引發溢位」的加法:
```rust
let x = i8::MAX; // 127
let y = x.saturating_add(1); // 計算 x+1,如果會導致溢位的話則維持在上限值
println!("x: {x}, y: {y}"); // x: 127, y:127
```
在某些特殊情況下,你也可以基於情境的需要而允許它產生 overflow,使用 `wrapping_add` 來向 Rust 保證「我很確定這裡發生的溢位是我刻意為之的,你不要阻止我」:
```rust
let x = i8::MAX; // 127
let y = x.wrapping_add(1); // 計算 x+1,如果會導致溢位則放任其溢位,得到 -128
println!("x: {x}, y: {y}"); // x: 127, y: -128
```
你還能透過 `overflowing_add` 來檢查是否發生溢位。因為得到的結果是一對沒有實現 `Display` 特徵的複合型態 `tuple`,我們需要用 :? 讓它能夠正常顯示:
```rust
let x = i8::MAX; // 127
let add_result = x.overflowing_add(1); // 計算 x+1,允許溢位的同時確認過程中是否有發生溢位
println!("add_result: {add_result:?}"); // -> (-128, true)
```
除了 add 以外,也有 sub(減法)、mul(乘法)、div(除法)、abs(絕對值)、pow(冪次)、neg(負數)……可以說是基本運算都能夠使用內建的溢位處理機制,你完全不需要自己從頭判斷它具體的計算過程。
在 Rust 編譯器的預設行為當中,如果你不使用任何的處理機制,直接執行加法:
```rust
let x = i8::MAX; // 127
let y = x + 1;
println!("x: {x}, y: {y}");
```
那麼當你使用 `cargo run` / `cargo build` 執行或編譯出來的程式,會在發生溢位的時候觸發 panic,使得程式強制中止;使用 `cargo run --release` / `cargo build --release` 執行或編譯出來的程式,則會允許溢位行為發生而得到 -128。
僅僅是內建的方法,我們就能從這些貼心的細節當中感受到 Rust 所散發出來的魅力了。在接下來的許多章節,我們還可以看見 Rust 是如何提供你豐富的選項和工具,讓你能夠依照需求精細地量身訂製。
<br/>
## ► 其他的基本資料型態
若要講起「Rust 的基本資料型態」,就有 `bool`、`char`、`tuple`、`array`、`fn`。其中 `bool` 實在是沒有必要單獨拿出來說,畢竟會來寫 Rust 的人通常都是已經具備程式設計基礎的人,而其他的型態涉及的東西又多得沒辦法一口氣說完,這也是為何這篇文只著重於「數值型態」,而不是「資料型態」了。
在本系列的下一篇文當中,就來說說變數的可變性,以及 Rust 的字串是如何設計的吧。
<br/>
###### tags: `Rust` `程式設計`