rust高級話題
前言
每一種語言都有它比較隱秘的點。rust也不例外。
零大小類型ZST
struct Foo; //類單元結構
struct Zero(
(), //單元類型
[u8;0], //0大小的數組
Foo,
);//零大小類型組成的類型
動態大小類型DST
無法靜態確定大小或對齊的類型。
- 特征對象trait objects:dyn Mytrait
- 切片slices:[T]、str
特征包含vtable,通過vtable訪問成員。切片是數組或Vec的一個視圖。
也許你會產生好奇,為什么字符串和特征不能像一般語言設計的那樣,設計成一個指針就好,而弄成一個動態大小的類型。這和普通指針有什么不同?
從程序員的角度出發,所謂動態大小類型是不存在的,因為你不能構造一個動態大小類型的對象出來,不管如何你只能構造"動態大小類型的指針"。動態大小類型更像是一個思維過程的中間產物。
注意,動態大小類型的指針和普通指針是不同的:
- 動態大小類型指針是胖指針,有地址,還有大小,也就是多維的。
如&str 可以想象成:
&str{
ptr:*u8,
size:usize,
}
既然如此,那么解引用*&str就是無意義的,因為它丟失了對象的大小。這個角度去理解動態大小類型,或者比較具體。
rust中的動態大小類型,其本質是將原本對象中的大小信息,直接放到指針里面,形成一個胖指針,而對象自身是不包含大小的。這是合理的,比如c語言中的字符串,本質就是一個\0
結束的字符串序列而已,並不包含什么大小字段,也不可能要求所有字符串都要帶一個大小的字段。
為了類型安全,大小信息又是必要的,因而對這類基礎類型做一個抽象,然后用胖指針來指向,不失為一個合理方案。
特征對象的指針也是如此,rust中作為一個胖指針來實現,因此特征對象本身就成了無法構造的動態大小類型了。
對於特征對象,rust有兩個特殊關鍵字支撐其行為(impl 和 dyn):
- impl 靜態分發,泛型技術。(不要和impl xxx for y搞混)
- dyn 動態分發,即指針。(用dyn能更好的對應靜態分發的寫法)
trait S{fn so(&self);};
impl S for i32{fn so(&self){println!("i32");}}
impl S for &str{fn so(&self){println!("&str");}}
//靜態分發,0成本
fn f(a:impl S)->impl S{
a.so();
1
}
f("hi").so();
//動態分發,少量代價
fn f2(a:&dyn S)->&dyn S{
a.so();
&"hi"
}
f2(&1).so();
正確的安裝方法
rust是一個快速變化的語言和編譯系統,建議用最新版,否則可能出現兼容性問題而無法通過編譯。
rustup 管理工具鏈的版本,分別有三個通道:nightly、beta、stable。如上所述,建議同時安裝nightly版本,保持最新狀態。
wget -O- https://sh.rustup.rs |sh #下載安裝腳本並執行
安裝之后,就可以用rustup命令來控制整個工具鏈的更新了。
rustup toolchain add nightly #安裝每夜版本的工具鏈
rustup component add rust-src #安裝源代碼
cargo +nightly install racer #用每夜版本的工具鏈安裝racer,一個代碼補全的工具。因為當前只支持每夜版本
rustup component add rls # 這是面向編輯器的一個輔助服務
rustup component add rust-analysis #分析工具
#vscode : 搜索插件rls安裝即可
結構體
struct 結構體是一種記錄類型。成員稱為域field,有類型和名稱。也可以沒有名稱,稱為元組結構tuple strcut。 只有一個域的特殊情況稱為新類型newtype。一個域也沒有稱為類單元結構unit-like struct。
類別 | 域名稱 | 域個數 | 寫法舉例 |
---|---|---|---|
一般結構 | 有 | >1 | strcut S{x:i32,y:&str} |
元組結構 | 無 | >1 | strcut S(i32,&str) |
新類型 | 無 | 1 | struct S(i32) |
類單元結構 | 無 | 0 | struct S |
枚舉和結構是不同的,枚舉是一個集合(類似c語言種的聯合union,變量的枚舉成員可選,而不是全體成員),而結構是一個記錄(成員必定存在)。
作為一個描述力比較強的語法對象,結構很多時候都在模擬成基礎類型,但是更多時候是具備和基礎類型不同的特征,而需要由用戶來定制它的行為。
#[derive(Copy,Clone,Debug)] //模擬基礎數據類型的自動復制行為,Debug 特征是為了打印輸出
struct A;
let a = A;
let b = a;//自動copy 而不是move
println!("{:?}", a);//ok
復制和移動
rust的基本類型分類可以如此安排:
- 有所有權
- 默認實現copy
- 基本數據類型
- 元素實現copy的復合類型
- 數組
- 元組
- 沒有默認實現copy,都是move
- 結構
- 標准庫中的大部分智能指針(基於結構來實現)
- 標准庫中的數據結構
- String
- 枚舉
- 結構
- 默認實現copy
- 無所有權
- 引用
- &str
- &[T]
- 指針
- 引用
基礎數據類型,引用類1(引用&T,字符串引用&str,切片引用&[T],特征對象&T:strait,原生指針*T),元組(T2),數組[T 2;size],函數指針fn()默認都是copy;而結構、枚舉和大部分標准庫定義的類型(智能指針,String等)默認是move。
注意:
- 引用只是復制指針本身,且沒有數據的所有權;因為可變引用唯一,在同一作用域中無法復制,此時是移動語義的,但可以通過函數參數復制傳遞,此時等於在此位置重新創建該可變引用。
- 元素必須為可復制的
特征對象
特征是一個動態大小類型,它的對象大小根據實現它的實體類型而定。這一點和別的語言中的接口有着本質的不同。rust定義的對象,必須在定義的時候即獲知大小,否則編譯錯誤。
但是,泛型定義除外,因為泛型的實際定義是延遲到使用該泛型時,即給出具體類型的時候。
或者,可以用間接的方式,如引用特征對象。
trait s{}
fn foo(a:impl s)->impl s{a} //這里impl s相等於泛型T:s,屬於泛型技術,根據具體類型單態化
引用、生命周期、所有權
rust的核心創新:
- 引用(借用):borrowing
- 所有權:ownership
- 生命周期:lifetimes
必須整體的理解他們之間的關系。
借用(引用)是一種怎樣的狀態?
例子:
- let mut 舊 = 對象;
- let 新 = &舊 或 &mut 舊;
操作 | 舊讀 | 舊寫 | 新讀 | 新寫 | 新副本 |
---|---|---|---|---|---|
copy | ✔ | ✔ | ✔ | ✔ | ✔ |
move | ✘ | ✘ | ✔ | ✔ | ✘ |
& | ✔ | ✔1 | ✔ | ✘ | ✘ |
&mut | ✔ | ✔1 | ✔ | ✔ | ✘ |
注1:舊寫后引用立即無效。
從上表格可以看出,引用(借用)並不只是產生一個讀寫指針,它同時跟蹤了引用的原始變從量是否有修改,如果修改,引用就無效。這有點類似迭代器,引用是對象的一個視圖。
一般性原則:可以存在多個只讀引用,或者存在唯一可寫引用,且零個只讀引用。(共享不可變,可變不共享)
特殊補充(便利性規則):
可以將可寫引用轉換為只讀引用。該可寫引用只要不寫入或傳遞,rust會認為只是只讀引用間的共享,否則,其他引用自動失效(rust編譯器在不停的進化,其主要方向是注重實質多於形式上,提供更大的便利性)。
let mut a = 1;
let mut rma = &mut a;
let mut ra = &a;//error
let ra = rma as &i32; //ok
println!("*ra={}", ra);//ok
println!("*rma={}", rma);//ok
*rma = 2; //ra 失效
println!("*ra={}", ra);//error
println!("*rma={}", rma);//ok
a = 3; //ra、rma 都失效
println!("*ra={}", ra);//error
println!("*rma={}", rma);//error
用途在哪里?
let p1 = rma; //error
let p2 = ra; //ok
即:需要產生多個引用,但不是傳遞函數參數的場合。如轉換到基類或特征接口(如for循環要轉換到迭代器)。
let v=[1,2,3];
let p1=&mut v;
let p2=p1 as &[i32];
// 注釋掉其中一組代碼
// 1
for item in p1{}
println!("{}",p1);//error
// 2
for item in p2{}
println!("{}",p2);//ok
引用無法移動指向的數據:
let a = String::from("hi");
let pa = &a;
let b = *pa; //error
生命周期
- 所有權生命周期 > 引用
引用並不管理對象生命周期,因為它沒有對象的所有權。引用需要判定的是引用的有效性,即對象生命周期長於引用本身即可。
當需要管理生命周期時,不應該使用引用,而應該用智能指針。
一般而言,我們不喜歡引用,因為引用引入了更多概念,所以我們希望智能指針這種東西來自動化管理所有資源。但不可否認,很多算法直接使用更底層的引用能提高效率。個人認為:結構組織需要智能指針,算法內部可以使用引用。
錯誤處理
為了處理異常情況,rust通過Result<T,E>
定義了基礎的處理框架。
fn f1()->Result<(),Error>{
File::open("file")?; //?自動返回錯誤
OK(()) //正常情況
}
//main()函數怎么處理?
fn main()->Result<(),Error>{}
pub trait Termination{ //返回類型須支持該特征
fn report(self) -> i32;
}
impl Termination for (){
fn report(self) ->i32{
ExitCode::SUCCESS.report()
}
}
impl<E: fmt::Debug> Termination for Result<(),E>{
fn report(self)->i32{
match self{
Ok(())=>().report(),
Err(err)=>{
eprintln!("Error:{:?}",err);
ExitCode::FAILURE.report()
}
}
}
}
交叉編譯
rustup target add wasm-wasi #編譯目標為wasm(網頁匯編)
cargo build --target=wasm-wasi
智能指針
解引用:
- 當調用成員函數時,編譯器會自動調用解引用,這樣
&T => &U => &K
自動轉換類型,直到找到該成員函數。
// *p=>*(p.deref())
// =>*(&Self)
// =>Self
pub trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
// *p=>*(p.deref_mut())
// =>*(&mut Self)
// =>mut Self
pub trait DerefMut: Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}
常用智能指針(管理堆內存上的數據):
擁有所有權:
Box<T>
=>T 堆上變量,對應普通棧上變量Vec<T>
=>[T] 序列String == Vec<u8> =>[u8]
滿足utf8編碼
共享所有權(模擬引用):
Rc<T>
=>T 共享,智能化的&,采用引用計數技術Rc<T>::clone()
產生鏡像,並增加計數Rc<RefCell<T>>
模擬&mut,內部元素可寫
Arc<T>
多線程共享
無所有權:
Weak<T>
弱引用
特殊用途:
Cell<T>
內部可寫RefCell<T>
內部可寫Cow<T>
Clone-on-WritePin<T>
防止自引用結構移動
產生循環引用的條件(如:Rc<RefCell<T>>
):
- 使用引用計數指針
- 內部可變性
設計遞歸型數據結構例子:
//遞歸型數據結構,需要使用引用
//引用是間接結構,否則該遞歸數據結構有無限大小
//其次,引用需要明確初始化,引用遞歸自身導致無法初始化
//因此,需要Option來定義沒引用的情況
//Node(Node) :無限大小
//=> Node(Box<Node>) :不能初始化
//=> Node(Option<Box<Node>>) :ok
struct Node<T>{
next: Option<Box<Self>>,
data:T,
}
有些數據結構,一個節點有多個被引用的關系,比如雙向鏈表,這時只能用Option<Rc<RefCell<T>>>
這套方案,但存在循環引用的風險,程序員需要建立一套不產生循環引用的程序邏輯,如正向強引用,反向弱引用。
編譯器會對自引用(自己的成員引用自己,或另一個成員)的情況進行檢查,因為違反了移動語義。
閉包
pub trait FnOnce<Args> {
type Output;
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
pub trait FnMut<Args> : FnOnce<Args> {
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
pub trait Fn<Args> : FnMut<Args> {
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}
動態分派和靜態分派
靜態分派:泛型
動態反派:特征對象(指針)
特殊類型
// 只讀轉可寫引用:
#[lang = "unsafe_cell"]
#[stable(feature = "rust1", since = "1.0.0")]
pub struct UnsafeCell<T: ?Sized> {
value: T,
}
// 假類型(占位類型)
#[lang = "phantom_data"]
#[stable(feature = "rust1", since = "1.0.0")]
pub struct PhantomData<T:?Sized>;
// 內存分配器
#[derive(Copy, Clone, Default, Debug)]
pub struct Heap;
成員方法
struct A;
impl A{
fn f1(self){}
fn f2(this:Self){}
fn f3()->Self{A}
}
trait X{fn fx(self);}
impl X for A{fn fx(self){}}
//Self 表示實現的具體類型,本例為A
//self = self:Self
//&self = self:&Self
//&mut self = self:&mut Self
let a = A;
a.f1(); //ok 可以用.調用成員方法
a.f2(); //error 不可以,因為第一個參數名字不是self,雖然是Self類型
A::f2(a); //ok
let a=A::f3(); //無參數構造函數的形式,名字隨意
impl i32{} //error ,i32不是當前項目開發的,不能添加默認特征的實現,但可以指定一個特征來擴展i32
impl X for i32{} //ok
規則: 特征和實現類型,必須有一個是在當前項目,也就是說不能對外部類型已經實現的特征進行實現。也就是說,庫開發者應該提供完整的實現。
規則: 用小數點.調用成員函數時,編譯器會自動轉換類型為self的類型(只要可能)。
但是要注意,&self -> self
是不允許的,因為引用不能移動指向的數據。可以實現對應&T
的相同接口來解決這個問題。
let a = &A;
a.fx(); //error 相當於調用fx(*a)
impl X for &A{
fn fx(self){} //這里的self = self:&A
}
a.fx(); //ok
容器、迭代器、生成器
容器 | 說明 |
---|---|
Vec | 序列 |
VecDeque | 雙向隊列 |
LinkedList | 雙向鏈表 |
HashMap | 哈希表 |
BTreeMap | B樹表 |
HashSet | 哈希集 |
BTreeSet | B樹集 |
BinaryHeap | 二叉堆 |
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
...
}
//for 循環實際使用的迭代器
trait IntoIterator {
type Item;
type IntoIter: Iterator<Item=Self::Item>;
fn into_iter(self) -> Self::IntoIter;
}
容器生成三種迭代器:
·iter()
創造一個Item是&T類型的迭代器;·iter_mut()
創造一個Item是&mut T類型的迭代器;·into_iter()
根據調用的類型來創造對應的元素類型的迭代器。- T => R 容器為T,返回元素為R,即move
- &T => &R
- &mut T=> &mut R
適配器(運算完畢返回迭代器):
生成器(實驗性):
let mut g=||{loop{yield 1;}};
let mut f = ||{ match Pin::new(&mut g).resume(){
GeneratorState::Yielded(v)=>println!("{}", v),
GeneratorState::Complete(_)=>{},
};
f(); //print 1
f(); //print 1
類型轉換
pub trait AsRef<T: ?Sized> {
fn as_ref(&self) -> &T;
}
pub trait AsMut<T: ?Sized> {
fn as_mut(&mut self) -> &mut T;
}
//要求hash不變
pub trait Borrow<Borrowed: ?Sized> {
fn borrow(&self) -> &Borrowed;
}
pub trait From<T> {
fn from(T) -> Self;
}
//標准庫已經有默認實現,即調用T::from
pub trait Into<T> {
fn into(self) -> T;
}
//克隆后轉換
pub trait ToOwned {
type Owned: Borrow<Self>;
fn to_owned(&self) -> Self::Owned;
fn clone_into(&self, target: &mut Self::Owned) { ... }
}
//克隆寫入
pub enum Cow<'a, B>
where
B: 'a + ToOwned + ?Sized,
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
pub trait ToString {
fn to_string(&self) -> String;
}
pub trait FromStr {
type Err;
fn from_str(s: &str) -> Result<Self, Self::Err>;
}
運算符重載
trait Add<RHS = Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
I/O 操作
平台相關字符串:
- OsString
- PathBuf
- OsStr
- Path
文件讀寫:
- File
- Read
- BufReader
- Write
標准輸入輸出:
- Stdin
- std::io::stdin()
- Stdout
- std::io::stdout()
- std::env::args()
- std::process::exit()
反射
std::any
多任務編程
啟動新線程框架代碼:
use std::thread;
let child = thread::spawn(move ||{});
child.join(); //等待子線程結束
數據競爭三個條件:
- 數據共享
- 數據修改
- 沒有同步( rust 編譯器保證同步)
數據同步框架:
- Sync 訪問安全
- rust引用機制保證了數據訪問基本是安全的
- 內部可變的類型除外(用了不安全代碼)
- 被
lock()
的內部可變類型也是安全的
- Send 移動安全
- 沒有引用成員的類型;
- 泛型元素T是Send的;
- 或被
lock()
包裝的。
多線程下的數據總結:
T
:移動(或復制),因而無法共享(也就是只能給一個線程使用)&T
- 指向局部變量,生命周期報錯
- 指向
static T
,ok
&mut T
- 同理
- 指向
static mut T
, 不安全報錯
Box<T>
:普通智能指針沒有共享功能,等價T
Rc<T>
:普通共享指針沒有Send特征,技術實現使用了內部可變,但沒有加線程鎖進行安全處理Arc<T>
提供了線程安全的共享指針Mutex<T>
提供了線程安全的可寫能力。- RwLock
- AtomicIsize
- RwLock
use std::sync::{Arc,Mutex};
use std::thread;
const COUNT:u32=1000000;
let a = Arc::new(Mutex::new(123));//線程安全版共享且內部可變
// 1
let c = a.clone();
let child1 = thread::spawn(move ||{for _ in 0..COUNT {*c.lock().unwrap()+=2;}});
// 2
let c = a.clone();
let child2 = thread::spawn(move ||{for _ in 0..COUNT {*c.lock().unwrap()-=1;}});
// 多任務同步
child1.join().ok();
child2.join().ok();
println!("final:{:?}", a);//1000123
模式匹配
模式匹配我之前沒什么接觸,所以感覺挺有意思的(所以有了這一節)。
在rust中,let,函數參數,for循環,if let,while let,match等位置實際上是一個模式匹配的過程,而不是其他語言中普通的定義變量。
let x = 1; //x 這個位置是一個模式匹配的過程
模式匹配會有兩種結果,一種是不匹配,一種是匹配。一個模式匹配位置,要求不能存在不匹配的情況,這種叫必然匹配“irrefutable”,用於定義。否則,用於分支判斷。
很明顯定義變量必然是要求匹配的,如let,函數參數,for循環。而用於分支判斷的是if let,wdhile let,match。
那為什么要用模式匹配來定義變量?因為很靈活,看一下它的表達能力:
- 普通類型的匹配:
x --> T
- 解構引用:
&x --> &T
得 x=T - 解構數組:
[_,_,x,_,_] --> [T;5]
得 x= 第三個元素 - 解構元組:
(..,x) --> (1,2,"x")
得 x= 第三個成員 - 解構結構:
T{0:_,1:x} --> T(1,"x")
得 x= 第二個成員
通過與目標類型差不多的寫法,對復雜類型進行解構,相對直觀。
另一方面,用於判斷的模式匹配可以針對動態的內容,而不只是類型來進行匹配,如:
- 值范圍匹配:
x@1...10 --> 7
得 x=7 - 切片屬於值匹配:
x@[2,_] --> &[1,2,3][1..3]
得 x=&[2,3]
//演示代碼
let [_,_,x,_,_] = [1;5];
let (..,x) = (1,2,'y');
let hi = &['h','e','l','l','o'][2..4];
struct Ax(i32,char);
let Ax{1:_,0:x} = Ax(1,'x');
if let x@['l',_]=hi {println!("{:?}", x)};
if let x@1...10 = 7 {println!("{}",x)};