rust 高級話題


rust高級話題

前言

每一種語言都有它比較隱秘的點。rust也不例外。

零大小類型ZST

struct Foo; //類單元結構 
struct Zero(
    (), //單元類型
    [u8;0], //0大小的數組
    Foo,
);//零大小類型組成的類型

動態大小類型DST

無法靜態確定大小或對齊的類型。

  1. 特征對象trait objects:dyn Mytrait
  2. 切片slices:[T]、str

特征包含vtable,通過vtable訪問成員。切片是數組或Vec的一個視圖。

也許你會產生好奇,為什么字符串和特征不能像一般語言設計的那樣,設計成一個指針就好,而弄成一個動態大小的類型。這和普通指針有什么不同?

從程序員的角度出發,所謂動態大小類型是不存在的,因為你不能構造一個動態大小類型的對象出來,不管如何你只能構造"動態大小類型的指針"。動態大小類型更像是一個思維過程的中間產物。

注意,動態大小類型的指針和普通指針是不同的:

  1. 動態大小類型指針是胖指針,有地址,還有大小,也就是多維的。

如&str 可以想象成:

&str{
    ptr:*u8,
    size:usize,
}

既然如此,那么解引用*&str就是無意義的,因為它丟失了對象的大小。這個角度去理解動態大小類型,或者比較具體。

rust中的動態大小類型,其本質是將原本對象中的大小信息,直接放到指針里面,形成一個胖指針,而對象自身是不包含大小的。這是合理的,比如c語言中的字符串,本質就是一個\0結束的字符串序列而已,並不包含什么大小字段,也不可能要求所有字符串都要帶一個大小的字段。

為了類型安全,大小信息又是必要的,因而對這類基礎類型做一個抽象,然后用胖指針來指向,不失為一個合理方案。

特征對象的指針也是如此,rust中作為一個胖指針來實現,因此特征對象本身就成了無法構造的動態大小類型了。

對於特征對象,rust有兩個特殊關鍵字支撐其行為(impl 和 dyn):

  1. impl 靜態分發,泛型技術。(不要和impl xxx for y搞混)
  2. 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的基本類型分類可以如此安排:

  1. 有所有權
    1. 默認實現copy
      1. 基本數據類型
      2. 元素實現copy的復合類型
        1. 數組
        2. 元組
    2. 沒有默認實現copy,都是move
      1. 結構
        1. 標准庫中的大部分智能指針(基於結構來實現)
        2. 標准庫中的數據結構
        3. String
      2. 枚舉
  2. 無所有權
    1. 引用
      1. &str
      2. &[T]
    2. 指針

基礎數據類型,引用類1(引用&T,字符串引用&str,切片引用&[T],特征對象&T:strait,原生指針*T),元組(T2),數組[T 2;size],函數指針fn()默認都是copy;而結構、枚舉和大部分標准庫定義的類型(智能指針,String等)默認是move。

注意:

  1. 引用只是復制指針本身,且沒有數據的所有權;因為可變引用唯一,在同一作用域中無法復制,此時是移動語義的,但可以通過函數參數復制傳遞,此時等於在此位置重新創建該可變引用。
  2. 元素必須為可復制的

特征對象

特征是一個動態大小類型,它的對象大小根據實現它的實體類型而定。這一點和別的語言中的接口有着本質的不同。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

生命周期

  1. 所有權生命周期 > 引用

引用並不管理對象生命周期,因為它沒有對象的所有權。引用需要判定的是引用的有效性,即對象生命周期長於引用本身即可。

當需要管理生命周期時,不應該使用引用,而應該用智能指針。

一般而言,我們不喜歡引用,因為引用引入了更多概念,所以我們希望智能指針這種東西來自動化管理所有資源。但不可否認,很多算法直接使用更底層的引用能提高效率。個人認為:結構組織需要智能指針,算法內部可以使用引用。

錯誤處理

為了處理異常情況,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

智能指針

解引用:

  1. 當調用成員函數時,編譯器會自動調用解引用,這樣&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;
}

常用智能指針(管理堆內存上的數據):

擁有所有權:

  1. Box<T> =>T 堆上變量,對應普通棧上變量
  2. Vec<T> =>[T] 序列
  3. String == Vec<u8> =>[u8] 滿足utf8編碼

共享所有權(模擬引用):

  1. Rc<T> =>T 共享,智能化的&,采用引用計數技術
    1. Rc<T>::clone() 產生鏡像,並增加計數
    2. Rc<RefCell<T>> 模擬&mut,內部元素可寫
  2. Arc<T> 多線程共享

無所有權:

  1. Weak<T> 弱引用

特殊用途:

  1. Cell<T> 內部可寫
  2. RefCell<T> 內部可寫
  3. Cow<T> Clone-on-Write
  4. Pin<T> 防止自引用結構移動

產生循環引用的條件(如:Rc<RefCell<T>>):

  1. 使用引用計數指針
  2. 內部可變性

設計遞歸型數據結構例子:

//遞歸型數據結構,需要使用引用
//引用是間接結構,否則該遞歸數據結構有無限大小
//其次,引用需要明確初始化,引用遞歸自身導致無法初始化
//因此,需要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;
}

容器生成三種迭代器:

  1. ·iter() 創造一個Item是&T類型的迭代器;
  2. ·iter_mut() 創造一個Item是&mut T類型的迭代器;
  3. ·into_iter() 根據調用的類型來創造對應的元素類型的迭代器。
    1. T => R 容器為T,返回元素為R,即move
    2. &T => &R
    3. &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(); //等待子線程結束

數據競爭三個條件:

  1. 數據共享
  2. 數據修改
  3. 沒有同步( rust 編譯器保證同步)

數據同步框架:

  • Sync 訪問安全
    1. rust引用機制保證了數據訪問基本是安全的
    2. 內部可變的類型除外(用了不安全代碼)
    3. lock()的內部可變類型也是安全的
  • Send 移動安全
    1. 沒有引用成員的類型;
    2. 泛型元素T是Send的;
    3. 或被lock()包裝的。

多線程下的數據總結:

  1. T :移動(或復制),因而無法共享(也就是只能給一個線程使用)
  2. &T
    1. 指向局部變量,生命周期報錯
    2. 指向static T,ok
  3. &mut T
    1. 同理
    2. 指向static mut T, 不安全報錯
  4. Box<T>:普通智能指針沒有共享功能,等價T
  5. Rc<T>:普通共享指針沒有Send特征,技術實現使用了內部可變,但沒有加線程鎖進行安全處理
  6. Arc<T> 提供了線程安全的共享指針
  7. Mutex<T>提供了線程安全的可寫能力。
    1. RwLock
    2. AtomicIsize
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。

那為什么要用模式匹配來定義變量?因為很靈活,看一下它的表達能力:

  1. 普通類型的匹配:x --> T
  2. 解構引用:&x --> &T 得 x=T
  3. 解構數組:[_,_,x,_,_] --> [T;5] 得 x= 第三個元素
  4. 解構元組:(..,x) --> (1,2,"x") 得 x= 第三個成員
  5. 解構結構:T{0:_,1:x} --> T(1,"x") 得 x= 第二個成員

通過與目標類型差不多的寫法,對復雜類型進行解構,相對直觀。

另一方面,用於判斷的模式匹配可以針對動態的內容,而不只是類型來進行匹配,如:

  1. 值范圍匹配:x@1...10 --> 7 得 x=7
  2. 切片屬於值匹配: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)};


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM