[為你自己學 Rust] 資料型態(原始型別 - 數字篇)
幾乎每款程式語言都有設計不同的資料型別,像是數字、字串、布林值之類的。Rust 自然也不例外,這個章節我們來看看在 Rust 裡的原始型別(Primitives)資料型態的「數字」。
數字
在寫 JavaScript 的時候,如果要宣告或定義數字,大概就是這樣寫:
let age = 20;
在 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 格,以此類推。如果我這樣宣告:
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
系列,例如:
let money: u32 = 28825252;
這個 u
是 unsigned
的意思,也就是給你的格子全部都可以拿來放值,第 1 格不用拿來放正負號,也就是說所有的值都會是正數。因此,i32
的最小值就是 0,最大值就是 232 - 1,也就是 4,294,967,295。
對於一般的網站工程師,這時候腦袋裡可能會有幾個問題:
1. 為什麼要分這麼細?就全部都數字就好了啊!
簡單的說,電腦的資源是有限的,如果明明知道用不到那麼多,幹嘛要拿那麼多資源?例如人類的年紀以目前的科學來說,沒意外的話,用 u8
應該很夠用(年齡不會是負數,而且正常人類活的歲數應該也不會超過 28 - 1 歲)。同時各位也可以想看看如果要宣告一個變數來存放你的銀行存款,該用多大的數字?
2. 如果超過範圍怎麼辦?
以 u8
來說,我故意放一個明顯超過這個範圍的數值:
let age: u8 = 1000;
println!("{}", age);
只要一執行就會發現 Rust 的編譯器比你更早發現這個問題,而且告訴你原因:
$ 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 這個程式語言的特別之一,就是它的錯誤訊息夠明顯。
如果我調皮一點,故意在邊界值再加一點點,像這樣:
let age: u8 = 255;
let new_age: u8 = age + 1;
println!("{}", age);
println!("{}", new_age);
各位在開車或騎車的時候,有沒有遇過車子的哩程表跑到 99999 公里之後再繼續跑會變多少公里?是會 + 1 變 100000 還是全部歸零成 00000?這在電腦科學領域有個專有名詞叫做「整數溢出(Integer Overflow)」,不同的程式語言在處理 overflow 的做法也不太一樣,有些會像哩程表一樣重頭再算過,有些則是會直接出錯。
Rust 在開發模式遇到這問題的時候會給個 Panic:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.10s
Running `target/debug/hello-rust`
thread 'main' panicked at 'attempt to add with overflow', src/main.rs:3:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Panic 在後面的章節還有更多的介紹,簡單的說,就是出錯並且中止程式。不過如果在 release 模式的話不會 Panic,而是給你「繞一圈」的答案:
$ 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 你可以使用 _
把數字稍微分開:
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 幫你算:
console.log((0.626).toString(2))
你會得到一個超級長的結果 0.10100000010000011000100100110111010010111100011010101
。事實上這根本算不完,就跟 10 除以 3 會得到 0.333333333... 一樣的無限循環,你在畫面上看到的只是一小部份。所以 7.626 轉換成二進位就變成 111.10100000010000011000100110...
,轉換成科學記號表示法就會變成 1.1110100000010000011000100110... x 22,這看起來還是差不多囉嗦,沒什麼幫助。目前很多程式語言都是根據 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 常會看到大家在笑它這個:
console.log(0.1 + 0.2 === 0.3) // 印出 false
然後就笑說 JavaScript 這什麼爛語言,事實上只要浮點數是照 IEEE 754 標準實作的,像 Python 跟 Ruby,包括 Rust 也是,印出來的答案都不會剛好等於 0.3。
型別推斷(Type Inference)
不像 JavaScript,Rust 對於型別是很要求的,型別不對就是不給過,所以照理說應該每當在宣告的時候都應該要明確的講明白它的型態。
let name: &str = "Hello Kitty";
let age: u8 = 20;
println!("hi, my name is {}, and I am {} years old", name, age);
那個 &str
的寫法現在可以暫時先略過它。但 Rust 的編譯器足夠聰明,就算沒有標記型態,它也能根據你給它的值推斷出來應該是哪個型別,所以這樣寫也是可以的:
let name = "Hello Kitty";
let age = 20;
println!("hi, my name is {}, and I am {} years old", name, age);
這樣寫起來清爽多了。不過型別推斷歸推斷,像這樣的程式碼之前在 JavaScript 寫起來沒什麼問題:
let age = 20 // 一開始是數字
age = "hello world" // 後來給它字串
console.log(age) // 最後印出 hello world 字串
但在 Rust 就沒辦法這樣了:
let mut age = 20;
age = 3.14;
println!("{}", age);
那個 mut
同樣可先暫時略過它,在後續的章節有更詳細的介紹,它是表示這個 age
變數是可以修改的。然而因為一開始你給 age
這個變數一個整數值 20
,所以 Rust 就推斷 age
應該是個整數,但後來你把它改成浮點數 3.14
,這就會造成型別上的錯誤:
$ 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
(誰知道你這數字以後會長多大?)。所以如果這樣寫:
let age = 100000000000000000000000000; // 明顯超過 i32 的範圍
執行的時候就會出錯了:
$ 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 難寫歸難寫,但它給的錯誤訊息還算挺清楚的。