Rust入門系列之語言特性 - 1

木木劍光發表於2022-02-10

1. 概述

Rust是一門編譯型的語言(AOT - ahead of time),生成可執行程式碼前需要先編譯,這是和 JavaScript 等解釋型語言根本上的區別。

2. 變數與可變性

Rust中通過let來宣告變數,但let宣告的變數預設是不可變(_Immutable_)變數

let 宣告變數後嘗試修改它的值,編譯時就會報錯。

fn main() {
    let a = 5;
    a = 6; // error: cannot assign twice to immutable variable
}

我們可以使用以下兩種方式來解決這個問題

  • let 宣告變數時使用 mut 關鍵字進行修飾,表示它是一個可變變數。需要注意的是,Rust是強型別語言,所以即使宣告為可變變數,也只能重新賦值為相同資料型別的值

    fn main() {
      let mut a = 5;
      a = 6;
      // 改變資料型別,編譯報錯
      a = "6"; // error: expected integer, found `&str`
    }
  • 使用 shadowing 特性,再次宣告此變數覆蓋之前的值,並且不受之前資料型別的限制。相當於重新宣告後,之前的變數就被隱藏了

    fn main() {
      let a = '5';   // '5'
      let a = 5;     // 5
      let a = a + 1; // 6
      let a = a * 2; // 12
    }

3. 常量

Rust中通過 const 關鍵字宣告常量,常量與變數的區別是

  • 不可以使用 mut 關鍵字
  • 常量在宣告時必須指定資料型別
  • 常量可以在任何作用域宣告,包括全域性作用域
  • 常量只可以繫結到常量表示式

命名規範:字母全部大寫,單詞之間通過 _ 連線,如

const MAX_AGE: u32 = 10_0000;

4. 資料型別

4.1 標量型別

4.1.1 整數型別

  • 有符號整數使用 i 開頭 $$[-(2^n-1) , 2^{n-1}-1]$$
  • 無符號整數使用 u 開頭$$[0 , 2^n - 1]$$
  • isize和usize型別的位數是由程式執行的計算機的架構所決定,執行在64位的計算機上,則是64位的
  • 整數的預設型別為 i32

截圖2022-02-01 下午9.48.42.png

4.1.1.1 整數字面值

  • 16進位制使用 0x 開頭
  • 8進位制使用 0o 開頭
  • 2進位制使用 0b 開頭
  • byte型別的資料型別僅限 u8,使用 b 開頭

截圖2022-02-01 下午10.10.27.png

除了 byte 型別外,所有數值的字面值都允許使用型別字尾,如

// 表示 u8 型別的數值 57
let a = 57u8;

4.1.1.2 整數溢位

如果把一個 u8(0-255) 型別的變數設定為 256,會有一下情況:

  • 開發模式下,Rust檢測到溢位,在程式執行時就會 panic
  • 釋出模式下,Rust不會檢測溢位,當發生溢位時會執行環繞操作:

    • 256 -> 0
    • 257 -> 1
    • ...

4.1.2 浮點型別

  • f32,單精度
  • f64,雙精度(Rust浮點數的預設型別,因為在現代CPU上 f64 和 f32 的速度差不多)
  • Rust 浮點型別使用了 IEEE-754 標準

4.1.3 布林型別

  • 佔用一個位元組
  • 符號為 bool,值為 true | false

4.1.4 字元型別

  • 符號為 char
  • 字面值使用單引號

4.2 複合型別

4.2.1 元組(tuple)

  • 宣告後長度不可變
  • 可將不同資料型別的值組合到一起

    // 宣告一個元組
    let tup: (i32, f64, u8) = (500, 5.6, 23);
    
    // 獲取元組成員
    
    // 1.解構
    let (x, y, z) = tup;
    println!("{}, {}, {}", x, y, z); // 500, 5.6, 23
    
    // 2.點標記法
    println!("{}, {}, {}", tup.0, tup.1, tup.2); // 500, 5.6, 23

4.2.2 陣列

  • 和 tuple 一樣,宣告後長度不可變
  • 陣列成員的資料型別必須相同
  • 陣列是存在棧記憶體中
  • 陣列的型別通過 [type; length] 的形式表示

    let arr: [i32; 5] = [1,2,3,4,5];
    
    // 特殊的陣列宣告方法
    let sameMemberArray = [3; 5]; // [3,3,3,3,3]
    
    // 訪問陣列成員
    let f = arr[0]; // 1
    
    // 訪問陣列越界 - rust編譯器會進行簡單的越界檢查
    let more = arr[6]; // 編譯報錯
    
    // 訪問陣列越界 - rust編譯器檢測不到的場景
    let temp = [6,7,8,9];
    let more_temp = arr[temp[0]]; // 編譯通過,執行時報錯

5. 函式

  • Rust中使用 fn 關鍵字宣告函式
  • 對於函式和變數名,使用 snake case 規範來命名 (單詞小寫,使用 _ 拼接)
  • 引數在定義時必須指定資料型別
  • 如果要提前返回一個值,則使用 return 關鍵字
  • 函式預設返回值是一個空元組

    fn main() {
      let number = get_number(5);
      
      println!("{}", number); // 10
    }
    
    fn get_number(a: i32) -> i32 {
      // 函式中最後行如果是表示式,那麼表示式的值則會作為函式的返回值
      // 如果行尾加了分號,則會被識別為語句,就不會被作為函式的返回值
      a + 5
    }

