首頁>技術>

​摘要:從開發環境、語法、屬性、記憶體管理和Unicode等五部分,為你帶來一份詳細的Rust語言學習的精華總結內容。

一、Rust開發環境指南

1.1 Rust程式碼執行

根據編譯原理知識,編譯器不是直接將源語言翻譯為目標語言,而是翻譯為一種“中間語言”,編譯器從業人員稱之為“IR”--指令集,之後再由中間語言,利用後端程式和裝置翻譯為目標平臺的組合語言。

Rust程式碼執行:

1)Rust程式碼經過分詞和解析,生成AST(抽象語法樹)。

2)然後把AST進一步簡化處理為HIR(High-levelIR),目的是讓編譯器更方便的做型別檢查

3)HIR會進一步被編譯為MIR(MiddleIR),這是一種中間表示,主要目的是:

a)縮短編譯時間;

b)縮短執行時間;

c)更精確的型別檢查。

4)最終MIR會被翻譯為LLVM IR,然後被LLVM的處理編譯為能在各個平臺上執行的目標機器碼。

Ø IR:中間語言

Ø HIR:高階中間語言

Ø MIR:中級中間語言

Ø LLVM :LowLevel Virtual Machine,底層虛擬機器

LLVM是構架編譯器(compiler)的框架系統,以C++編寫而成,用於最佳化以任意程式語言編寫的程式的編譯時間(compile-time)、連結時間(link-time)、執行時間(run-time)以及空閒時間(idle-time)

無疑,不同編譯器的中間語言IR是不一樣的,而IR可以說是集中體現了這款編譯器的特徵:他的演算法,最佳化方式,彙編流程等等,想要完全掌握某種編譯器的工作和執行原理,分析和學習這款編譯器的中間語言無疑是重要手段。

由於中間語言相當於一款編譯器前端和後端的“橋樑”,如果我們想進行基於LLVM的後端移植,無疑需要開發出對應目標平臺的編譯器後端,想要順利完成這一工作,透徹瞭解LLVM的中間語言無疑是非常必要的工作。

LLVM相對於gcc的一大改進就是大大提高了中間語言的生成效率和可讀性, LLVM的中間語言是一種介於c語言和組合語言的格式,他既有高階語言的可讀性,又能比較全面地反映計算機底層資料的運算和傳輸的情況,精煉而又高效。

1.1.1 MIR

MIR是基於控制流圖(Control Flow Graph,CFG)的抽象資料結構,它用有向圖(DAG)形式包含了程式執行過程中所有可能的流程。所以將基於MIR的借用檢查稱為非詞法作用域的生命週期。

MIR由一下關鍵部分組成:

l 基本塊(Basic block,bb),他是控制流圖的基本單位,

Ø 語句(statement)

Ø終止句(Terminator)

l 本地變數,佔中記憶體的位置,比如函式引數、區域性變數等。

l 位置(Place),在記憶體中標識未知的額表示式。

l 右值(RValue),產生值的表示式。

具體的工作原理見《Rust程式設計之道》的第158和159頁。

可以在play.runst-lang.org中生成MIR程式碼。

1.1 Rust安裝

Ø 方法一:見Rust官方的installation章節介紹。

實際上就是呼叫該命令來安裝即可:curl https://sh.rustup.rs -sSf | sh

Ø 方法二:下載離線的安裝包來安裝,具體的可見Rust官方的Other Rust Installation Methods章節。

1.2 Rust編譯&執行

1.2.1 Cargo包管理

Cargo是Rust中的包管理工具,第三方包叫做crate

Cargo一共做了四件事:

l 使用兩個元資料(metadata)檔案來記錄各種專案資訊

l 獲取並構建專案的依賴關係

l 使用正確的引數呼叫rustc或其他構建工具來構建專案

l 為Rust生態系統開發建議了統一標準的工作流

Cargo檔案:

l Cargo.lock:只記錄依賴包的詳細資訊,不需要開發者維護,而是由Cargo自動維護

l Cargo.toml:描述專案所需要的各種資訊,包括第三方包的依賴

cargo編譯預設為Debug模式,在該模式下編譯器不會對程式碼進行任何最佳化。也可以使用--release引數來使用釋出模式。release模式,編譯器會對程式碼進行最佳化,使得編譯時間變慢,但是程式碼執行速度會變快。

官方編譯器rustc,負責將rust原始碼編譯為可執行的檔案或其他檔案(.a、.so、.lib等)。例如:rustc box.rs

Rust還提供了包管理器Cargo來管理整個工作流程。例如:

l cargo new first_pro_create :建立名為first_pro_create的專案

l cargo new --lib first_lib_create :建立命令first_lib_create的庫專案

l cargo doc

l cargo doc --open

l cargo test

l cargo test -- --test-threads=1

l cargo build

l cargo build --release

l cargo run

l cargo install --path

l cargo uninstall first_pro_create

l cargo new –bin use_regex

1.2.2 使用第三方包

Rust可以在Cargo.toml中的[dependencies]下新增想依賴的包來使用第三方包。

然後在src/main.rssrc/lib.rs檔案中,使用extern crate命令宣告引入該包即可使用。

例如:

值得注意的是,使用extern crate宣告包的名稱是linked_list,用的是下劃線_”,而在Cargo.toml中用的是連字元-”。其實Cargo預設會把連字元轉換成下劃線

Rust也不建議以“-rs”或“_rs”為字尾來命名包名,而且會強制性的將此後綴去掉。

具體的見《Rust程式設計之道》的第323頁。

1.4 Rust常用命令

1.5 Rust命令規範

Ø 函式:蛇形命名法(snake_case),例如:func_name()

Ø 檔名:蛇形命名法(snake_case),例如file_name.rs、main.rs

Ø 臨時變數名:蛇形命名法(snake_case)

Ø 全域性變數名

Ø 結構體:大駝峰命名法,例如:structFirstName { name: String}

