位元詩人 [Rust] 程式設計教學:變數 (Variable) 和資料型別 (Data Type)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

在本章,我們介紹撰寫 Rust 程式的基本概念,包括 Rust 程式的組成、變數和型別。

前幾章的程式,大部分都很簡單,但仍建議讀者實際練習一次;即使只是看著書照著打一次,都會有一些些幫助,因為 (1) 藉由這個過程熟悉建立 Rust 程式的過程,(2) 對於一些初階的錯誤,像是忘了加分號或打錯函式名稱,藉由實際練習才會改善。

透過閱讀得到知識的過程很快,但也很容易遺忘,透過肌肉操作得到知識的過程較慢,然而,一旦學會,記憶可維持較久。

再訪 Hello World

我們重新檢視 Hello World 範例:

fn main() {
    println!("Hello, World");
}

這個程式有單一的 main 函式 (function),在該函式內有一行 println!("Hello, World"); 敘述 (statement)。

函式是一種程式碼再利用的形式,透過函式,我們可以將一段程式碼組織起來,成為可重覆呼叫的區塊,避免撰寫重覆的程式碼。但 main 函式是一個特殊的函式,每個 Rust 應用程式都會有一個 main 函式,而且也只能有一個 main 函式。這個函式是程式的進入點。

main 函式固定的形式如下:

fn main() {
 // Implement code here.
}

現階段,我們不需要在意函式的其他相關細節,只要記得程式碼要寫在 main() 函式中即可。

println! 是一個巨集 (macro),其作用為在終端機 (console) 印出字串及附加換行 (newline) 符號。

在我們的範例中,println!("Hello, World") 會印出 "Hello, World" 字串。巨集是一種特殊的函式,在 Rust 程式中,巨集會用驚嘆號 ! 和一般的函式區別開來。現階段,我們不需要了解巨集的細節,就當成現有的函式即可。

println!("Hello, World"); 是一條敘述。程式由許多敘述組成,每條敘述的最後面會加上分號 ;。在預設情形下,程式會由上往下,依序執行敘述。見以下範例:

fn main() {
    println!("Hello, Rustacean.");
    println!("Welcome to Rust Programming.");
}

在本程式中,有兩條敘述,而本程式會由上往下依序印出 "Hello, Rustacean.""Welcome to Rust Programming." 這兩行字串。

註解 (Comments)

在撰寫程式時,註解 (comment) 是給程式人看的,不會影響程式的運行。在編譯程式時,註解會被抹除掉,故不影響程式的運行。

Rust 有三種註解的形式,分別為:

  • 單行註解 (line comments):以 // 開頭,之後該行文字皆視為註解
  • 多行註解 (block comments):以一對 /**/ 包起來,可跨越多行
  • 文件註解 (doc comments) 以 /// 開頭,製作程式說明文件時使用

以下為範例:

/* main function is program entry point.
   Each binary should contain one and
   only one main function. */
fn main() {
    // Print out the string Hello World in console.
    println!("Hello, World");
}

雖然程式碼本身就足以說明程式運作的過程,良好的註解可使得閱讀程式碼更有效率。本書會加入少量的註解以說明程式的運作過程。

由於程式碼本身即可顯示程式運作的細節,在實務上,註解通常用來標註某段程式的意圖 (intention),而非其運作方式。

關鍵字 (Keywords)

每個程式語言都會有自己的關鍵字 (keyword),關鍵字在程式碼中被賦予特殊的意義,不能作為其他用途,像是 fn 表示宣告一個函式。以下是 Rust 的關鍵字:

abstract alignof  as       become
box      break    const    continue
crate    do       else     enum
extern   false    final    fn
for      if       impl     in
let      loop     macro    match
mod      move     mut      offsetof
override priv     proc     pub
pure     ref      return   Self
self     sizeof   static   struct
super    trait    true     type
typeof   unsafe   unsized  use
virtual  where    while    yield

請讀者不需要去背誦關鍵字,因為:

  • 編輯器或 IDE 會用顏色提示使用者那些部分是關鍵字
  • 持續使用某個語言一段時間後,就會自然記住
  • 忘記時再查閱即可。像是每個程式語言的 else if 的寫法都略有不同,過一陣子沒用就忘了,也不需要刻意去記

變數 (Variable)

使用變數

在程式語言中,變數 (variable) 的作用在於使用資料 (data)。在程式碼中,程式透過變數來引用資料。見以下範例:

fn main() {
    let name = "Michelle";
    println!("Hello, {}", name);
}

在以上程式中,我們定義了變數 name,其值為字串 "Michelle"。接著,我們將此變數傳入 println! 巨集中印出。"Hello, {}" 的寫法是字串安插 (string interpolation),會在後面接續變數或資料。let 是 Rust 的關鍵字,其作用為宣告變數。

我們再看另一個範例:

fn main() {
    let greeting = "Goodbye";
    let name = "Michelle";
    println!("{}, {}", greeting, name);
}

在這個程式中,我們安插兩個變數進入 println! 巨集中,結果會印出 "Goodbye, Michelle" 字串。

如果變數沒有賦值就使用,會發生什麼事呢?見以下範例:

fn main() {
    let x;
    println!("x = {}", x);
}

這個程式會引發下列錯誤:

error[E0282]: unable to infer enough type information about `_`

這裡隱含著兩個概念,首先,變數要有值,否則會引發錯誤。再來,就是型別 (type) 的觀念;因為變數沒有值,Rust 無法藉由值推論變數的型別,故吐出此錯誤訊息。

程式語言的意義在於操作資料 (data),大部分的程式語言會將以不同的型別區分資料,Rust 也有許多的型別,我們後續會提到型別的相關概念。

但是,即使我們加上型別的資訊,若沒有指定值,仍然會引發錯誤。見以下範例:

fn main() {
    let x: i32;
    println!("x = {}", x);
}

這時候,引發了另一個錯誤:

error[E0381]: use of possibly uninitialized variable: `x`

這個錯誤告訴我們變數 x 未初始化,即未指定值。我們再改寫一下程式:

fn main() {
    let x = 3;
    println!("x = {}", x);
}

這次程式可正確地執行。雖然我們沒有在程式中明確指定 x 的型別,Rust 可由 3 得知 x 為 i32 (32 位元整數) 型別。這是由於 Rust 提供型別推斷 (type inference) 的功能,使得程式撰寫起在像 Python 等高階語言。

建立識別字 (Identifier) 的規則

變數名稱又稱為識別字 (identifier)。識別字原本在 Rust 程式中是沒有意義的,透過宣告變數這項動作對特定識別字賦予意義。

目前 Rust 的識別字採用以下規則:

  • 第一個字元為英文或底線 _
  • 第二個之後的字元為英文、數字或底線
  • 只有單一的底線 _ 不是變數

以下是合 Rust 規範的變數名稱:

  • x
  • x1
  • a_long_variable
  • aLongVariable
  • _var

對於較長的變數名稱,Rust 社群建議使用 snake case (像是 a_long_variable) 而非 camel case (像是 aLongVariable)。

Rust 會對不符合其撰碼風格的變數或函式名稱發出警告訊息,但不會引發錯誤。雖然 Rust 支援 Unicode,但目前只能用英文字母來命名變數 (見此 issue)。

變數的可變性

以下程式看似正常:

fn main() {
    let x = 0;
    x = 3;  // Error
    println!("x = {}", x);
}

但卻引發了下列錯誤:

error[E0384]: re-assignment of immutable variable `x`

Rust 和許多程式語言不同,在預設情形下,變數一旦賦值後就不能改變。然而,改變變數狀態是程式設計常見的功能,要如何處理呢?Rust 要求程式設計者必需明確指出某個變數是可變的 (mutable)。使用較安全的規範,是 Rust 的特色,程式設計者要試著去適應 Rust 的思維。

我們將程式改寫如下:

fn main() {
    let mut x = 0;
    x = 3;
    println!("x = {}", x);
}

這次程式即可正確執行。因為我們在程式中使用 mut 保留字使 Rust 知道我們的變數 x 是可變的。

宣告常數 (Constant)

Rust 另外提供 const 做為宣告常數 (constant) 使用。constlet 的區別在於 const 的值要在編譯期決定,僅能放入常值,不能放入函式回傳值或其他在執行期才能決定的值。

通常常數用在程式中不會更動且在程式中會出現多次的值。我們在程式中不應寫死數值,而應改用常數。藉由使用常數,日後我們只要更動一次常數的值,就可以自動作用在程式中所有出現該常數的地方。

資料型別 (Data Type)

我們在前面的程式中,使用了 Rust 的變數宣告,卻沒有明確指定 Rust 的型別 (type),這是因為 Rust 可自動推斷變數的型別。

Rust 就像大部分的程式語言,定義許多的型別。在程式設計中,資料型別規範該資料在程式中允許的操作,像是數字可以加、減、乘、除,字串可以相接等。Rust 定義了數個基礎型別 (primitive types)。除此之外,使用者也可以新增新的型別和其相關的操作。

如果 Rust 可正確推斷型別時,不需明確給定型別,但有時仍要明確給定型別,故我們在撰寫 Rust 程式時仍然要有型別的概念。

Rust 的內建型別

Rust 包括以下內建型別:

  • 布林數 bool
  • 數字 (numbers)
    • 整數 (integer)
    • 帶號整數 (signed)
    • 無號整數 (unsigned)
    • 浮點數 (floating-point number)
  • 字元 char
  • 字串 (strings)
    • 字串 str
    • 字串物件
  • 陣列 (array)、向量 (vector)、切片 (slice)
  • 元組 (tuple)
  • 指標 (pointer)
    • 參考 (reference)
    • Box
    • 祼指標 (raw pointers)

布林數 (Boolean)

Rust 內建布林 (boolean) 值,包括 truefalse 兩種值。布林主要用於條件句 (condition),後續的章節會說明。

