Rust學習筆記之基礎概念快速入門

去年就學習過一段時間的Rust,除了略微“詭異”的所有權規則,整個語言的工具鏈體驗還是很好的,起碼Cargo真的很舒服。

聊聊Rust

Rust is technology from the past came to save the future from itself.
——Graydon Hoare

Rust 和 C++ 是同一系列的,經常被拿來一同比較的 Go 其實對標不是 C++,而是 Python 與 Java。我對 Rust 的看法就是沒有歷史包袱,現代化的 C++,由於 go 設計有 gc (垃圾回收),因此 go 可以說是一個更接地氣的 Python 或者 Java。

Rust 的歷史可以追溯到 2006 年,由 Graydon Hoare 開發,目前主要由 Mozilla 主導。第一個穩定版本 1.0 是在2015年發佈的。據說,1.0 之前的那些版本,各種特性很容易在下一個版本就得不到兼容,1.0 之後才改變了這種輕易翻盤的情況,各種特性開始開始穩定下來。

Mozilla 主導 Rust 主要寄希望能解決 C++ 的內存安全問題,因爲火狐瀏覽器的引擎 gecko 就是用 C++ 編寫的。C++ 年代久遠,對併發的支持也沒有提供更好抽象,非常容易誤用,這也是 Rust 試圖解決的問題之一。

Rust 借鑑了很多語言,比如Cyclone(一種安全的C語言方言)的基於區域的內存管理模型;C++ 的RAII 原理;Haskell 的類型系統、錯誤處理類型、typeclasses等等。Rust 有非常小的 runtime,不需要垃圾回收,默認情況下是在棧上進行分類內存,而不是堆上。Rust 的編譯器 rustc,一開始使用 Ocaml (一種函數式語言)編寫,後來在2011年 Rust 實現了自舉。

安裝、更新和卸載

在Mac和Linux上安裝相對而言會簡單很多,只需要在終端輸入以下命令:

curl https://sh.rustup.rs -sSf | sh

會自動配置好環境變量,重啓一下終端,或者輸入:

source $HOME/.cargo/env

更新的命令:

rustup update

卸載

rustup self uninstall

檢驗是否成功安裝

rustc --version

Hello World

爲了體現出 Rust 的特性,寫一個稍微富含 Rust 風格的 Hello World。

// great.rs
use std::env;

fn main() {
  let name = env::args().skip(1).next();
  match name {
    Some(n) => println!("Hello, {}", n),
    None => panic!("Did not receive any name?"),
  }
}

現在使用 rustc 編譯項目,並運行一下:

➜ rust_projects rustc greet.rs
➜ rust_projects ./greet Guyu2019
Hello, Guyu2019
➜ rust_projects ./greet         
thread 'main' panicked at 'Did not receive any name?', greet.rs:8:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

第二行,從 std Crate(庫被稱爲 Crate) 中引入了 env 模塊。第 4 行就是大家熟悉的 main 程序入口。

第一個命令行參數(下標爲0),是程序本身的名字。第2個參數(下標爲1)纔是第一個輸入參數。skip(1) 的意思就是跳過一個元素,它返回一個迭代器。由於迭代器是懶加載的邏輯,沒有進行預計算,所以需要顯式使用 next() 去獲取元素。

next() 返回的也不是單純的字符串,而是一個 Option 類型。Option 類型避免使用 null,用 None 來表示沒有值。

第六行的 match 類似我們熟悉的 if ,也比較像 switch。如果返回 next() 返回了值,則調用 println!()。如果沒有值,則到 Nonepanic!。最後的 ! 表明這是一個宏(macro),而不是我們通常理解的函數。

println! 宏接受一個字符串,還可以帶有佔位的 {} 語法。我們通常稱這種字符串爲格式化字符串(fotmat strings)。如果只是替換成原始的內置簡單類型,可以使用 {},其它的類型通常會使用 {:?}