Ø enum型別: 大駝峰命名法。

Ø 關聯常量:常量名必須全部大寫。什麼是關聯常量見《Rust程式設計之道》的第221頁。

Ø Cargo預設會把連字元-”轉換成下劃線_”。

Ø Rust也不建議以“-rs”或“_rs”為字尾來命名包名,而且會強制性的將此後綴去掉。

二、Rust語法

2.1 疑問&總結

2.1.1 Copy語義 && Move語義(Move語義必須轉移所有權)

型別越來越豐富,值型別和引用型別難以描述全部情況,所以引入了:

Ø 值語義(Value Semantic)

複製以後,兩個資料物件擁有的儲存空間是獨立的,互不影響。

基本的原生型別都是值語義,這些型別也被稱為POD(Plain old data)。POD型別都是值語義,但是值語義型別並不一定都是POD型別。

具有值語義的原生型別,在其作為右值進行賦值操作時,編譯器會對其進行按位複製

Ø 引用語義(Reference Semantic)

複製以後,兩個資料物件互為別名。操作其中任意一個數據物件,則會影響另外一個。

智慧指標Box<T>封裝了原生指標,是典型的引用型別。Box<T>無法實現Copy,意味著它被rust標記為了引用語義,禁止按位複製。

引用語義型別不能實現Copy,但可以實現Clone的clone方法,以實現深複製。

在Rust中,可以透過是否實現Copy trait來區分資料型別的值語義引用語義。但為了更加精準,Rust也引用了新的語義:複製(Copy)語義移動(Move)語義

Ø Copy語義:對應值語義,即實現了Copy的型別在進行按位複製時是安全的。

Ø Move語義:對應引用語義。在Rust中不允許按位複製,只允許移動所有權

2.1.2 哪些實現了Copy

Ø 結構體 :當成員都是複製語義型別時,不會自動實現Copy。

Ø 列舉體 :當成員都是複製語義型別時,不會自動實現Copy。

結構體 && 列舉體

1) 所有成員都是複製語義型別時,需要新增屬性#[derive(Debug,Copy,Clone)]來實現Copy。

2)如果有移動語義型別的成員,則無法實現Copy。

Ø 元組型別 :本身實現了Copy。如果元素均為複製語義型別,則預設是按位複製,否則執行移動語義。

Ø 字串字面量 &str: 支援按位複製。例如:c = “hello”; 則c就是字串字面量。

2.1.3 哪些未實現Copy

Ø 字串物件String to_string() 可以將字串字面量轉換為字串物件。

2.1.4 哪些實現了Copy trait

Ø 原生整數型別

對於實現Copy的型別,其clone方法只需要簡單的實現按位複製即可。

2.1.5 哪些未實現Copy trait

Ø Box<T>

實現了Copy trait,有什麼作用?

實現Copy trait的型別同時擁有複製語義,在進行賦值或者傳入函式等操作時,預設會進行按位複製。

Ø 對於預設可以安全的在棧上進行按位複製的型別,就只需要按位複製,也方便管理記憶體。

Ø 對於預設只可在堆上儲存的資料,必須進行深度複製。深度複製需要在堆記憶體中重新開闢空間,這會帶來更多的效能開銷。

2.1.6 哪些是在棧上的?哪些是在堆上的?

2.1.7 let繫結

Ø Rust宣告的繫結預設為不可變

Ø 如果需要修改,可以用mut來宣告繫結是可變的。

2.2 資料型別

很多程式語言中的資料型別是分為兩類:

Ø 值型別

一般是指可以將資料都儲存在同一位置的型別。例如數值、布林值、結構體等都是值型別。

值型別有:

l 原生型別

l 結構體

l 列舉體

Ø 引用型別

會存在一個指向實際儲存區的指標。比如通常一些引用型別會將資料儲存在堆中,而棧中只存放指向堆中資料的地址(指標)。

引用型別有:

l 普通引用型別

l 原生指標型別

2.2.1 基本資料型別

布林型別

bool型別只有兩個值:truefalse

基本數字型別

主要關注取值範圍,具體的見《Rust程式設計之道》的第26頁。

字元型別

單引號來定義字元(char)型別。字元型別代表一個Unicode標量值,每個位元組佔4個位元組。

陣列型別

陣列的型別簽名為[T; N]T是一個泛型標記,代表陣列中元素的某個具體型別。N代表陣列長度,在編譯時必須確定其值。

陣列特點:

l 大小固定

l 元素均為同類型

l 預設不可變

切片型別

切片(Slice)型別是對一個數組的引用片段。在底層,切片代表一個指向陣列起始位置的指標和陣列長度。用[T]型別表示連續序列,那麼切片型別就是&[T]&mut[T]

具體的見《Rust程式設計之道》的第30頁。

str字串型別

字串型別str,通常是以不可變借用的形式存在,即&str(字串切片)。

Rust將字串分為兩種:

1) &str :固定長度字串

2)String :可以隨意改變其長度。

&str字串型別由兩部分組成:

1)指向字串序列的指標;

2)記錄長度的值。

&str儲存於棧上,str字串序列儲存於程式的靜態只讀資料段或者堆記憶體中。

&str是一種胖指標

never型別

never型別,即!。該型別用於表示永遠不可能有返回值的計算型別。

其他(此部分不屬於基本資料型別)

此部分不屬於基本資料型別,由於編排問題,暫時先放在此處。

胖指標

胖指標:包含了動態大小型別地址資訊和攜帶了長度資訊的指標。

具體的見《Rust程式設計之道》的第54頁。

零大小型別

零大小型別(Zero sized Type,ZST)的特點是:它們的值就是其本身,執行時並不佔用記憶體空間。

單元型別單元結構體大小為零,由單元型別組成的陣列大小也是零。

ZST型別代表的意義是“”。

底型別

底型別其實是介紹過的never型別,用歎號!)表示。它的特點是:

l 沒有值

