Rust 語言精要

本文大量參考《Rust 編程之道》[1] 第二章 - 語言精要。

文章構成

• 環境安裝與工具鏈 • 環境安裝 • 編譯器與包管理器 • 核心庫與標準庫 • 語法和語義介紹 • 語句與表達式 • 變量聲明語義 • 函數與閉包 • 流程控制 • 類型系統 • 基礎類型 • 複合類型 • 標準庫通用集合類型 • 智能指針 • 泛型 •trait• 錯誤處理 • 註釋與打印

環境安裝與工具鏈

Rust 語言使用 rustup[2] 作爲安裝器,它可以安裝、更新和管理 Rust 的所有官方工具鏈。絕大多數情況下建議使用者使用該工具進行環境安裝。

環境安裝

對於*nix系統用戶而言,執行:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

對於Windows系統用戶而言,下載安裝 rustup-init.exe[3]。

安裝完畢後可以通過rustup show獲取工具鏈安裝地址,進一步查看有哪些工具鏈,例如在筆者的 macOS 上是:

❯ rustup show
Default host: x86_64-apple-darwin
rustup home:  /Users/yuchanns/.rustup
stable-x86_64-apple-darwin (default)
rustc 1.49.0 (e1884a8e3 2020-12-29)
❯ ls /Users/yuchanns/.rustup
settings.toml toolchains    update-hashes
❯ ls /Users/yuchanns/.rustup/toolchains
stable-x86_64-apple-darwin
❯ ls /Users/yuchanns/.rustup/toolchains/stable-x86_64-apple-darwin
bin   etc   lib   share
❯ ls /Users/yuchanns/.rustup/toolchains/stable-x86_64-apple-darwin/bin
cargo         cargo-clippy  cargo-fmt     clippy-driver rust-gdb      rust-gdbgui   rust-lldb     rustc         rustdoc       rustfmt

通過rustup doc可以打開本地的 Rust 文檔,而不用網絡。

編譯器與包管理器

rustc官方編譯器,負責將源代碼編譯爲可執行文件或庫文件。經過分詞和解析生成 AST,然後處理爲 HIR(進行類型檢查),接着編譯爲 MIR(實現增量編譯),最終翻譯爲 LLVM IR,交由 LLVM 作爲後端編譯爲各個平臺的目標機器碼,因此 Rust 是跨平臺的,並且支持交叉編譯。

rustc可以用run命令和build命令編譯運行源碼,但大多數情況下用戶不直接使用rustc對源碼執行操作,而是使用cargo這一工具間接調用rustc

cargo官方包管理器,可以方便地管理包依賴的問題。

使用cargo new proj_name可以創建一個新的項目,包含一個Cargo.toml依賴管理文件和src源碼文件夾。

❯ cargo new proj_name
     Created binary (application) `proj_name` package
❯ tree proj_name
proj_name
├── Cargo.toml
└── src
    └── main.rs
1 directory, 2 files

執行cargo run .可以簡單編譯運行默認的代碼,編譯結果將會與src同級的target下,包含target/debugtarget/release兩個文件夾。

cd proj_name
❯ cargo run .
   Compiling proj_name v0.1.0 (/Users/yuchanns/Coding/backend/github/rustbyexample/trpl/proj_name)
    Finished dev [unoptimized + debuginfo] target(s) in 1.02s
     Running `target/debug/proj_name .`
Hello, world!

同時我們注意到文件根目錄下生成了一個Cargo.lock文件,記錄詳細的依賴版本信息。然後觀察Cargo.toml

❯ cat Cargo.toml
[package]
name = "proj_name"
version = "0.1.0"
authors = ["yuchanns <airamusume@gmail.com>"]
edition = "2018"
[dependencies]
rand = "0.8.1"

可以看到,[package]記錄的是關於本項目的一些信息,而下方的[dependencies]則記錄了對外部包的依賴。

添加依賴,是通過編輯該文件,手動寫入包名和版本,然後在編譯過程中 cargo 就會自動下載依賴並使用。