字元 (Character)

字元代表單一的 Unicode scalar value (32 bit),字元以一對單引號 ' 括起來。

字串 (String)

Rust 中有兩種字串型別,一種是 String 類別,一種是 str。我們將於後續章節介紹字串的使用。

數字

Rust 有數種數字型別,主要可分為:

  • 整數

    • 帶號固定整數:包括 i8i16i32i64
    • 無號固定整數:包括 u8u16u32u64
    • 變動整數:包括 isizeusize
  • 浮點數

    • 單精度 (六至九位精碓度):f32
    • 倍精度 (15 位精確度):f64

有號和無號整數的差別在於是否有帶正負號,這會影響該數字的最小值和最大值。例如,i8 的最小值為 -128,最大值為 127,而 u8 的最小值為 0,最大值為 255。固定整數有一定的位元數,而變動整數的位元數會因平台而有所不同。浮點數有兩種,分別對應 IEEE-754 單精確度和雙精確度浮點數。

Rust 的整數有以下表示法:

  • 十進位數 (decimal):如 98_222
  • 十六進位數 (hex):如 0xff
  • 八進位數 (octal):如 0o77
  • 二進位數 (binary):如 0b1111_0000
  • Byte (僅限 u8):如 b'A'

以下程式列出 Rust 的每個數字型別的最小值和最大值。

// Call related modules in standard library
use std::{i8, i16, i32, i64, isize};
use std::{u8, u16, u32, u64, usize};
use std::{f32, f64};

fn main() {
    // signed, fixed-width integers
    println!("i8 min: {}, max: {}", i8::min_value(), i8::max_value());
    println!("i16 min: {}, max: {}", i16::min_value(), i16::max_value());
    println!("i32 min: {}, max: {}", i32::min_value(), i32::max_value());
    println!("i64 min: {}, max: {}", i64::min_value(), i64::max_value());

    // unsigned, fixed-width integers
    println!("u8 min: {}, max: {}", u8::min_value(), u8::max_value());
    println!("u16 min: {}, max: {}", u16::min_value(), u16::max_value());
    println!("u32 min: {}, max: {}", u32::min_value(), u32::max_value());
    println!("u64 min: {}, max: {}", u64::min_value(), u64::max_value());

    // variable-width integers (platform dependant)
    println!("isize min: {}, max: {}",
             isize::min_value(),
             isize::max_value());
    println!("usize min: {}, max: {}",
             usize::min_value(),
             usize::max_value());

    // floating point numbers
    println!("f32 min: {}, max: {}, min positive: {}",
             f32::MIN,
             f32::MAX,
             f32::MIN_POSITIVE);
    println!("f64 min: {}, max: {}, min positive: {}",
             f64::MIN,
             f64::MAX,
             f64::MIN_POSITIVE);
}

該程式實際的值會和系統相關,讀者可自行在自己的電腦上嘗試看看。

溢位 (overflow) 是程式在運算時,超過該型別的最大值;而下溢 (underflow) 則是程式在運算時,小於該數字型別的最小值。在 Rust 中,溢位或下溢會引發錯誤,這是較安全的設計。例如,以下程式引發溢位:

use std::i32;

fn main() {
    let mut n: i32 = i32::max_value();

    // Overflow
    n = n + 1;  
}

顯示以下錯誤訊息:

thread 'main' panicked at 'attempt to add with overflow'

由於電腦內部儲存數字的方式,數字有位數的限制。如果要計算的數字較大,需使用大數運算相關套件透過軟體模擬大數,如下例:

// Call third-party package
extern crate num;

use num::bigint::ToBigInt;

fn main() {
    let x = 2.to_bigint().unwrap();
    println!("{}", num::pow(x, 100));
}

若讀者想實際執行本程式,需在 Cargo.toml 中加入外部套件,如下:

[dependencies]
num = "0.1"

元組 (Tuple)

元組是一種異質容器,主要用來存放少量不同型別的資料。參考以下短例:

fn main() {
    let tup = (true, 100, "hello");

    assert_eq!(tup.0, true);
    assert_eq!(tup.1, 100);
    assert_eq!(tup.2, "hello");
}

在這個例子中,我們建立元組 tup,該元組存放三個值,三個值的型別皆相異。

陣列、向量、切片

這些為容器 (collection),將於後續章節中介紹。

指標 (Pointer)

Rust 的指標有以下數種:

  • 參考 (reference):受 Rust 管理的指標
  • Box:將資料存於記憶體的堆積 (heap) 中
  • 祼指標 (raw pointers):相當於 C 或 C++ 的指標,需於 unsafe 區塊才能使用

我們將於後續文章中討論指標。

使用物件 (Object) 建立新的型別

Rust 也支援物件導向程式,透過這種範式,程式設計者可以創造新的型別。不過,Rust 的物件系統和 Java 或 Python 等傳統的物件系統略有不同,我們會於後續文章中討論 Rust 的物件。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。