# [為你自己學 Rust] 資料型態（原始型別 - 數字篇）

> Rust 程式語言的數字資料型態分為整數與浮點數，並細分為多種位元型別。這篇文章說明了 Rust 中的原始型別，如何有效利用整數的位數來節省資源，同時介紹了整數溢出問題及其處理方式，幫助開發者深入理解數字型別的運用。

Published: 2023-09-18
URL: http://cdn.kaochenlong.com/primitive-data-types-part-1

---

幾乎每款程式語言都有設計不同的資料型別，像是數字、字串、布林值之類的。Rust 自然也不例外，這個章節我們來看看在 Rust 裡的原始型別（Primitives）資料型態的「數字」。

## 數字

在寫 JavaScript 的時候，如果要宣告或定義數字，大概就是這樣寫：

```javascript
let age = 20;
```

在 Rust 也差不多：

```rust
let age = 20;
```

看起來一模一樣對吧！表面上看起來一樣，但 Rust 的數字的種類分的比較細，在 JavaScript 不管是一般的整數或是小數，統統都是 `Number` 型別，但在 Rust 的數字就有細分成整數（Integer）跟浮點數（Floating-Point）兩種，而且分別還細分不同的範圍。

### 整數

整數，也就是不帶小數點的數字，根據不同的需求在 Rust 有 8 bit、16 bit、32 bit、64 bit 以及 128 bit 等不同的型別，8 bit 表示「我給你 8 個格子給你放東西，裡面可以放 0 或 1」，16 bit 就是 16 格，以此類推。如果我這樣宣告：

```rust
let age: i8 = 20;
```

這裡的 `i8` 表示宣告了一個 8 bit 的整數，但是 `i8` 的這 8 個格子，並不是全部都給你放 0 跟 1，它的第 1 個格子是給你放正負號，所以事實上只剩 7 個格子可以存放值，所以 `i8` 型別的最小值就是負 27，也就是 -128，而最大值是 27 - 1， 也就是 127。咦？為什麼正數要減 1，但負的不用？因為還要把卡在中間的 0 也算進來。

同理，`i32` 的最大值是 231 - 1 也就是 2,147,483,647，最小值是 -231，也就是 -2,147,483,648。

跟 `i` 系列有點像的還有 `u` 系列，例如：

```rust
let money: u32 = 28825252;
```

這個 `u` 是 `unsigned` 的意思，也就是給你的格子全部都可以拿來放值，第 1 格不用拿來放正負號，也就是說所有的值都會是正數。因此，`i32` 的最小值就是 0，最大值就是 232 - 1，也就是 4,294,967,295。

對於一般的網站工程師，這時候腦袋裡可能會有幾個問題：

1\. 為什麼要分這麼細？就全部都數字就好了啊！

簡單的說，電腦的資源是有限的，如果明明知道用不到那麼多，幹嘛要拿那麼多資源？例如人類的年紀以目前的科學來說，沒意外的話，用 `u8` 應該很夠用（年齡不會是負數，而且正常人類活的歲數應該也不會超過 28 - 1 歲）。同時各位也可以想看看如果要宣告一個變數來存放你的銀行存款，該用多大的數字？

2\. 如果超過範圍怎麼辦？

以 `u8` 來說，我故意放一個明顯超過這個範圍的數值：

```rust
let age: u8 = 1000;
println!(&quot;{}&quot;, age);
```

只要一執行就會發現 Rust 的編譯器比你更早發現這個問題，而且告訴你原因：

```shell
$ cargo run               
error: literal out of range for `u8`
  |
2 |     let age: u8 = 1000;
  |                   ^^^^
  |
  = note: the literal `1000` does not fit into the type `u8` whose range is `0..=255`
```

它告訴你 `type u8 whose range is 0..=255` 就是原因。Rust 這個程式語言的特別之一，就是它的錯誤訊息夠明顯。

如果我調皮一點，故意在邊界值再加一點點，像這樣：

```rust
let age: u8 = 255;
let new_age: u8 = age + 1;

println!(&quot;{}&quot;, age);
println!(&quot;{}&quot;, new_age);
```

各位在開車或騎車的時候，有沒有遇過車子的哩程表跑到 99999 公里之後再繼續跑會變多少公里？是會 + 1 變 100000 還是全部歸零成 00000？這在電腦科學領域有個專有名詞叫做「整數溢出（Integer Overflow）」，不同的程式語言在處理 overflow 的做法也不太一樣，有些會像哩程表一樣重頭再算過，有些則是會直接出錯。

Rust 在開發模式遇到這問題的時候會給個 Panic：

```shell
$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.10s
     Running `target/debug/hello-rust`
thread &#39;main&#39; panicked at &#39;attempt to add with overflow&#39;, src/main.rs:3:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

Panic 在後面的章節還有更多的介紹，簡單的說，就是出錯並且中止程式。不過如果在 release 模式的話不會 Panic，而是給你「繞一圈」的答案：

```shell
$ cargo run --release
    Finished release [optimized] target(s) in 0.09s
     Running `target/release/hello-rust`
255
0
```

如果是 `u8` 型別，255 + 1 會變成 0， `i8` 型別的話 127 + 1 會變成 -128。以結果來說，程式執行不會出錯，但我想算出來的答案不會是你想要的。

附帶一提，我們人類比較習慣在千位數的地方加上逗號，能更快識別出這個數字是幾位數，在 Rust 你可以使用 `_` 把數字稍微分開：

```rust
let books: u16 = 1_000_00_0;
```

但其實這個 `_` 並沒有什麼意思，所以像我上面這樣隨便亂加也無所謂。（其實在其它程式語言像是 JavaScript、Python、Ruby 也都可以這樣寫，這不是 Rust 特有的寫法）

除了固定的 8、16、32、64 以及 128 位元外，還有兩個比較特別的 `isize` 跟 `usize`，從字面上大概可以猜的出來 `i` 跟 `u` 的意思，而 `size` 則是會依據作業系統本身的 CPU 架構而有所不同，例如在 32 位元的作業系統，`isize` 就等同於 `i32`，同理，如果是在 64 位元的作業系統，`isize` 就等於 `i64`。

## 浮點數

浮點數其實就是帶有小數點的數字，跟整數一樣，浮點數也有分 bit，但只有 32 bit（`f32`） 跟 64 bit（`f64`），而且第 1 個 bit 都是帶正負號的，不像整數還有 unsigned 的設計。根據 Rust 手冊上寫著，根據 IEEE-754 標準，`f32` 是「單精準度（single-precision）」浮點數，`f64` 則是「雙精準度（double-precision）」浮點數。

蛤？等等...什麼單雙倍的？這是什麼意思，不就是加個小數點嗎？這裡就有一些計算機概論的內容需要科普一下了。

### 科學記號表示法

我記得以前讀書時候，老師有時候會把一些特別大或是特別小數字用另一種方式來表示，例如在我高中化學曾經學過的亞佛加厥常數 6.02 × 1023 或是原子質量 1.66 × 10-27，老實說當時我不知道學這個常數或是原子質量要幹嘛，只知道用這樣的寫法可以讓數字看起來簡單一點，這樣的表示法稱之「科學記號表示法（Scientific Notation）」。使用科學計算表示法除了可以簡化原本很大或很小的數字外，在做運算的時候也挺方便，例如 30,000,000,000 乘以 0.000000015 等於多少？我相信這不難算，但那麼多個零看了眼睛都花了，如果改寫成科學記號表示法的話會變成 3 x 1010 乘以 1.5 x 10-8，這樣一來計算的時候就可以分開算，前面 3 x 1.5 = 4.5，而後面的 1010 x 10-8 就會得到 102，最後答案就是 4.5 x 102，也就是 450。

我們人類最常見的數字系統是十進位，我們能用科學記號表示法寫出 4.5 x 102 就是建立在十進位的系統之上。

我們再看看電腦的二進位，例如數字 7.625，它要怎麼轉成 2 進位？整數的部份比較簡單，5 可以分解成：

```
1 x 22 + 1 x 21 + 1 x 20 = 4 + 2 + 1 = 7
```

所以 7 轉成二進位就是 `111`。小數 .625 的部份也是差不多的原理，只是指數的部份要改用負數：

```
1 x 2-1 + 0 x 2-2 + 1 x 2-3 = 0.5 + 0 + 0.125 = 0.625
```

所以 7.625 轉成二進位就是 `111.101`。如果再轉換成二進位的科學記號表示法就會變成 1.11101 x 22。7.625 是剛好可以完美轉換成二進位的數字，但不是每天都在過年的，如果再大一點點，例如 7.626 呢？整數部份沒問題，還是 `111`，但小數部份就麻煩了，這有點難算，所以你可以用 JavaScript 幫你算：

```javascript
console.log((0.626).toString(2))
```

你會得到一個超級長的結果 `0.10100000010000011000100100110111010010111100011010101`。事實上這根本算不完，就跟 10 除以 3 會得到 0.333333333... 一樣的無限循環，你在畫面上看到的只是一小部份。所以 7.626 轉換成二進位就變成 `111.10100000010000011000100110...`，轉換成科學記號表示法就會變成 1.1110100000010000011000100110... x 22，這看起來還是差不多囉嗦，沒什麼幫助。目前很多程式語言都是根據 [IEEE 754](https://zh.wikipedia.org/zh-tw/IEEE_754) 的規範來顯示小數部份，IEEE 754 規範了幾種用來呈現浮點數的方式，其中 32 位元的就是「單精準度」，而 64 位元因為是 32 位元的兩倍，所以就是「雙精準度」。就以 32 位元的單精確度的遊戲規則來說：

第 1 位元是放正負數的符號（sign bit），如果 0 表示正數，1 表示負數。 第 2 \~ 9 這 8 個位元是指數（exponent） 剩下第 10 \~ 32 這 23 個位元則是放實際的值（fraction）。也就是說，一個 32 位元的浮點數，只能存放 23 位有效數值。如果是雙精準度的 64 位元的話，它的指數部份佔 11 位元，所以實際能存放的有效位數只有 52 位數。

但問題是，後面會無限循環的數字就算是能放 1000 位數也沒用，再怎麼樣就是不夠放，沒辦法顯示完整怎麼辦？不完整也沒辦法了，就算了吧。也就是因為有效位數沒辦法放完整的數值，所以這也是為什麼大家常說浮點數不是 100% 精準的原因。

參考資料：https://zh.wikipedia.org/zh-tw/IEEE_754

### 0.1 + 0.2 = ?

就是 0.3 啊，不然呢？這是個很好的面試題，以人類的常識來說， 0.1 + 0.2 就是 0.3，但以電腦來說就不是這樣了。如上面所說，電腦裡存放的 0.1 跟 0.2 都不是剛好真的 0.1 跟 0.2，只是非常接近而已。所以在電腦上運算 0.1 + 0.2 的結果也會很接近 0.3，但因為有效位數沒辦法存放所有的位數，剛好在相加進位之後變成 `0.30000000000000004`，所以在 JavaScript 常會看到大家在笑它這個：

```javascript
console.log(0.1 + 0.2 === 0.3)  // 印出 false
```

然後就笑說 JavaScript 這什麼爛語言，事實上只要浮點數是照 IEEE 754 標準實作的，像 Python 跟 Ruby，包括 Rust 也是，印出來的答案都不會剛好等於 0.3。

## 型別推斷（Type Inference）

不像 JavaScript，Rust 對於型別是很要求的，型別不對就是不給過，所以照理說應該每當在宣告的時候都應該要明確的講明白它的型態。

```rust
let name: &amp;str = &quot;Hello Kitty&quot;;
let age: u8 = 20;

