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