iT邦幫忙

2024 iThome 鐵人賽

DAY 8
0

變數

之前 echo function 有提到 Rust 預設變數(variable)是不可變的。Rust 設計上偏好讓開發者知道自己在做什麼,有點像在引導開發者思考方向,變數和 mut 的設計就是這樣。所有需要可變的地方都要用 mut 顯示的表達,會很自然而然地思考哪些變數是真的需要被變動的,這樣的設計對安全性、效能、可維護性都有正向作用。

  • 安全性:
    如果寫一個多執行緒程式,一個變數可能會被多個執行緒讀取,Rust 會在編譯期就做一些檢查,保證不會有操作去更改到不可變變數的值,這樣就不需要在程式執行後擔心某個執行緒會意外地修改它的值,導致其他執行緒在讀取該變數時出現非預期的結果,尤其這種錯誤很難排查。

  • 效能:
    當變數不可變時,編譯器可以以該值不會改變為前提進行優化,因此它可以減少對這個變數的重複讀取或重新計算,從而提高程式的性能。此外,Rust 也可以更安全地在多執行緒環境下共享不可變數據,而無需加鎖,進一步提高了並行程式的效率。

  • 可維護性:
    mut 的顯式表達使程式碼更好理解,當一個變數在其生命週期內不會改變時,開發者能更輕鬆地判斷程式的行為,而使用 mut 的地方代表需要修改某個變數,幫助開發者在審查程式碼時可以更明確的找出可能有的副作用或是影響範圍。

接著我們來看程式碼:

fn main() {
    let x = 5;
    println!("x 的數值為:{x}");
    x = 6;
    println!("x 的數值為:{x}");
}
$ cargo run
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         -
  |         |
  |         first assignment to `x`
  |         help: consider making this binding mutable: `mut x`
3 |     println!("x 的數值為:{x}");
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error

當我們嘗試重複賦值到不可變的變數時,Rust 的編譯器就會告訴我們這是不被允許的,如果需要重複賦值就需要用 mut 明確標示這個變數是可變的。

fn main() {
    let mut x = 5;
    println!("x 的數值為:{x}");
    x = 6;
    println!("x 的數值為:{x}");
}

接著在前面的元組與陣列有提到它們的長度都是不可變的,那元素是可以被改變的嗎?

答案是和前面純量的變數一樣,默認是不可變的。

fn main() {
    let my_tuple = (0, 'a', false);
    println!("original: {:?}", my_tuple);
    my_tuple.1 = 'b';
    println!("updated: {:?}", my_tuple);
}
$ cargo run
error[E0594]: cannot assign to `my_tuple.1`, as `my_tuple` is not declared as mutable
 --> src/main.rs:4:5
  |
4 |     my_tuple.1 = 'b';
  |     ^^^^^^^^^^^^^^^^ cannot assign
  |
help: consider changing this to be mutable
  |
2 |     let mut my_tuple = (0, 'a', false);
  |         +++

For more information about this error, try `rustc --explain E0594`.

編譯器提示也很清楚了,一樣在宣告變數的地方加上 mut 就可以了。

fn main() {
    let mut my_tuple = (0, 'a', false);
    println!("original: {:?}", my_tuple);
    my_tuple.1 = 'b';
    my_tuple.2 = true;
    println!("updated: {:?}", my_tuple);
}
original: (0, 'a', false)
updated: (0, 'b', true)

不過需要注意的是,因為是以變數為單位宣告,就代表這個元組所有的元素都是可變了,陣列的邏輯也是一樣的,它們都不允許有的元素可變有的元素不可變的情況。

常數

另外程式裡面常用到的另一個概念是常數(constants)

常數和不可變變數一樣,數值與名稱綁定且不允許被改變,但是常數又再更嚴格。

  1. 常數不可用 mut ,代表常數是永遠不可變的。
error: const globals cannot be mutable
  1. 常數在宣告的時候必須指定型別,不像變數如果我們沒有指定編譯器會自己判斷。
fn main() {
    const START_POINT = 0;
    println!("start point: {START_POINT}");
}
error: missing type for `const` item
  --> src/main.rs:10:22
   |
10 |     const START_POINT = 0;
   |                      ^ help: provide a type for the constant: `: i32`
  1. 常數只能被常數表達式設置,Rust 要在編譯的時候就確定數值,不能用在運行的時候才產生的其他數值設置。
use rand::Rng;

fn main() {
    let random_num = rand::thread_rng().gen_range(1..=100);
    const START_POINT: i32 = 0 + random_num;
    println!("start point: {START_POINT}");
}
error[E0435]: attempt to use a non-constant value in a constant
 --> src/main.rs:5:34
  |
5 |     const START_POINT: i32 = 0 + random_num;
  |     -----------------            ^^^^^^^^^^ non-constant value
  |     |
  |     help: consider using `let` instead of `const`: `let START_POINT`

另外 Rust 習慣常數的命名用英文大寫,英文詞彙中間用 _ 隔開,例如:

fn main() {
    const START_POINT: i32 = 0;
    println!("start point: {START_POINT}");
}

雖然用其他命名方式也不影響程式執行,例如把 START_POINT改成 startPoint,但編譯器會警告,建議還是照它的命名習慣。

$ cargo run
warning: constant `startPoint` should have an upper case name
 --> src/main.rs:2:11
  |
2 |     const startPoint: i32 = 0;
  |           ^^^^^^^^^^ help: convert the identifier to upper case: `START_POINT`
  |
  = note: `#[warn(non_upper_case_globals)]` on by default

warning: `variables` (bin "variables") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.09s
     Running `target/debug/variables`
start point: 0

全局變數

Rust 支援全局變數(global variable),在 Rust 中稱為靜態變數(static variable)。使用關鍵字 static宣告。靜態變數有以下特點:

  • 生命週期:靜態變數中的引用必須是'static生命週期,意味著它們在整個程式生命週期中都有效。
  • 初始化:必須在編譯時初始化。對於複雜的初始化邏輯,可以使用lazy_static巨集。
  • 命名慣例:使用尖叫蛇式命名(SCREAMING_SNAKE_CASE)。
  • 類型聲明:與一般變數不同,宣告時必須指定類型。
  • 可變性:與常數不同,靜態變數可以是可變的(使用mut)。

不可變靜態變數的例子:

static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("name 為:{}", HELLO_WORLD);
}

可變靜態變數的情況:

  • 訪問或修改被認為是不安全的操作,例如有多個執行緒同時存取同一個可變全域變數的情況,可能導致資料競爭(data race):讀取變數的值變得不可預知,可能造成程式非預期行為或崩潰。。
  • 使用可變靜態變數時,需要使用 unsafe 關鍵字。
static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);

    unsafe {
        println!("COUNTER: {}", COUNTER);
    }
}

unsafe關鍵字會開啟一個新的程式碼區塊,封裝不安全的操作。
這種設計允許開發者在必要時繞過 Rust 的一些安全檢查,但同時也將安全性的責任轉移到開發者身上。
訪問或修改可變靜態變數就是需要使用unsafe的操作之一。

對於簡單的全局值,優先使用const,常數更安全、更容易優化。
如果要使用可變全域變數,謹慎使用全局可變狀態,確保使用適當的同步機制,可以利用 Rust 中內建並發安全型別:Atomic(原子操作)、Mutex(互斥鎖)和 RwLock(讀寫鎖)等等。


上一篇
Day7 - 型別:元組與陣列
下一篇
Day9 - 變數遮蔽
系列文
螃蟹幼幼班:Rust 入門指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言