{} 的行爲主要來自 Display trait,{:?} 則來自於 Debug trait。顧名思義,{} 最主要的功能是顯示,{:?} 則多了調試的含義。對於內置的簡單類型,Debug 顯得就不那麼重要了。如果是自己定義的類型,或者比較複雜的類型,可以使用 #[derive(Debug)] 去定義調試的行爲,當然這是後話。

內置的原始類型

Rust 內建的原始類型有這些:

  • bool:布爾值,truefalse
  • char:單個字符
  • 整數類型:有一點特殊,Rust 可以支持 128 bit 的整數
長度 有符號 無符號
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize

其中,isizeusize依賴運行程序的計算機架構:64位的機器上是64位(等價與i64),32位上是32位(等價於i32),一般可以用來表示指針。如果拿不定主意,默認的i32就很好,通常是最快的。

  • f32:基於IEEE 754標準的32位的浮點數類型;
  • f64:64位浮點數類型;
  • [T; N]:固定長度的數組;
  • [T]:可變尺寸的 T 類型數組(類似 C++ 的vector);
  • str:字符串切片,主要用於引用,比如&str
  • (T, U, ..):無窮序列,TU 可以是不同的類型;
  • f(i32) -> i32:函數類型。函數在 Rust 中也是一等公民,函數可以像變量一樣使用。這裏僅僅只是舉例,f(i32) -> i32 表示這個函數接受一個 i32 類型的值,然後函數返回 i32,函數類型根據函數參數和返回值發生變化。

定義變量

Rust中使用 let 進行定義變量。和C或者C++不同,變量默認是不可變的。這個主要是爲了併發和所有權上進行考慮的。你可以理解成 let 就是 C++ 裏的 const。如果要聲明可變的變量,應該在變量名前面加上mut

// variables.rs

fn main() {
  let target = "world";
  let mut greeting = "Hello";
  println!("{}, {}", greeting, target);
  greeting = "How are you doing";
  target = "mate";
  println!("{}, {}", greeting, target);
}

編譯運行這段代碼,會發現報錯了。編譯器告訴你,target 不能改,要改請加上 mut。嗯,非常友好。
“變量”不變

函數與閉包

函數類型沒什麼大的意外。不過這裏提一句,編寫函數儘量編寫純函數,有的語言管純函數叫函數,其它的叫方法。**純函數的意思簡單來講就是不修改全局變量,函數內部需要的外部資源從參數中獲取,然後不要直接修改外部資源,而是返回給外部。**這樣的函數有明顯的高內聚,低耦合的特點,比較容易進行調試和並行。

// functions.rs

fn add(a: u64, b: u64) -> u64 {
  a + b
}

fn main() {
  let a: u64 = 17;
  let b = 3;

  let result = add(a, b);
  println!("Result {}", result);
}

// Result 20

沒有什麼意外,和 C++ 有點差別的是,最後會自動返回最後 a + b 的結果,不需要加上 return。而且,Rust 的函數借鑑於函數式編程,就算沒有返回值,也會默認返回 () 類型,可以把它當作 void

// function_mut.rs

fn increate_by(mut val: u32, how_much: u32) {
  val += how_much;
  println!("You made {} points", val);
}

fn main() {
  let score = 2048;
  increate_by(score, 30);

  println!("{}", score);
}
// You made 2078 points
// 2048

閉包相比與函數擁有環境的信息,它可以沒有名字,所以有時候也被叫做匿名函數。最簡單的閉包形式是這樣的:

let my_closure = || ();

這樣就定義了一個沒有參數,然後什麼也沒幹的函數。可以使用 my_closure() 去調用,這點上和函數沒什麼差別。

// closures.rs

fn main() {
  let doubler = |x| x * 2;
  let value = 5;
  let twice = doubler(value);
  println!("{} doubled is {}", value, twice);

  let big_closure = |b, c| {
    let z = b + c;
    z * twice
  };

  let some_number = big_closure(1, 2);
  println!("Rusult from closure: {}", some_number);

}
// 5 doubled is 10
// Rusult from closure: 30