println!(&quot;hi, my name is {}, and I am {} years old&quot;, name, age);
```

那個 `&amp;str` 的寫法現在可以暫時先略過它。但 Rust 的編譯器足夠聰明，就算沒有標記型態，它也能根據你給它的值推斷出來應該是哪個型別，所以這樣寫也是可以的：

```rust
let name = &quot;Hello Kitty&quot;;
let age = 20;

println!(&quot;hi, my name is {}, and I am {} years old&quot;, name, age);
```

這樣寫起來清爽多了。不過型別推斷歸推斷，像這樣的程式碼之前在 JavaScript 寫起來沒什麼問題：

```javascript
let age = 20           // 一開始是數字
age = &quot;hello world&quot;    // 後來給它字串

console.log(age)       // 最後印出 hello world 字串
```

但在 Rust 就沒辦法這樣了：

```rust
let mut age = 20;
age = 3.14;

println!(&quot;{}&quot;, age);
```

那個 `mut` 同樣可先暫時略過它，在後續的章節有更詳細的介紹，它是表示這個 `age` 變數是可以修改的。然而因為一開始你給 `age` 這個變數一個整數值 `20` ，所以 Rust 就推斷 `age` 應該是個整數，但後來你把它改成浮點數 `3.14`，這就會造成型別上的錯誤：

```shell
$ cargo run
error[E0308]: mismatched types
  |
2 |     let mut age = 20;
  |                   -- expected due to this value
3 |     age = 3.14;
  |           ^^^^ expected integer, found floating-point number
```

這個抱怨內容大概就是「不是說好是整數嗎？怎麼變成浮點數了」。

但是整數有那麼多種，如果只寫 `let age = 18`，它會給哪一種？沒特別講的話，就算你只給它一個小小的數字 `1`，Rust 預設還是會給你 `i32`。如果沒特別標記型別的話，Rust 的確是會看你是整數或浮點數，分別給你 `i32` 以及 `f64`，但並不會自動依據數值的大小自動調整成 `i8` 或 `i64`（誰知道你這數字以後會長多大？）。所以如果這樣寫：

```rust
let age = 100000000000000000000000000;  // 明顯超過 i32 的範圍
```

執行的時候就會出錯了：

```shell
$ cargo run
error: literal out of range for `i32`
  |
2 |     let age = 100000000000000000000000000;  // 明顯超過 i32 的範圍
  |               ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: the literal `100000000000000000000000000` does not fit into the type `i32` whose range is `-2147483648..=2147483647`
```

不得不說，Rust 難寫歸難寫，但它給的錯誤訊息還算挺清楚的。