也許有的讀者好奇是否還有類似於其他語言的 CLI 命令,通過cargo add等命令添加依賴的方式,遺憾的是官方並沒有提供這樣的支持。而社區則提供了一個 killercup/cargo-edit[4] 實現了這一需求:

cargo install cargo-edit
cargo add rand
cargo rm rand

在一個 issue Subcommand to add a new dependency to Cargo.toml #2179[5] 中官方推薦了該工具,可能很多人 (包括筆者在內) 都如同下面這位老哥一樣很難接受官方因爲社區有解決方案而不提供官方解決的決定。不過也許可以理解爲這就是官方宣稱的 “重視社區” 的身體力行吧。

和許多其他語言一樣,身在中國境內,用戶還需要設置 Cargo 的鏡像站點,改善下載狀況:

❯ cat ~/.cargo/config
[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
replace-with = 'ustc'
[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index"

核心庫與標準庫

Rust 語言分爲核心庫和標準庫。

核心庫是語言核心,不依賴於操作系統和網絡,不提供併發和 I/O,全部是棧分配:

• 基礎 trait• 基礎類型 • 內鍵宏

標準庫提供開發所需要的基礎和跨平臺支持:

• 基礎 trait 和數據類型 • 併發、I/O 和運行時 • 平臺抽象 • 底層操作接口 • 錯誤處理類型和迭代器

語法和語義介紹

語句與表達式

Rust 語法分爲 語句 (Statement) 和 表達式 (Expression) 。

語句用於聲明數據結構和引入包、模塊等:

• 通過externuse引入外部代碼:use std::prelude::v1::*;• 通過let聲明變量,通過fn聲明函數:let greet = "world";• 宏語句,語句名以!爲結尾,可像函數一樣被調用:println!("hello {}", greeter);

表達式進行求值:

• 表達式結尾沒有;則返回求值結果,有;則返回單元值()• 由{}和一系列表達式組成的表達式爲 塊表達式 (Block Expression) ,總是返回最後一個表達式的求值結果,如果有;則返回單元值

因此塊表達式常常可以這樣使用:

fn main() {
    let a = {
        let a = 1;
        let b = 2;
        a + b // 注意這裏沒有;會直接返回求值結果
    };
    println!("a: {}", a);
}

變量聲明語義

表達式內部又可分爲 位置表達式 (Place Expression) 和 值表達式 (Vaue Expression) 。

位置表達式表示內存位置,可以對數據單元的內存進行讀寫,代表持久性數據;值表達式引用數據值,只能讀,代表臨時數據。

fn main () {
    let a = "hello world"
}

如上,a是位置表達式,持久性地將值寫入到內存中;而"hello world"則是值表達式,是一個臨時數據,不可寫,只可被讀。

有其他語言背景的讀者可能就會覺得,這只是左值和右值的另一種稱呼,實際上並不是,這兩個概念是爲了下面會提到的內存管理所服務的。

表達式的求值過程具有求值上下文,分爲位置上下文和值上下文:

• 賦值表達式的左側,稱爲位置上下文 •match判別式也是位置上下文 • 賦值表達式的右側使用ref模式時也是位置上下文 • 其他情況都是值上下文

Rust 使用let聲明變量時默認不可對位置表達式重新賦值,需要在聲明時通過mut關鍵字聲明可變的位置表達式:

fn main () {
    let mut a = "hello";
    a = "world";
}

通過let可以重複對同一個變量名進行不同數據類型的賦值,這樣的操作會 “遮蔽” 前一個同名變量,可以認爲是“只對變量名字進行復用”(那個變量的實際上還在內存當中):

fn main() {
    let a = String::from("hello world");
    let b = &a;
    let a = String::from("hello yuchanns");
    println!("a is {}, b is {}", a, *b);
}

當位置表達式出現在值上下文中,會出現內存地址的轉移,同時 轉移 (Move) 對內存的 所有權 (Ownership) ,其結果是將無法再通過這個位置表達式讀寫該內存地址。

fn main () {
    let a = String::from("hello world");
    // 下面的表達式中位置表達式出現在值上下文(即賦值表達式的右側)
    // 將一個位置表達式賦值給另一個位置表達式,出現了所有權的轉移
    let b = a;
    println!("b is {}", b);
    // println!("a is {}", a); // 這裏會編譯失敗,提示:a value used here after move.
}

細心的讀者這時候會注意到上面的代碼清單中聲明字符串使用了另一種方式,這和 Rust 的內存分配有關,本文不展開討論,暫時不必深究。

Rust 沒有 GC,就是 依靠所有權實現對內存的管理 。

與 轉移 (Move) 語義相對的,還有 複製 (Copy) 語義,不轉移而對內存進行復制。

同時 Rust 也提供了 借用 (Borrow) 操作符 (&),在不轉移的情況下獲取內存位置,並通過 解引用 (Deref) 操作符 (*) 取值。

變量在塊表達式的詞法作用域範圍時結束生命週期。可以在詞法作用域內主動使用{}開闢一段新的詞法作用域。

函數與閉包

Rust 使用fn聲明函數定義,並通過在入參後面加: type的方式約定入參類型,通過在函數括號後面加-> type的方式約定函數返回類型:

fn fizz_buzz(num: i32) -> String {
    // ...
}

函數在 Rust 中是一等公民,可以作爲參數和返回值使用。

有其他語言背景的讀者也許會覺得,當函數作爲返回值使用時,它就是閉包。但在 Rust 中還是有所不同的:

• 閉包實際上是一個匿名結構體和 trait 的組合實現 • 函數無法引用外部變量 • 閉包使用||代替函數的()• 閉包需要使用move關鍵字顯式轉移變量所有權避免成爲懸垂指針 (即使你忘了,編譯器也會幫你檢查出來)

fn main() {
    // 返回值裏的impl表明閉包實際上是用匿名結構體實現了一個trait
    fn make_true2() -> impl Fn() -> bool {
        let s = "hello world2";
        // 函數作爲返回值
        fn is_true() -> bool {
            //函數內部無法引用外部變量
            // println!("s: {}", s); // can't capture dynamic environment in a fn item
            true
        }
        fn make_true() -> fn() -> bool {
            is_true
        }
        println!("make_true: {}", make_true()());
        // 閉包作爲返回值
        // 使用||代替函數的()
        move || -> bool {
            // 閉包可以引用外部變量
            // 但需要通過move顯式轉移所有權,代替默認的引用
            println!("s: {}", s);
            true
        }
    }
    println!("make_true2: {}", make_true2()());
}

流程控制

Rust 中沒有三元操作符,if表達式的分支必須返回同一個類型的值。每一個if分支其實也是一個塊表達式。

循環表達式有三種:whileloopfor...in

for...in本質上是一個迭代器 • 無限循環請使用loop而不是while true,因爲編譯器會忽略循環體裏的表達式,引起報錯

fn main () {
    let n = 13;
    // if分支是塊表達式,返回類型必須相同
    let result = if (n > 10) {
        true
    } else {
        false
    };
    println!("result: {}", result);
    for n in 1..10 {
        println!("now n is {}", n);
    }
    fn while_true() -> i32 {
        while true {
            return 10; // 編譯器忽略內部會返回i32,因爲認爲while條件有真有假,不會一直爲true
        }
        return 11; // 如果省略這一行,編譯器會認爲函數最終返回了一個單元值()
    }
    println!("while_true: {}", while_true());
}

Rust 還提供了match表達式和某些場景下可以代替它進行簡化的if letwhile let表達式:

match表達式返回類型必須一致 •match表達式左側可以通過操作符@將匹配值賦予某個變量 •match表達式必須窮盡所有可能,可以用通配符_處理剩餘情況

fn main() {
    let number = 42;
    match number {
        0 => println!("zero"),
        n @ 42 => println!("value is {}", n),
        _ => println!("rest of all"),
    }
    let mut v = vec![1, 2, 3, 4];
    while let Some(x) = v.pop() {
        println!("{}", x);
    }
}

類型系統

基礎類型

• 布爾: let x = true; let y: bool = false;,任意一個比較操作都會產生 bool 類型 • 數字:• 可以使用類型後綴 (例如let a = 42u32;)• 可以使用_提升可讀性 (例如let a = 100_000;)• 可以使用前綴表示進制 (十六進制0x2A、八進制0o106、二進制0b1101_1011)• 可以使用字節字面量 (例如b'*'等價於42u8)• 可以表示無窮大 (INFINITY),負無窮大 (NEG_INFINITY),非數字值 (NAN),最小有限值 (MIN) 和最大有限值 (MAX)

jRBThX

• 字符:• 使用''來表示字符類型,代表一個 Unicode 標量值 • 每個字符佔 4 個字節 • 可使用 ASCII 和 Unicode 碼定義 ('\x2A'表示*'\u{151}'表示ő)• 數組:• 簽名爲[T; N](let arr: [i32; 3] = [1, 2, 3])• 類型必須一致 • 編譯時必須確定長度,不可變化長短 • 會在編譯器檢查越界訪問 • 必須聲明mut才能修改值 • 範圍:• 本質是迭代器 • 左閉右開區間(1..5)• 全閉區間(1..=5)• 切片:• 引用數組的一部分,無需拷貝 • 包含指向數組其實位置的指針和數組長度 • 通過&產生 (let arr = [1, 2, 3, 4];let b = &arr[1..3];)• 可以通過聲明mut修改值 • 通過lenis_empty判斷長度和是否爲空 (b.len(); b.is_empty();)• 字符串:• 基礎類型字符串爲固定長度字符串 • 類型寫作&str• 可以通過as_ptrlen獲取指針和長度 • 原生指針:提供不可變原生指針 (*const T) 和可變原生指針 (*mut T),不安全,需要在unsafe塊中執行,一般不直接使用 •never:• 表示永遠不可能有返回值 • 用!表示

複合數據類型

Rust 提供 4 種複合數據類型:

• 元組 Tuple:let tuple: (i32, char) = (5, 'c');• 元素可以類型不同 • 長度固定 • 可以使用let解構 (let (x, y) = tuple;)• 只有一個值時需要加,(let tuple = (0,))• 單元值是空元組 • 結構體:• 使用struct聲明定義 • 元組結構體:• 字段沒有名稱,只有類型 •struct Color(i32, i32, i32);• 單元結構體:• 沒有任何字段的結構體 • 多個實例在 Relase 編譯模式下會被優化編譯成同一個對象 •struct Empty;• 具名結構體:• 命名建議使用駝峯 • 可使用impl關鍵字爲結構體添加方法和類似構造函數 • 方法中第一個參數如果是&self,通過.調用 • 否則方法使用::調用

struct People {
    name: &'static str,
    gender: u32,
}
impl People {
    fn new(name: &'static str, gender: u32) -> Self {
        return People{name: name, gender: gender};
    }
    // 自身需要mut
    fn set_name(&mut self, name: &'static str) {
        self.name = name;
    }
    fn name(&self) {
        println!("name: {:?}", self.name);
    }
    fn gender(&self) {
        let gender = if self.gender == 1 {"boy"} else {"girl"};
        println!("name: {:?}", gender);
    }
}
fn main() {
    // 需要mut才能調用set_name
    let mut p = People::new("yuchanns", 1);
    p.name();
    p.set_name("yuchanns2");
    p.name();
    p.gender();
}

• 枚舉體:• 使用enum聲明定義 • 成員是值,不是類型 • 也支持類 C 枚舉體 • 還支持攜帶類型參數

// 成員是值
enum Number {
    Zero,
    One,
    Two,
}
// 類C枚舉
enum Color {
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff,
}
// 攜帶類型參數
enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}
fn main() {
    // 調用
    let a = Number::One;
    match a {
        Number::Zero => println!("0"),
        Number::One => println!("1"),
        Number::Two => println!("2"),
    }
}

標準庫通用集合類型

Rust 標準庫提供了 4 種通用集合類型:

• 線性序列:VecVecDequeLinkedList• 映射表:無序的 HashMap、有序的 BTreeMap• 集合:無序的 HashSet、有序的 BTreeSet• 優先隊列:二叉堆 BinaryHeap

智能指針

可以自動釋放內存,無痛使用堆內存,確保內存安全。

Box<T>爲例:

• 值被默認分配到棧內存,可以通過Box::new(value)分配到堆上 • 返回一個指向類型 T 的堆內存分配值的智能指針。• 可以通過*解引用取值 • 超出作用域範圍時自動析構,銷燬內部對象,釋放內存。

泛型

和其他語言的泛型類似,解決代碼複用。

通常使用<T>來表示。

可以結合 trait 指定泛型行爲。

trait

•trait 是 Rust 唯一的接口抽象方式 • 可以靜態分發,也可以動態分發 • 可以作爲標籤標記類型擁有某些特定性爲 • 組合優於繼承,面向接口編程

struct Plane;
struct Car;
trait Behave {
    fn behave(&self);
}
impl Behave for Plane {
    fn behave(&self) {
        println!("plane move by fly");
    }
}
impl Behave for Car {
    fn behave(&self) {
        println!("car move by wheels");
    }
}
// 泛型結合trait限定行爲
fn behave_static<T: Behave>(s: T) {
    s.behave();
}
fn behave_dyn(s: &dyn Behave) {
    s.behave();
}
fn main() {
    let plane = Plane;
    // 靜態分發,編譯時展開,無運行時開銷
    behave_static::<Plane>(plane);
    // 動態分發,有運行時開銷
    behave_dyn(&Car);
}

錯誤處理

Rust 的錯誤處理通過返回Result<T, E>的方式進行,這是一個枚舉體。

enum Result<T, E> {
    Ok(T),
    Err(E),
}

結合match進行處理,下面這個猜數字遊戲 [6] 是一個簡單的示例:

use rand::Rng;
use std::cmp::Ordering;
use std::io::stdin;
fn main() {
    println!("Guess the number!");
    let secret_number = rand::thread_rng().gen_range(1..101);
    loop {
        println!("Please input your guess:");
        let mut guess = String::new();
        stdin().read_line(&mut guess).expect("Failed to read line");
        // Result是個枚舉
        // 可以通過match Result進行成功或失敗的處理
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                // 失敗的時候跳過當次循環
                println!("Please type a number!");
                continue;
            }
        };
        println!("Your guessed: {}", guess);
        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

註釋與打印

Rust 註釋分爲普通註釋和文檔註釋:

• 普通註釋:• 使用/*...*/進行塊註釋 • 使用//進行行註釋 • 文檔註釋:• 使用/////!註釋 • 支持 Markdown 語法 • 可以通過rustdoc構建生成 HTML 文檔

使用println!進行格式化打印:

• 只有{}表示 trait Display,需要實現該 trait 才能打印:println!("{}", 2);{:?}表示 trait Debug,需要實現該 trait 才能打印:println!("{:?}", 2);{:o}表示八進制 •{:x}表示十六進制小寫 •{:X}表示十六進制大寫 •{:p}表示指針 •{:b}表示二進制 •{:e}表示指數小寫 •{:E}表示指數大寫

引用鏈接

[1] 《Rust 編程之道》: https://book.douban.com/subject/30418895/
[2] rustup: https://rustup.rs/
[3] rustup-init.exe: https://win.rustup.rs/x86_64
[4] killercup/cargo-edit: https://github.com/killercup/cargo-edit
[5] Subcommand to add a new dependency to Cargo.toml #2179: https://github.com/rust-lang/cargo/issues/2179
[6] 猜數字遊戲: https://rust-lang.budshome.com/ch02-00-guessing-game-tutorial.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/twg1IgwIZ3AcFuh52DWT0A