閉包有很多用法,最典型就是作爲高階函數的參數。高階函數就是以另一個函數或閉包作爲參數的函數。比如,標準庫的 spawn 接受一個閉包,這個閉包要在另外一個線程裏運行;還有比如作爲 filter 的篩選函數。

字符串

字符串非常常用,通常在 Rust 中用 &str 或者 String

// strings.rs

fn main() {
  let question = "How are you?"; // a &str type
  let person: String = "Bob".to_string();

  let namaste = String::from("🙏");

  println!("{}: {} {}", namaste, question, person);
}
// 🙏:How are you? Bob

條件分支與match表達式

if {} else {} 結構和類C語言一樣。不過,在 Rust 中,if 是表達式而不是語句。一般而言,表達式會返回值,而語句不會。因此,if 可能會返回空的 () 單元值,也可能會返回實際的值。

// if_assign.rs

fn main() {
  let result = if 1 == 2 {
    "Wait, what ?"
  } else {
    "Rust makes sense"
  };

  println!("You know what ? {}.", result);
}
// You know what ? Rust makes sense.

如果把 else 分支去掉,則編譯器會報錯:
類型不一致
沒有 else 分支,如果 if 條件爲 false,那麼就會返回 ();但是如果 iftrue,就返回 &str。Rust 不允許一個變量存儲多個類型。所以,ifelse 都要返回相同的類型。

如果在字符串後面加上分號 ;,則字符串會被當作語句,然後返回 ()。注意要把 {}換成{:?}

// if_else_no_value.rs

fn main() {
  let result = if 1 == 2 {
    "Wait, what ?";
  } else {
    "Rust makes sense";
  };

  println!("You know what ? {:?}.", result);
}
// You know what ? ().

match 很類似 C 語言中的 switch 語句,也不難理解。

// match_expression.rs

fn req_status() -> u32 {
  404
}

fn main() {
  let status = req_status();
  match status {
    404 => println!("Not Found"),
    200 => println!("Success"),
    other => {
      println!("Request failed with code: {}", other);
    }
  }
}
// Not Found

循環

Rust 有三種結構的循環:loopwhilefor,同樣也使用 continuebreak 去控制循環流程。
loop 等價於 C++ 的 while (true) 無限循環:

// loops.rs

fn main() {
  let mut x = 1024;
  loop {
    if x < 0 {
      break;
    }
    println!("{} more runs to go", x);
    x -= 1;
  }
}
// ...
// 2 more runs to go
// 1 more runs to go
// 0 more runs to go

在嵌套循環的場景下,可以用標籤指定要打破到哪一層次:

// loop_labels.rs

fn silly_sub(a: i32, b: i32) -> i32 {
  let mut result = 0;
  'increment: loop {
    if result == a {
      let mut dec = b;
      'decrement: loop {
        if dec == 0 {
          break 'increment;
        } else {
          result -= 1;
          dec -= 1;
        }
      }
    } else {
      result += 1;
    }
  }
  result
}

fn main() {
  let a = 10;
  let b = 4;
  let result = silly_sub(a, b);
  println!("{} minus {} is {}", a, b, result);
}
// 10 minus 4 is 6

while 循環就沒什麼意外了:

// while.rs

fn main() {
  let mut x = 1000;
  while x > 0 {
    println!("{} more runs to go", x);
    x -= 1;
  }
}
// ...
// 3 more runs to go
// 2 more runs to go
// 1 more runs to go

Rust 也提供了 for 循環,是一個語法糖,它涉及一個迭代器的概念,很類似其它語言裏的 for-loop 循環。


// for_loops.rs

