ETH官方钱包

前往
大廳
主題 達人專欄

初探 Rust 數值型態,史上最精簡的命名

解凍豬腳 | 2024-07-07 19:15:02 | 巴幣 1362 | 人氣 730

 
本篇文章有不少程式碼,如果這裡的排版讓你感到閱讀困難,請服用 HackMD 好讀版



Rust 的數值資料型態名稱,可以說是我在各種程式語言裡頭看過最簡練的。

在許多靜態型別、重視性能的程式語言當中,我們時常會需要給變數顯式地指定數值型態的大小。比方說,當我們使用 Java 寫遊戲,需要宣告一個變數來儲存玩家通關次數的時候,我們可能會考慮到一個玩家不可能累積通關數萬次,因而選擇使用最適合的 short 型態,而不是更大的 int 或 long 型態,以節省更多的記憶體空間。

然而,對於一個程式設計師而言,想分清楚這些資料型態的個別長度,多少還是需要一點記憶成本的。程式設計師難以單從「short」這個名字看出它佔 2 bytes 大小,且若遇上了像 C++ 那樣「int 長度因編譯器和平臺而異」的情況,則更是折磨人。



? 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」這種依賴於慣例、由編譯器來決定的名稱,所有的選項細節都應該要是程式設計師自己也能清楚掌握的。



? 宣告變數

在 Rust 當中,變數和函數都採 lower_snake_case 命名法。

透過 let name: type = value 來宣告一個變數:
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:
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)的時候才會比較明白,就留到之後再特別拿出來講吧。



? Rust 內建的 overflow 處理機制

Rust 是安全至上的語言──這樣的理念貫穿了 Rust 的原則,極大地反映在內建的函式庫和許多語法的設計上面。

我們都知道溢位可能會帶來預期以外的行為,甚至是災難性的後果:若有一個 i32 型態的變數(範圍:-2147483648 ~ 2147483647)值為 2147483647,當電腦試圖把它往上加 1 的時候,它就會從最小值開始循環,變成 -2147483648。如果這變數是發生在遊戲當中角色的持有金錢數上面,這就會導致非常有錢的角色因為太過富有而一瞬間破產,從而引發其他更多潛在的 bug(例如存檔整個廢掉、角色卡關)。

對於一個上古時代以來就這麼具備代表性的常見漏洞,Rust 提供了完整的內建函式庫來處理。像是我們可以使用飽和加法 saturating_add 來包裝那些「可能會引發溢位」的加法:
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 保證「我很確定這裡發生的溢位是我刻意為之的,你不要阻止我」:
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,我們需要用 :? 讓它能夠正常顯示:
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 編譯器的預設行為當中,如果你不使用任何的處理機制,直接執行加法:
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 是如何提供你豐富的選項和工具,讓你能夠依照需求精細地量身訂製。



? 其他的基本資料型態

若要講起「Rust 的基本資料型態」,就有 bool、char、tuple、array、fn。其中 bool 實在是沒有必要單獨拿出來說,畢竟會來寫 Rust 的人通常都是已經具備程式設計基礎的人,而其他的型態涉及的東西又多得沒辦法一口氣說完,這也是為何這篇文只著重於「數值型態」,而不是「資料型態」了。

在本系列的下一篇文當中,就來說說變數的可變性,以及 Rust 的字串是如何設計的吧。



HackMD 好讀版:https://hackmd.io/@upk1997/rust-number-types

縮圖素材原作者:Karen Rustad T?lva(CC0 1.0)
送禮物贊助創作者 !
0
留言

創作回應

拉克螺絲起子
好強...
2024-07-07 19:57:52
解凍豬腳
強大的是 Rust [e5]
2024-07-22 20:32:37
愛德莉雅.萊茵斯提爾
能畫又睿智,豬腳超人(′∩ω∩`)
2024-07-07 20:20:19
解凍豬腳
Rust 語言讓我充滿力量https://truth.bahamut.com.tw/s01/202304/b056da90a1bd31bcbd3386c0baf956c0.JPG
2024-07-22 20:33:07

相關創作

更多創作