l 是其他任意型別的子型別

如果說ZST型別表示“”的話,那麼底型別就表示“”。

底型別無值,而且它可以等價於任意型別。

具體的見《Rust程式設計之道》的第57頁。

2.2.2 複合資料型別

元組

Rust提供了4中複合資料型別:

l 元組(Tuple)

l 結構體(Struct)

l 列舉體(Enum)

l 聯合體(Union)

先來介紹元組。元組是一種異構有限序列,形如(T,U,M,N)。所謂異構,就是指元組內的元素可以是不同型別。所謂有限,是指元組有固定的長度。

l 空元組()

l 只有一個值時,需要加逗號: (0,)

結構體

Rust提供了3中結構體:

l 具名結構體

l 元組結構體

l 單元結構體

例如:

Ø 具名結構體

struct People{

name: &’static str,

}

Ø 元組結構體:欄位沒有名稱,只有型別:

struct Color(i32, i32, i32);

當一個元組結構體只有一個欄位的時候,稱為New Type模式。例如:

struct Integer(u32);

Ø 單元結構體:沒有任何欄位的結構體。單元結構體例項就是其本身。

struct Empty;

結構體更新語法

使用Struct更新語法(..)從其他例項建立新例項。當新例項使用舊例項的大部分值時,可以使用structupdate語法。 例如:

#[derive(Debug,Copy,Clone)]

struct Book<’a> {

name: &’a str,

isbn: i32,

version: i32,

}

let book = Book {

name: “Rust程式設計之道”, isbn: 20181212, version:1

};

let book2 = Book {version: 2, ..book};

注:

l 如果結構體使用了移動語義的成員欄位,則不允許實現Copy。

l Rust不允許包含了String型別欄位的結構體實現Copy。

l 更新語法會轉移欄位的所有權。

列舉體

該型別包含了全部可能的情況,可以有效的防止使用者提供無效值。例如:

enum Number {

Zero,

One,

}

Rust還支援攜帶型別引數的列舉體。這樣的列舉值本質上屬於函式型別,他可以透過顯式的指定型別來轉換為函式指標型別。例如:

enum IpAddr{

V4(u8, u8, u8, u8),

V6(String),

}

列舉體在Rust中屬於非常重要的型別之一。例如:Option列舉型別。

聯合體

2.2.3 常用集合型別

線性序列:向量

在Rust標準庫std::collections模組下有4中通用集合型別,分別如下:

l 線性序列向量(Vec)雙端佇列(VecDeque)連結串列(LinkedList)

l Key-Value對映表無序雜湊表(HashMap)有序對映表(BTreeMap)

l 集合型別無序集合(HashSet)有序集合(BTreeSet)

l 優先佇列二叉(BinaryHeap)

具體的見《Rust程式設計之道》的第38頁和271頁。

向量也是一種陣列,和基本資料型別中的陣列的區別在於:向量可動態增長

示例:

let mut v1 = vec![];

let mut v2 = vec![0; 10];

let mut v3 = Vec::new();

vec!是一個宏,用來建立向量字面量。

線性序列:雙端佇列

雙端佇列(Double-ended Queue,縮寫Deque)是一種同時具有佇列(先進先出)和棧(後進先出)性質的資料結構。

示例:

use std::collections::VecDeque;

let mut buf = VecDeque::new();

buf.push_front(1);

buf.get(0);

buf.push_back(2);

線性序列:連結串列

Rust提供的連結串列是雙向連結串列,允許在任意一端插入或彈出元素。最好使用Vec或VecDeque型別,他們比連結串列更加快速,記憶體訪問效率更高。

示例:

use std::collections::LinkedList;

let mut list = LinkedList::new();

list.push_front(‘a’);

list.append(&mut list2);

list.push_back(‘b’);

Key-Value對映表:HashMap和BTreeMap

l HashMap<K, V> => 無序

l BTreeMap<K, V> => 有序

其中HashMap要求key是必須可雜湊的型別,BTreeMap的key必須是可排序的。

Value必須是在編譯期已知大小的型別。

示例:

use std::collections::BTreeMap;

use std::collections::HashMap;

let mut hmap = HashMap::new();

let mut bmap = BTreeMap::new();

hmap.insert(1,”a”);

bmap.insert(1,”a”);

集合:HashSet和BTreeSet

HashSet<K>BTreeSet<K>其實就是HashMap<K, V>BTreeMap<K, V>把Value設定為空元組的特定型別。

l 集合中的元素應該是唯一的。

l HashSet中的元素都是可雜湊的型別,BTreeSet中的元素必須是可排序的。

l HashSet應該是無序的,BTreeSet應該是有序的。

示例:

use std::collections::BTreeSet;

use std::collections::HashSet;

let mut hset = HashSet::new();

let mut bset = BTreeSet::new();

hset.insert(”This is a hset.”);

bset.insert(”This is a bset”);

優先佇列:BinaryHeap

Rust提供的優先佇列是基於二叉最大堆(Binary Heap)實現的。

示例:

use std::collections::BinaryHeap;

let mut heap = BinaryHeap::new();

heap.peek(); =>peek是取出堆中最大的元素

heap.push(98);

容量(Capacity)和大小(Size/Len)

無論是Vec還是HashMap,使用這些集合容器型別,最重要的是理解容量(Capacity)和大小(Size/Len)

容量是指為集合容器分配的記憶體容量。

大小是指集合中包含的元素數量。

​​2.2.4 Rust字串

Rust字串分為以下幾種型別:

l str:表示固定長度的字串

l String:表示可增長的字串

l CStr:表示由C分配而被Rust借用的字串。這是為了相容windows系統。

l CString:表示由Rust分配且可以傳遞給C函式使用的C字串,同樣用於和C語言互動。

l OsStr:表示和作業系統相關的字串。這是為了相容windows系統。

l OsString:表示OsStr的可變版本。與Rust字串可以相互交換。

l Path:表示路徑,定義於std::path模組中。Path包裝了OsStr。

l PathBuf:跟Path配對,是path的可變版本。PathBuf包裝了OsString。

str屬於動態大小型別(DST),在編譯期並不能確定其大小。所以在程式中最常見的是str的切片(Slice)型別&str。

&str代表的是不可變的UTF-8位元組序列,建立後無法再為其追加內容或更改其內容。&str型別的字串可以儲存在任意地方:

Ø 靜態儲存區

Ø 堆分配

Ø 棧分配

具體的見《Rust程式設計之道》的第249頁。

String型別本質是一個成員變數為Vec<u8>型別的結構體,所以它是直接將字元內容存放於堆中的。

String型別由三部分組成:

Ø 執行堆中位元組序列的指標(as_ptr方法)

Ø 記錄堆中位元組序列的位元組長度(len方法)

Ø 堆分配的容量(capacity方法)

2.2.4.1 字串處理方式

Rust中的字串不能使用索引訪問其中的字元,可以透過byteschars兩個方法來分別返回按位元組按字元迭代的迭代器。

Rust提供了另外兩種方法:getget_mut來透過指定索引範圍來獲取字串切片。

具體的見《Rust程式設計之道》的第251頁。

2.2.4.2 字串修改

Ø 追加字串:pushpush_str,以及extend迭代器

Ø 插入字串:insertinsert_str

Ø 連線字串:String實現了Add<&str>AddAssign<&str>兩個trait,所以可以使用“+”和“+=”來連線字串

Ø 更新字串:透過迭代器或者某些unsafe的方法

具體的見《Rust程式設計之道》的第255頁。

2.2.4.3 字串的查詢

Rust總共提供了20個方法涵蓋了以下幾種字串匹配操作:

Ø 存在性判斷

Ø 位置匹配

Ø 分割字串

Ø 捕獲匹配

Ø 替代匹配

具體的見《Rust程式設計之道》的第256頁。

2.2.4.4 型別轉換

Ø parse:將字串轉換為指定的型別

Ø format!宏:將其他型別轉成成字串

2.2.5 格式化規則

l 填充字串寬度:{:5},5是指寬度為5

l 擷取字串:{:.5}

l 對齊字串:{:>}{:^}{:<},分別表示左對齊位於中間右對齊

l {:*^5} 使用*替代預設空格來填充

l 符號+:表示強制輸出整數的正負符號

l 符號#:用於顯示進位制的字首。比如:十六進位制0x

l 數字0:用於把預設填充的空格替換成數字0

l {:x} :轉換成16進位制輸出

l {:b} :轉換成二進位制輸出

l {:.5}:指定小數點後有效位是5

l {:e}:科學計數法表示

具體的見《Rust程式設計之道》的第265頁。

2.2.6 原生字串宣告語法:r”…”

原生字串宣告語法(r”…”)可以保留原來字串中的特殊符號。

具體的見《Rust程式設計之道》的第270頁。

2.2.7 全域性型別

Rust支援兩種全域性型別:

l 普通常量(Constant)

l 靜態變數(Static)

區別:

l 都是在編譯期求值的,所以不能用於儲存需要動態分配記憶體的型別

l 普通常量可以被內聯的,它沒有確定的記憶體地址,不可變

l 靜態變數不能被內聯,它有精確的記憶體地址,擁有靜態生命週期

l 靜態變數可以透過內部包含UnsafeCell等容器實現內部可變性

l 靜態變數還有其他限制,具體的見《Rust程式設計之道》的第326頁

l 普通常量也不能引用靜態變數

在儲存的資料比較大需要引用地址具有可變性的情況下使用靜態變數。否則,應該優先使用普通常量。

但也有一些情況是這兩種全域性型別無法滿足的,比如想要使用全域性的HashMap,在這種情況下,推薦使用lazy_static包。利用lazy_static包可以把定義全域性靜態變數延遲到執行時,而非編譯時。

2.3 trait

trait是對型別行為的抽象。trait是Rust實現零成本抽象的基石,它有如下機制:

l trait是Rust唯一的介面抽象方式;

l 可以靜態分發,也可以動態分發;

l 可以當做標記型別擁有某些特定行為的“標籤”來使用。

示例:

structDuck;

structPig;

trait Fly{

fn fly(&self) -> bool;

}

impl Fly for Duck {

fn fly(&self) -> bool {

returntrue;

}

}

impl Fly for Pig {

fn fly(&self) -> bool {

returnfalse;

}

}

靜態分發和動態分發的具體介紹可見《Rust程式設計之道》的第46頁。

trait限定

以下這些需要繼續深入理解第三章並總結。待後續繼續補充。

trait物件

標籤trait

Copy trait

Deref解引用

as運算子

From和Into

2.4 指標

2.3.1 引用Reference

&& mut運算子來建立。受Rust的安全檢查規則的限制。

引用是Rust提供的一種指標語義。引用是基於指標的實現,他與指標的區別是:指標儲存的是其指向記憶體的地址,而引用可以看做某塊記憶體的別名(Alias)。

在所有權系統中,引用&x也可以稱為x的借用(Borrowing)。透過&運算子來完成所有權租借

2.3.2 原生指標(裸指標)

*const T*mut T。可以在unsafe塊下任意使用,不受Rust的安全檢查規則的限制。

2.3.3 智慧指標

實際上是一種結構體,只是行為類似指標。智慧指標是對指標的一層封裝,提供了一些額外的功能,比如自動釋放堆記憶體。

智慧指標區別於常規結構體的特性在於:它實現了DerefDrop這兩個trait。

Ø Deref:提供瞭解引用能力

Ø Drop:提供了自動析構的能力

2.3.3.1 智慧指標有哪些

智慧指標擁有資源的所有權,而普通引用只是對所有權的借用。

Rust中的值預設被分配到棧記憶體。可以透過Box<T>將值裝箱(在堆記憶體中分配)。

Ø String

Ø Vec

String型別和Vec型別的值都是被分配到堆記憶體返回指標的,透過將返回的指標封裝來實現DerefDrop

Ø Box<T>

Box<T>是指向型別為T的堆記憶體分配值的智慧指標。當Box<T>超出作用域範圍時,將呼叫其解構函式,銷燬內部物件,並自動釋放堆中的記憶體。

Ø Arc<T>

Ø RC<T>

單執行緒引用計數指標,不是執行緒安全的型別。

可以將多個所有權共享給多個變數,每當共享一個所有權時,計數就會增加一次。具體的見《Rust程式設計之道》的第149頁。

Ø Weak<T>

RC<T>的另一個版本。

透過clone方法共享的引用所有權稱為強引用RC<T>是強引用。

Weak<T>共享的指標沒有所有權,屬於弱引用。具體的見《Rust程式設計之道》的第150頁。

Ø Cell<T>

實現欄位級內部可變的情況。

適合複製語義型別。

Ø RefCell<T>

適合移動語義型別。

Cell<T>和RefCell<T>本質上不屬於智慧指標,只是提供內不可變性的容器。

Cell<T>和RefCell<T>使用最多的場景就是配合只讀引用來使用。

具體的見《Rust程式設計之道》的第151頁。

Ø Cow<T>

Copyon write:一種列舉體的智慧指標。Cow<T>表示的是所有權的“借用”和“擁有”。Cow<T>的功能是:以不可變的方式訪問借用內容,以及在需要可變借用或所有權的時候再克隆一份資料。

Cow<T>旨在減少複製操作,提高效能,一般用於讀多寫少的場景。

Cow<T>的另一個用處是統一實現規範。

2.3.4 解引用deref

解引用會獲得所有權。

解引用運算子: *

哪些實現了deref方法

Ø Box<T>:原始碼見《Rust程式設計之道》的第147頁。

Ø Cow<T>:意味著可以直接呼叫其包含資料的不可變方法。具體的要點可見《Rust程式設計之道》的第155頁。

Ø Box<T>支援解引用移動, Rc<T>和Arc<T>智慧指標不支援解引用移動。

2.4 所有權機制(ownership):

Rust中分配的每塊記憶體都有其所有者,所有者負責該記憶體的釋放和讀寫許可權,並且每次每個值只能有唯一的所有者。

在進行賦值操作時,對於可以實現Copy的複製語義型別,所有權並未改變。對於複合型別來說,是複製還是移動,取決於其成員的型別。

例如:如果陣列的元素都是基本的數字型別,則該陣列是複製語義,則會按位複製。

2.4.1 詞法作用域(生命週期)

matchforloopwhileif letwhile let花括號函式閉包都會建立新的作用域,相應繫結的所有權會被轉移,具體的可見《Rust程式設計之道》的第129頁。

函式體本身是獨立的詞法作用域:

Ø 當複製語義型別作為函式引數時,會按位複製。

Ø 如果是移動語義作為函式引數,則會轉移所有權。

2.4.2 非詞法作用域宣告週期

借用規則借用方的生命週期不能長於出借方的生命週期。用例見《Rust程式設計之道》的第157頁。

因為以上的規則,經常導致實際開發不便,所以引入了非詞法作用域生命週期(Non-Lexical Lifetime,NLL)來改善。

MIR是基於控制流圖(Control Flow Graph,CFG)的抽象資料結構,它用有向圖(DAG)形式包含了程式執行過程中所有可能的流程。所以將基於MIR的借用檢查稱為非詞法作用域的生命週期。

2.4.2 所有權借用

使用可變借用的前提是:出借所有權的繫結變數必須是一個可變繫結。

在所有權系統中,引用&x也可以稱為x的借用(Borrowing)。透過&運算子來完成所有權租借。所以引用並不會造成繫結變數所有權的轉移。

引用在離開作用域之時,就是其歸還所有權之時。

Ø 不可變借用(引用)不能再次出借為可變借用。

Ø 不可變借用可以被出借多次。

Ø 可變借用只能出借一次。

Ø 不可變借用和可變借用不能同時存在,針對同一個繫結而言。

Ø 借用的生命週期不能長於出借方的生命週期。具體的舉例見《Rust程式設計之道》的第136頁。

核心原則:共享不可變,可變不共享

因為解引用操作會獲得所有權,所以在需要對移動語義型別(如&String)進行解引用時需要特別注意。

2.4.3 生命週期引數

編譯器的借用檢查機制無法對跨函式的借用進行檢查,因為當前借用的有效性依賴於詞法作用域。所以,需要開發者顯式的對借用的生命週期引數進行標註。

2.4.3.1 顯式生命週期引數

Ø 生命週期引數必須是以單引號開頭;

Ø 引數名通常都是小寫字母,例如:'a

Ø 生命週期引數位於引用符號&後面,並使用空格來分割生命週期引數和型別。

標註生命週期引數是由於borrowedpointers導致的。因為有borrowed pointers,當函式返回borrowed pointers時,為了保證記憶體安全,需要關注被借用的記憶體的生命週期(lifetime)。

標註生命週期引數並不能改變任何引用的生命週期長短,它只用於編譯器的借用檢查,來防止懸垂指標。即:生命週期引數的目的是幫助借用檢查器驗證合法的引用,消除懸垂指標

例如:

&i32; ==> 引用

&'a i32; ==> 標註生命週期引數的引用

&'a mut i32; ==> 標註生命週期引數的可變引用

允許使用&'a str;的地方,使用&'static str;也是合法的。

對於'static:當borrowedpointers指向static物件時需要宣告'static lifetime。

如:

static STRING: &'static str = "bitstring";

2.4.3.2 函式簽名中的生命週期引數

fn foo<'a>(s: &'a str, t: &'a str) -> &'a str;

函式名後的<'a>為生命週期引數的宣告。函式或方法引數的生命週期叫做輸入生命週期(input lifetime),而返回值的生命週期被稱為輸出生命週期(output lifetime)

規則:

Ø 禁止在沒有任何輸入引數的情況下返回引用,因為會造成懸垂指標。

Ø 從函式中返回(輸出)一個引用,其生命週期引數必須與函式的引數(輸入)相匹配,否則,標註生命週期引數也毫無意義。

對於多個輸入引數的情況,也可以標註不同的生命週期引數。具體的舉例見《Rust程式設計之道》的第139頁。

2.4.3.3 結構體定義中的生命週期引數

結構體在含有引用型別成員的時候也需要標註生命週期引數,否則編譯失敗。

例如:

struct Foo<'a> {

part: &'a str,

}

這裡生命週期引數標記,實際上是和編譯器約定了一個規則:

結構體例項的生命週期應短於或等於任意一個成員的生命週期。

2.4.3.4 方法定義中的生命週期引數

結構體中包含引用型別成員時,需要標註生命週期引數,則在impl關鍵字之後也需要宣告生命週期引數,並在結構體名稱之後使用。

例如:

impl<'a> Foo<'a> {

fn split_first(s: &'a str) -> &'a str {

}

}

在新增生命週期引數'a之後,結束了輸入引用的生命週期長度要長於結構體Foo例項的生命週期長度。

注:列舉體和結構體對生命週期引數的處理方式是一樣的。

2.4.3.5 靜態生命週期引數

靜態生命週期 'static:是Rust內建的一種特殊的生命週期。'static生命週期存活於整個程式執行期間。所有的字串字面量都有生命週期,型別為& 'static str

字串字面量是全域性靜態型別,他的資料和程式程式碼一起儲存在可執行檔案的資料段中,其地址在編譯期是已知的,並且是隻讀的,無法更改。

2.4.3.6 省略生命週期引數

滿足以下三條規則時,可以省略生命週期引數。該場景下,是將其硬編碼到Rust編譯器重,以便編譯期可以自動補齊函式簽名中的生命週期引數。

生命週期省略規則:

l 每一個在輸入位置省略的生命週期都將成為一個不同的生命週期引數。即對應一個唯一的生命週期引數。

l 如果只有一個輸入的生命週期位置(無論省略還是沒省略),則該生命週期都將分配給輸出生命週期。

l 如果有多個輸入生命週期位置,而其中包含著 &self 或者 &mutself,那麼 self 的生命週期都將分配給輸出生命週期。

以上這部分規則還沒理解透徹,需要繼續熟讀《Rust程式設計之道》的第143頁。

2.4.3.7 生命週期限定

生命週期引數可以向trait那樣作為泛型的限定,有以下兩種形式:

l T: 'a,表示T型別中的任何引用都要“獲得”和'a一樣長。

l T: Trait + 'a,表示T型別必須實現Trait這個trait,並且T型別中任何引用都要“活的”和'a一樣長。

具體的舉例見《Rust程式設計之道》的第145頁。

2.4.3.8 trait物件的生命週期

具體的舉例見《Rust程式設計之道》的第146頁。

2.4.3.9 高階生命週期

Rust還提供了高階生命週期(Higher-Ranked Lifetime)方案,該方案也叫高階trait限定(Higher-Ranked Trait Bound,HRTB)。該方案提供了for<>語法。

for<>語法整體表示此生命週期引數只針對其後面所跟著的“物件”。

具體的可見《Rust程式設計之道》的第192頁。

2.5 併發安全與所有權

2.5.1 標籤trait:Send和Sync

Ø 如果型別T實現了Send: 就是告訴編譯器該型別的例項可以線上程間安全傳遞所有權

Ø 如果型別T實現了Sync:就是向編譯器表明該型別的例項在多執行緒併發中不可能導致記憶體不安全,所以可以安全的跨執行緒共享

2.5.2 哪些型別實現了Send

2.5.3 哪些型別實現了Sync

2.6 原生型別

Rust內建的原生型別 (primitive types) 有以下幾類:

l 布林型別:有兩個值true和false。

l 字元型別:表示單個Unicode字元,儲存為4個位元組。

l 數值型別:分為有符號整數 (i8, i16, i32, i64, isize)、 無符號整數 (u8, u16, u32, u64, usize) 以及浮點數 (f32, f64)。

l 字串型別:最底層的是不定長型別str,更常用的是字串切片&str和堆分配字串String, 其中字串切片是靜態分配的,有固定的大小,並且不可變,而堆分配字串是可變的。

l 陣列:具有固定大小,並且元素都是同種型別,可表示為[T; N]。

l 切片:引用一個數組的部分資料並且不需要複製,可表示為&[T]。

l 元組:具有固定大小的有序列表,每個元素都有自己的型別,透過解構或者索引來獲得每個元素的值。

l 指標:最底層的是裸指標const T和mut T,但解引用它們是不安全的,必須放到unsafe塊裡。

l 函式:具有函式型別的變數實質上是一個函式指標。

l 元型別:即(),其唯一的值也是()。

2.7 函式

2.7.1 函式引數

l 當函式引數按值傳遞時,會轉移所有權或者執行復制(Copy)語義。

l 當函式引數按引用傳遞時,所有權不會發生變化,但是需要有生命週期引數(符合規則時不需要顯示的標明)。

2.7.2 函式引數模式匹配

l ref :使用模式匹配來獲取引數的不可變引用。

l ref mut :使用模式匹配來獲取引數的可變引用。

l 除了ref和refmut,函式引數也可以使用萬用字元來忽略引數。

具體可見《Rust程式設計之道》的第165頁。

2.7.3 泛型函式

函式引數並未指定具體的型別,而是用了泛型T,對T只有一個Mult trait限定,即只有實現了Mul的型別才可以作為引數,從而保證了型別安全。

泛型函式並未指定具體型別,而是靠編譯器來進行自動推斷的。如果使用的都是基本原生型別,編譯器推斷起來比較簡單。如果編譯器無法自動推斷,就需要顯式的指定函式呼叫的型別。

2.7.4 方法和函式

方法代表某個例項物件的行為,函式只是一段簡單的程式碼,它可以透過名字來進行呼叫。方法也是透過名字來進行呼叫,但它必須關聯一個方法接受者。

2.7.5 高階函式

高階函式是指以函式作為引數或返回值的函式,它是函數語言程式設計語言最基礎的特性。

具體可見《Rust程式設計之道》的第168頁。

2.8 閉包Closure

閉包通常是指詞法閉包,是一個持有外部環境變數的函式。

外部環境是指閉包定義時所在的詞法作用域。

外部環境變數,在函數語言程式設計正規化中也被稱為自由變數,是指並不是在閉包內定義的變數。

將自由變數和自身繫結的函式就是閉包

閉包的大小在編譯期是未知的。

2.8.1 閉包的基本語法

閉包管道符(兩個對稱的豎線)和花括號(或圓括號)組成。

Ø 管道符裡是閉包函式的引數,可以向普通函式引數那樣在冒號後新增型別標註,也可以省略。例如:let add = |a, b| -> i32 { a + b};

Ø 花括號裡包含的是閉包函式執行體,花括號和返回值也可以省略。

例如:let add = |a, b| a + b;

Ø 當閉包函式沒有引數只有捕獲的自由變數時,管道符裡的引數也可以省略。

例如: let add = || a + b;

2.8.2 閉包的實現

閉包是一種語法糖。閉包不屬於Rust語言提供的基本語法要素,而是在基本語法功能之上又提供的一層方便開發者程式設計的語法。

閉包和普通函式的差別就是閉包可以捕獲環境中的自由變數。

閉包可以作為函式引數,這一點直接提升了Rust語言的抽象表達能力。當它作為函式引數傳遞時,可以被用作泛型的trait限定,也可以直接作為trait物件來使用。

閉包無法直接作為函式的返回值,如果要把閉包作為返回值,必須使用trait物件。

2.8.3 閉包與所有權

閉包表示式會由編譯器自動翻譯為結構體例項,併為其實現Fn、FnMut、FnOnce三個trait中的一個。

l FnOnce:會轉移方法接收者的所有權。沒有改變環境的能力,只能呼叫一次。

l FnMut:會對方法接收者進行可變借用。有改變環境的能力,可以多次呼叫。

l Fn:會對方法接收者進行不可變借用。沒有改變環境的能力,可以多次呼叫。

Ø 如果要實現Fn,就必須實現FnMutFnOnce

Ø 如果要實現FnMut,就必須實現FnOnce

Ø 如果要實現FnOnce,就不需要實現FnMutFn

2.8.3.1 捕獲環境變數的方式

l 對於複製語義型別,以不可變引用(&T)來進行捕獲。

l 對於移動語義型別,執行移動語義,轉移所有權來進行捕獲。

l 對於可變繫結,並且在閉包中包含對其進行修改的操作,則以可變引用(&mut T)來進行捕獲。

具體可見《Rust程式設計之道》的第178頁。

Rust使用move關鍵字來強制讓閉包所定義環境中的自由變數轉移到閉包中。

2.8.3.2 規則總結

l 如果閉包中沒有捕獲任何環境變數,則預設自動實現Fn

l 如果閉包中捕獲了複製語義型別的環境變數,則:

Ø 如果不需要修改環境變數,無論是否使用move關鍵字,均會自動實現Fn

Ø 如果需要修改環境變數,則自動實現FnMut

l 如果閉包中捕獲了移動語義型別的環境變數,則:

Ø 如果不需要修改環境變數,而且沒有使用move關鍵字,則會自動實現FnOnce

Ø 如果不需要修改環境變數,而且使用move關鍵字,則會自動實現Fn

Ø 如果需要修改環境變數,則自動實現FnMut

l FnMut的閉包在使用move關鍵字時,如果捕獲變數是複製語義型別的,則閉包會自動實現Copy/Clone。如果捕獲變數是移動語義型別的,則閉包不會自動實現Copy/Clone。

​​2.9 迭代器

Rust使用的是外部迭代器,也就是for迴圈。外部迭代器:外部可以控制整個遍歷程序。

Rust中使用了trait來抽象迭代器模式。Iterator trait是Rust中對迭代器模式的抽象介面。

迭代器主要包含:

l next方法:迭代其內部元素

l 關聯型別Item

l size_hint方法:返回型別是一個元組,該元組表示迭代器剩餘長度的邊界資訊。

示例:

let iterator = iter.into_iter();

let size_lin = iterator.size_hint();

let mut counter = Counter { count: 0};

counter.next();

Iter型別迭代器,next方法返回的是Option<&[T]>Option<&mut [T]>型別的值。for迴圈會自動呼叫迭代器的next方法。for迴圈中的迴圈變數則是透過模式匹配,從next返回的Option<&[T]>Option<&mut [T]>型別中獲取&[T]&mut [T]型別的值。

Iter型別迭代器在for迴圈中產生的迴圈變數為引用

IntoIter型別的迭代器的next方法返回的是Option<T>型別,在for迴圈中產生的迴圈變數是,而不是引用

示例:

let v = vec![1, 2, 3];

for i in v {

}

為了確保size_hint方法可以獲得迭代器長度的準確資訊,Rust引入了兩個trait,他們是Iterator的子trait,均被定義在std::iter模組中。

l ExactSizeIterator :提供了兩個額外的方法lenis_empty

l TrustedLen :像一個標籤trait,只要實現了TrustLen的迭代器,其size_hint獲取的長度資訊均是可信的。完全避免了容器的容量檢查,提升了效能。

2.9.1 IntoIteratortrait

如果想要迭代某個集合容器中的元素,必須將其轉換為迭代器才可以使用。

Rust提供了FromIterator和IntoIterator兩個trait,他們互為反操作。

l FromIterator :可以從迭代器轉換為指定型別。

l IntoIterator :可以從指定型別轉換為迭代器。

Intoiter可以使用into_iter之類的方法來獲取一個迭代器。into_iter的引數時self,代表該方法會轉移方法接收者的所有權。而還有其他兩個迭代器不用轉移所有權。具體的如下所示:

l Intoiter :轉移所有權,對應self

l Iter :獲取不可變借用,對應&self

l IterMut :獲得可變借用,對應&mut slef

2.9.2 哪些實現了Iterator的型別?

只有實現了Iterator的型別才能作為迭代器。

實現了IntoIterator的集合容器可以透過into_iter方法來轉換為迭代器。

實現了IntoIterator的集合容器有:

l Vec<T>

l &’a [T]

l &’a mut [T] => 沒有為[T]型別實現IntoIterator

2.9.3 迭代器介面卡

透過介面卡模式可以將一個介面轉換成所需要的另一個介面。介面卡模式能夠使得介面不相容的型別在一起工作。

介面卡也叫包裝器(Wrapper)

迭代器介面卡,都定義在std::iter模組中:

l Map :透過對原始迭代器中的每個元素呼叫指定閉包來產生一個新的迭代器。

l Chain :透過連線兩個迭代器來建立一個新的迭代器。

l Cloned :透過複製原始迭代器中全部元素來建立新的迭代器。

l Cycle :建立一個永遠迴圈迭代的迭代器,當迭代完畢後,再返回第一個元素開始迭代。

l Enumerate :建立一個包含計數的迭代器,它返回一個元組(i,val),其中i是usize型別,為迭代的當前索引,val是迭代器返回的值。

l Filter :建立一個機遇謂詞判斷式過濾元素的迭代器。

l FlatMap :建立一個類似Map的結構的迭代器,但是其中不會包含任何巢狀。

l FilterMap :相當於Filter和Map兩個迭代器一次使用後的效果。

l Fuse :建立一個可以快速遍歷的迭代器。在遍歷迭代器時,只要返回過一次None,那麼之後所有的遍歷結果都為None。該迭代器介面卡可以用於最佳化。

l Rev :建立一個可以反向遍歷的迭代器。

具體可見《Rust程式設計之道》的第202頁。

Rust可以自定義迭代器介面卡,具體的見《Rust程式設計之道》的第211頁。

2.10 消費器

迭代器不會自動發生遍歷行為,需要呼叫next方法去消費其中的資料。最直接消費迭代器資料的方法就是使用for迴圈。

Rust提供了for迴圈之外的用於消費迭代器內資料的方法,叫做消費器(Consumer)

Rust標準庫std::iter::Iterator中常用的消費器:

l any :可以查詢容器中是否存在滿足條件的元素。

l fold :該方法接收兩個引數,第一個為初始值,第二個為帶有兩個引數的閉包。其中閉包的第一個引數被稱為累加器,它會將閉包每次迭代執行的結果進行累計,並最終作為fold方法的返回值。

l collect :專門用來將迭代器轉換為指定的集合型別。

l all

l for_each

l position

2.11 鎖

l RwLock讀寫鎖:是多讀單寫鎖,也叫共享獨佔鎖。它允許多個執行緒讀,單個執行緒寫。但是在寫的時候,只能有一個執行緒佔有寫鎖;而在讀的時候,允許任意執行緒獲取讀鎖。讀鎖和寫鎖不能被同時獲取

l Mutex互斥鎖:只允許單個執行緒讀和寫。

三、 Rust屬性

Ø #[lang = “drop”] : 將drop標記為語言項

Ø #[derive(Debug)] :

Ø #[derive(Copy, Clone)] :

Ø #[derive(Debug,Copy,Clone)]

Ø #[lang = “owned_box”] : Box<T>與原生型別不同,並不具備型別名稱,它代表所有權唯一的智慧指標的特殊性,需要使用lang item來專門識別。

Ø #[lang =“fn/fn_mut/fn_once”] :表示其屬於語言項,分別以fn、fn_mut、fn_once名稱來查詢這三個trait。

l fn_once:會轉移方法接收者的所有權

l fn_mut:會對方法接收者進行可變借用

l fn:會對方法接收者進行不可變借用

Ø #[lang =“rust_pareen_sugar”] :表示對括號呼叫語法的特殊處理。

Ø #[must_use=”iterator adaptors arelazy ……”] :用來發出警告,提示開發者迭代器介面卡是惰性的。

四、記憶體管理

4.1 記憶體回收

drop-flag:在函式呼叫棧中為離開作用域的變數自動插入布林標記,標註是否呼叫解構函式,這樣,在執行時就可以根據編譯期做的標記來呼叫解構函式

實現了Copy的型別,是沒有解構函式的。因為實現了Copy的型別會複製,其生命週期不受解構函式的影響。

需要繼續深入理解第4章並總結,待後續補充。

五、unicode

Unicode字符集相當於一張表,每個字元對應一個非負整數,該數字稱為碼點(Code Point)

這些碼點也分為不同的型別:

l 標量值

l 代理對碼點

l 非字元碼點

l 保留碼點

l 私有碼點

標量值是指實際存在對應字元的碼位,其範圍是0x0000~0xD7FF0xE000~0x10FFFF兩段。

Unicode字符集的每個字元佔4個位元組,使用的儲存方式是:碼元(Code Unit)組成的序列。

碼元是指用於處理和交換編碼文字的最小位元組合。

Unicode字元編碼表:

l UTF-8 => 1位元組碼元

l UTF-16 => 2位元組碼元

l UTF-32 => 4位元組碼元

Rust的原始碼檔案.rs的預設文字編碼格式是UTF-8。

六、Rust附錄

字串物件常用的方法

16
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 自定義TBE運算元入門,不妨從單運算元開發開始