fn main() {
  print!("Normal ranges: ");
  for i in 0..10 {
    print!("{},", i);
  }

  println!(); // a new line
  print!("Inclusive ranges: ");
  for i in 0..=10 {
    print!("{}, ", i);
  }
}
// Normal ranges: 0,1,2,3,4,5,6,7,8,9,
// Inclusive ranges: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

注意一下,0..10 是不包括10的,而0..=10 則包括了10。

用戶自定義類型

struct

最簡單的用法,單元struct(unit struct):

// unit_struct.rs

struct Dummy;

fn main() {
  let value = Dummy;
}
// Red value: 255
// Green value: 255
// Blue value: 255
//
// R: 255, G: 165, B:0(orange)

在這裏,Dummy 是一個0大小的值,運行的時候不佔內存。

另外一種 struct,叫元組struct(tuple struct),它會綁定值。比如可以定義一個 struct 來表示 RGB:

// tuple_struct.rs

struct Color(u8, u8, u8);

fn main() {
  let white = Color(255, 255, 255);
  // You can pull them out by index
  let red = white.0;
  let green = white.1;
  let blue = white.2;
  
  println!("Red value: {}", red);
  println!("Green value: {}", green);
  println!("Blue value: {}\n", blue);

  let orange = Color(255, 165, 0);
  // You can also destructure the fields directly
  let Color(r, g, b) = orange;
  println!("R: {}, G: {}, B:{}(orange)", r, g, b);

  // Can also ignore fields while destructuring
  let Color(r, _, b) = orange;
}

Rust 也支持 C 語言那種結構體:

// structs.rs

struct Player {
  name: String,
  iq: u8,
  friends: u8,
  score: u16
}

fn bump_player_score(mut player: Player, score: u16) {
  player.score += score;
  println!("Updated player stats:");
  println!("Name: {}", player.name);
  println!("IQ: {}", player.iq);
  println!("friends: {}", player.friends);
  println!("Score: {}", player.score);
}

fn main() {
  let name = "Alice".to_string();
  let player = Player {
    name,
    iq: 171,
    friends: 134,
    score: 1129
  };

  bump_player_score(player, 120);
}
// Updated player stats:
// Name: Alice
// IQ: 171
// friends: 134
// Score: 1249

enum

// enums.rs

#[derive(Debug)]
enum Direction {
  N,
  E,
  S,
  W
}

enum PlayerAction {
  Move {
    direction: Direction,
    speed: u8
  },
  Wait,
  Attack(Direction)
}

fn main() {
  let simulated_player_action = PlayerAction::Move {
    direction: Direction::N,
    speed: 2,
  };

  match simulated_player_action {
    PlayerAction::Wait => println!("Player wants to wait"),
    PlayerAction::Move { direction, speed } => {
      println!("Player wants to move in direction {:?} with speed {}", direction, speed)
    },
    PlayerAction::Attack(direction) => {
      println!("Player wants to attack direction {:?}", direction)
    }
  }
}

#[derive(Debug)] 這個是必要的,這樣纔會允許使用 {:?} 打印實例。前面已經提到過了,{:?} 對應 Debug 的 trait。
如果沒有加 #[derive(Debug)],則會報錯。
Debug

impl

模塊、import、use

Rust 用 module 來組織代碼。module 是一個複雜的主題,這裏先簡要記錄一下:

  • 每一個 Rust 程序都需要一個根模塊;一般就是 main.rs,如果是庫的話,一般是 lib.rs。
  • 模塊可以其它模塊中聲明,也可以組織爲文件和目錄;
  • 爲了讓編譯器知道我們的自己模塊,就要用 mod 定義它;
  • 要使用模塊內的任何內容,都需要使用 use,以及模塊名稱;
  • 模塊中的定義默認是私有的,需要使用 pub 關鍵字公開給使用者;

集合

Arrays

// arrays.rs