6. 控制流

6.1 條件分支 - if

  • 需要注意的是,每個分支塊如果有返回值,必須保證資料型別相同

    let a = 3;
    
    // if else
    if a == 3 {
      println!("a is 3");
    } else {
      println!("a is not 3");
    }
    
    // 使用 表示式 的特性達到其他語言中 三元表示式 的效果
    let b = if a == 3 { 5 } else { 6 }; // 5 

6.2 迴圈

  • loop

    • loop 會無限迴圈執行迴圈體中的程式碼,直到被 break 中斷
    • break 可以為 loop 迴圈的表示式提供一個返回值
    // loop 迴圈
    let mut count = 0;
    
    let value = loop {
      count += 1;
      
      if count == 10 {
          break count * 2;
      }
    }
    
    println!("{}", value); // 20
  • while

    let arr = [1,2,3,4,5];
    let mut index = 0;
    
    // 使用 while 迴圈遍歷陣列
    while index < 5 {
      println!("{}", arr[index]);
      
      index = index + 1;
    }


  • for

    let arr = [1,2,3,4,5];
    
    for item in arr.iter() {
      println!("for item {}", item);
    }
    
    // 使用 range 實現指定次數的迴圈
    // 1. (1..5) -> 一個包含 1,2,3,4 的可迭代器
    // 2. 使用 rev 方法進行反轉
    for item in (1..5).rev() {
      println!("range item is {}", item)
    }

7. 所有權

所有權是 Rust 無需 GC 就能保證記憶體安全的核心特性

7.1 記憶體和分配

當變數走出作用域範圍後,Rust會自動呼叫 drop 函式將記憶體空間交還給作業系統

7.2 Stack上的資料複製:copy

  • 對於簡單的標量資料型別,以下程式碼最終會向 stack 中壓入兩個 5

    • 本質上是因為標量型別實現了 Copy trait
    • Copy 這個 trait 用於類似整數這種存放在 stack 上的資料型別,需要分配記憶體的資料型別都不能實現這個 trait
    • 如果實現了 Copy trait,那麼舊變數在賦值後任然可以使用
    • 如果一個型別實現了 Drop trait,那 Rust 就不允許再實現 Copy trait 了
    • 擁有 Copy trait 的資料型別

      • 整數
      • 浮點數
      • 布林
      • 字元
      • 元祖 (需要滿足成員都是擁有 Copy trait 的資料型別)
    let x = 5;
    let y = x;
    
    println!("{}, {}", x, y); // 5, 5

7.3 變數和資料互動的方式:move

  • 對應長度未知的複合資料型別,將一個變數賦給另一個變數後,前者就會失效(在 Rust 中被稱作 move,即 原先 s1 指向的記憶體空間移動到了 s2,完成移動後,s1 便失效了,由此來避免 s1 和 s2 走出作用域時對同一記憶體空間產生兩次釋放操作)

    • 二次釋放(double free)在其他需要手動控制記憶體的語言中是嚴重的bug,可能釋放掉正在被其他程式所使用的記憶體,導致未知的問題
    let s1 = String::from("haha");
    let s2 = s1;
    
    println!("{}", s1); // error: value borrowed here after move

7.4 所有權與函式

  • 其實把值傳遞給函式和變數的情況是類似的,將發生 move 或 copy

    fn main() {
      let string = String::from("hello");
    
      // Move,所有權發生移動,傳入到了函式作用域中
      move_case(string);
    
      /*
       * 在呼叫 move_case 時,string指向的記憶體空間 的所有權發生了 move,
       * 當 move_case 呼叫完畢時,string指向的記憶體空間已經被釋放了,
       * 所以之後再訪問 string,編譯時就會報錯
       */
      // println!("{}", string); // value borrowed here after move
    
      // ---------------------------------------------------------------------
    
      let number = 12;
    
      // Copy,傳入 number 值的副本
      copy_case(number);
    
      /*
       * number 是簡單的標量型別,實現了 Copy trait
       * 在呼叫 copy_case 時,僅僅是傳入了一個副本,
       * 所以後續任然可以繼續使用
       */
      println!("{}", number);
    
      // ---------------------------------------------------------------------
    
      let bar = String::from("bar");
    
      /*
       * 在以下函式的呼叫過程中,bar 指向的記憶體空間的 所有權 被移動到了 函式作用域 中,
       * take_ownership_and_return 這個函式的作用是得到一個記憶體空間的所有權並將其返回,
       * 最終 foo 拿到了該記憶體空間的 所有權,其實這段程式碼的效果與 let foo = bar; 相同
       */
      let foo = take_ownership_and_return(bar);
    
      println!("{}", foo)
    }
    
    fn move_case(string: String) {
      println!("{}", string);
    }
    
    fn copy_case(number: u32) {
      println!("{}", number);
    }
    
    fn take_ownership_and_return(s: String) -> String {
      s
    }

8. Stack & Heap

  • stack 是連續的記憶體空間
  • heap 是雜湊的記憶體空間,指向 heap記憶體 的指標是儲存在 stack 中的
  • 所有儲存在 stack 上的資料必須擁有已知的大小,編譯時大小未知的資料或執行時大小可能發生變化的資料必須儲存在 heap 上

相關文章