fn main() {
  let number: [u8; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
  let floats = [0.1f64, 0.2, 0.3];

  println!("Number: {}", number[4]);
  println!("Float: {}", floats[2]);
}
// Number: 5
// Float: 0.3

0.1f64 通過後綴指定了數組的類型,這也是一種方式。

Tuples

元組(Tuples)和數組的不同點在於它的元素類型可以是不一樣的。它也經常被函數用來返回多個值。

// tuples.rs

fn main() {
  let num_and_str: (u8, &str) = (40, "Have a good day!");
  println!("{:?}", num_and_str);
  let (num, string) = num_and_str;
  println!("From tuple: Number: {}, String: {}", num, string);
}
// (40, "Have a good day!")
// From tuple: Number: 40, String: Have a good day!

Vectors

向量很像數組,不過它的長度不需要預先知道,可以按需增長。可以通過 Vec::new 構造函數或者 vec![] 宏來創建向量。

// vec.rs

fn main() {
  let mut numbers_vec: Vec<u8> = Vec::new();
  numbers_vec.push(1);
  numbers_vec.push(2);

  let mut vec_with_macro = vec![1];
  vec_with_macro.push(2);
  // value ignored with '_'
  let _ = vec_with_macro.pop();
  let message = if numbers_vec == vec_with_macro {
    "They are equal"
  } else {
    "Nah! They look different to me"
  };

  println!("{} {:?}{:?}", message, numbers_vec, vec_with_macro);
}
// Nah! They look different to me [1, 2][1]

Hashmaps

Rust 也提供了映射,可以用來存儲鍵-值數據。Hashmapstd::collections 模塊中被引入,可以用 HashMap::new 構造函數來創建。

// hashmaps.rs

use std::collections::HashMap;

fn main() {
  let mut fruits = HashMap::new();
  fruits.insert("apple", 3);
  fruits.insert("mango", 6);
  fruits.insert("orange", 2);
  fruits.insert("avocado", 7);

  for (k, v) in &fruits {
    println!("I got {} {}", v, k);
  }

  fruits.remove("orange");
  let old_avocado = fruits["avocado"];
  fruits.insert("avocado", old_avocado + 5);
  println!("\nI now have {} avocados", fruits["avocado"]);
}
// I got 6 mango
// I got 7 avocado
// I got 2 orange
// I got 3 apple
//
// I now have 12 avocados

在上面的例子中,先創建了一個新的 Hashmap,然後插入元素。之後在用類似 for-loop的語法進行迭代器迭代。由於這裏只是讀取 fruits,不作更改,因此使用 &fruits。Hash 算法對 HashMap 類型的鍵進行散列處理,基於的是 Robin 開放尋址方案,但是可以自己根據用例進行自定義。

Slices

切片(slice)是一種從集合中獲取視圖的一種通用方式,使用 &[T]表示。。大多數情況下用於讀取操作。切片基本指向一個連續範圍的指針或者引用。切片是指向堆棧或堆中某個地方的數據的胖(fat)指針。通過胖指針,可以獲取指向了多少個數據,以及指向數據的指針。

// slices.rs

fn main() {
  let mut numbers: [u8; 4] = [1, 2, 3, 4];
  {
    let all: &[u8] = &numbers[..];
    println!("All of them: {:?}", all);
  }
  {
    let first_two: &mut [u8] = &mut numbers[0..2];
    first_two[0] = 100;
    first_two[1] = 99;
  }

  println!("Look ma! I can modify through slices: {:?}", numbers);
}
// All of them: [1, 2, 3, 4]
// Look ma! I can modify through slices: [100, 99, 3, 4]

迭代器

在 Rust 中,迭代器可以是任意類型的,只要實現了 Iterator Trait。大多數類型都可以通過調用 iter() 或者 into_iter() 轉換成迭代器。

總結

上面這些已經覆蓋了基礎的概念,有很多概念還需要更深入地瞭解,比如迭代器、impl、標準庫之類的。這些是後話啦~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章