久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

從C 轉(zhuǎn)向最受歡迎的Rust需要注意哪些問題,?

 新用戶0118F7lQ 2022-04-06

在日常開發(fā)過程中,,若長期使用C++語言,在初次使用Rust的過程中可能會碰到一些問題,。本文嘗試從C++的角度來說明在使用Rust時需要特別注意的一些地方,,特別是其中的思維方式的轉(zhuǎn)變(mind shift)。


 
1 
賦值的move語義

C++ vs Rust

C++的賦值操作是copy語義,,在不考慮優(yōu)化的情況下,,從語義的角度理解,賦值后內(nèi)存中的某個對象即變成了兩份,。修改新的對象并不會對舊對象產(chǎn)生副作用,。

而Rust對賦值操作有更加精細的控制,以下兩條:

  • 對于所有實現(xiàn)了Copy trait的類型來說,,賦值采用了copy語義,。

  • 對于其它情況,,采用move語義。

在Rust中直接使用編譯器來保證了move語義,,確保變量的值被移出后,,不能被再使用,如下例:

fn main() {
    let mut x = 5;
    let rx0 = &mut x;
    let rx1 = rx0;
    println!('test {}', rx0);
}

會產(chǎn)生編譯錯誤:

error[E0382]: borrow of moved value: `rx0`
 --> src/main.rs:5:25
  |
3 |     let rx0 = &mut x;
  |         --- move occurs because `rx0` has type `&mut i32`, which does not implement the `Copy` trait
4 |     let rx1 = rx0;
  |               --- value moved here
5 |     println!('test {}', rx0);
  |                         ^^^ value borrowed here after move

明確地說明了原因:變量在移動后又被使用了,,在哪兒被使用,,以及為什么采用了move語義。

而在C++中,,可以通過禁用class的拷貝構(gòu)造函數(shù)來達到禁止變量復制的目的,。如以下代碼是編譯不通過的:

#include <memory>

using namespace std;

int main(int argc, const char* argv[]) {
    auto int_p0 = unique_ptr<int>(new int);
    auto int_p1 = int_p0;
    *int_p0 = 5;
    return 0;
}

在clang++中會產(chǎn)生如下錯誤:

main.cc:8:10: error: call to implicitly-deleted copy constructor of 'std::__1::unique_ptr<int, std::__1::default_delete<int> >'
    auto int_p1 = int_p0;
         ^        ~~~~~~
/opt/llvm/clang-10.0.1/bin/../include/c++/v1/memory:2513:3: note: copy constructor is implicitly deleted because 'unique_ptr<int, std::__1::default_delete<int> >' has a user-declared move constructor
  unique_ptr(unique_ptr&& __u) _NOEXCEPT
  ^

但是只需要將錯誤行改成如下代碼即可以編譯通過:

auto int_p1 = std::move(int_p0);

但后果是,程序在對*int_p0進行賦值時會產(chǎn)生coredump,。這也是Rust所謂的內(nèi)存安全性,,即只要沒有使用unsafe,編譯器可以發(fā)現(xiàn)內(nèi)存的錯誤訪問,,并拒絕通過編譯,。

用&T與可變引用&mut T

還是上面的例子,如果將其中的可變引用改成非可變引用(默認形式的引用),,如下代碼:

fn main() {
    let x = 5;
    let rx0 = &x;
    let rx1 = rx0;
    println!('test {}', rx0);
}

可以通過編譯,。Rust的文檔中有如下說明:

The following traits are implemented for all &T, regardless of the type of its referent:

Copy
Clone (Note that this will not defer to T’s Clone implementation if it exists!)
Deref
Borrow
Pointer

*&mut T references get all of the above except Copy and Clone
(to prevent creating multiple simultaneous mutable borrows),
plus the following, regardless of the type of its referent:

DerefMut
BorrowMut

&mut T相較于&T少實現(xiàn)了Copy和Clone。因此,,對于可變引用&mut T來說,,賦值采用的是move語義,而對于普通引用&T來說采用的是copy語義,,所以改成普通引用上面的程序就可以編譯通過了,。

這也是為什么可變引用也被稱之為獨占引用,因為每次對可變引用的賦值,,都意味著舊變量的失效,,這就確保了全局只會存在一份可變引用。

Rust在這里體現(xiàn)了語言設(shè)計的優(yōu)雅:賦值操作的語義委托到了類型系統(tǒng),,通過定義基本的機制同時約束了自定義類型與內(nèi)建類型的行為,,在編譯期完成檢查,而不是需要開發(fā)去記憶各種特例,。這在不了解語言的時候會產(chǎn)生學習曲線,,但是一旦了解了其套路后(Thinking in Rust), 可以顯著地降低編碼過程中的心智負擔,。


 
2 
Option與空指針

enum與match

在C++中,,對于可能存在或不存在的變量,慣常的作法之一是傳入指針 (包括現(xiàn)代C++中智能指針shared_ptr和unique_ptr),,在處理時,,通過檢查指針是否為空來判斷變量是否存在,。這是一種非常便利的做法,但是同樣的,,此方案在編譯期無法做更多的檢查,,最終檢查的責任交給了開發(fā)。

Rust對此問題主要使用了兩個機制:枚舉(enum)模式匹配(match),。相比較C++的enum, Rust的enum更像是C++的union,。是 ADT(algebraic data type)中sum types(tagged union)在Rust中的實現(xiàn)。在Rust中enum可能包括一組類型中的一個,,如:

enum Message {
  Quit,
  Move {x: i32, y: i32},
  Write (String),
}

上面代碼表示,,一條消息(Message)可能有三種類型:Quit、Move和Write,。當類型為Move或者Write時,,還可以帶上自己的特定的數(shù)據(jù)。當處理Message時,,則會使用模式匹配機制取得具體類型進行處理:

match message {
  Message::Quit => todo!(),
  Message::Move { x, y } => todo!(),
  Message::Write(info) => todo!(),
}

為了避免在修改了enum的定義后,,忘記在match中添加相應(yīng)的處理,match會在編譯期要求分支必須覆蓋全部可能的情況,。如在Message中新加入一項:

enum Message {
  Quit,
  Move {x: i32, y: i32},
  Write (String),
  Send (String), // 新加入
}

再編譯時會出現(xiàn)以下錯誤,,提示開發(fā)將Send的處理加入match。

 --> src/main.rs:9:11
  |
1 | / enum Message {
2 | |     Quit,
3 | |     Move { x: i32, y: i32 },
4 | |     Write(String),
5 | |     Send(String),
  | |     ---- not covered
6 | | }
  | |_- `Message` defined here
...
9 |       match message {
  |             ^^^^^^^ pattern `Send(_)` not covered
  |
  = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms
  = note: the matched value is of type `Message`

由此可見,,在C++中,與其最相似的類型其實是C++17的std::variant,,而match機制類似于std::visit,。但Rust在這里做得更完善一些,體現(xiàn)在:

  • 相同的子類型可以因為Tag的不同出現(xiàn)多次,,如上面的Write和Send,,子類型都是String。這是std::variant無法直接做到的,,除非再封裝一個結(jié)構(gòu),。

  • match會要求分支覆蓋enum所有變體,std::visit也會在編譯期檢查完整的類型覆蓋,,但其中類型會考慮C++的隱式類型轉(zhuǎn)換,,使用時需要小心。

Option

有了上面的預備知識,,現(xiàn)在就可以來了解在Rust中是如何處理空懸指針的問題,。先看一下Option的定義:

pub enum Option<T> {
    /// No value
    None,
    /// Some value `T`
    Some(T),
}

在Rust中,對于可選的情景,,會定義為該變量類型的Option,。假設(shè)某函數(shù)提供從磁盤讀取某個token,,該token可能存在或者不存在,那么該函數(shù)的定義會是:

struct Token { /*...*/ };

fn load_token() -> Option<Token>;

在使用的時候會采用如下代碼:

let token = load_token(); // 此時 token 的類型是 Option<Token>

match token {
  Some(token) => {
    // 注意這里的 token 是由 Some(token) 這個 pattern 匹配出來的
    // 已經(jīng)覆蓋了最外層的 token. 此時 token 的類型是 Token, 已經(jīng)
    // 確保存在,。
    todo!()
  },
  None => todo!()
}

可以看到,,對于返回Option<T>的情形,無法直接將Option<T>當作T來處理,,只能使用模式匹配機制(match,,if let,while let等),,將T提取出來處理,。這一步強制的機制,確保了對可為空的變量進行檢查,,避免了對空懸指針的意外訪問,。

相較于使用指針來表達可選情形,Option<T>的表達力會更豐富一些,,因為沒有強制將T轉(zhuǎn)成T*,,保留了移動優(yōu)化的可能性;同時,,使用專門的類型來表達可選,,在語義上也理加精確一些。

了解Haskell的同學可以發(fā)現(xiàn),,Option與Maybe如出一轍,。事實上,Rust的類型系統(tǒng),,很大程度地受到了Haskell的影響,,所以很多地方可以看到Haskell的影子也并不奇怪。學習Haskell對理解Rust也會很有幫助,。

最后說明一下,,在C++17中加入的std::optional實現(xiàn)了類似的功能。從接口上說還是像智能指針,,使用前需要判斷,,否則對std::nullopt進行dereference還是會產(chǎn)生運行時故障。


 
3 
迭代器Iterator

Iterator在Rust中的地位

Iterator是Rust相對獨特的功能,。對于Rust來說,,采用如下的方式去遍歷數(shù)組是低效的:

let data = vec![1,2,3,4,5];

for i in 0..data.len() {
  println!('{}', data[i]);
}

因為向安全性的妥協(xié),每次data[i]的操作都會進行邊界檢查,,顯然這種檢查是不必要的,,在性能敏感的場景中也是不可接受的。因此,在Rust中推薦的做法是:

for v in data {
  println!('{}', v);
}

使用迭代器的形式避免了最終取值時的再一次邊界檢查,,同時也更加簡潔,。由此可見,以地道的Rust風格來說,,遍歷數(shù)組應(yīng)該使用迭代器來完成,,而不是通過遍歷下標來進行索引。

對于現(xiàn)代C++(C++11)來說,,也提供了類似的語法方式進行容器遍歷:

for (auto&& v: data) {
  // do something for v
}

取得迭代器的三種形式

對于可以迭代的對象,,以std::vec::Vec為例,通常會提供三種方式取得迭代器,,如下:

  • iter():取得元素的引用,,即&T,非消耗性,。

  • iter_mut():取得元素的可變引用,,即&mut T,非消耗性,。

  • into_iter():取得元素的所有權(quán),,即T,消耗性,。

這里消耗性指的是在迭代完成之后,,原來的容器是否還可以繼續(xù)使用。對于into_iter()來說,,在迭代過程中已經(jīng)將容器中的所有元素所有權(quán)全部取得,,所以最終容器不再持有任何對象,也同時被drop,。因此稱之為消耗性的,。

IntoIterator

對于一般的迭代形式:

for x in data {}

Rust期望data是一個實現(xiàn)了Iterator的對象。否則,,會嘗試使用IntoIterator將data轉(zhuǎn)換成Iterator對象。所以對于data: Vec<i32>來說,,實際展開成了如下代碼:

for x in IntoIterator::into_iter(data) { }

這里for ... in語句使用IntoIterator::into_iter獲取了目標對象的迭代器,。因此,凡是實現(xiàn)了IntoIterator的類型均可以使用for ... in語句進行迭代,。

以std::vec::Vec為例,,分別為Vec<T>、& Vec<T>和&mut Vec<T>實現(xiàn)了IntoIterator,,并分別代理到into_iter(),、iter() 和 iter_mut(),以應(yīng)對上面所說的三種不同迭代形式。如下例(清晰起見,,將類型注解加上了):

let mut data: Vec<i32> = Vec::from([1,2,3,4]);
// 取得引用
for v: &i32 in &data {}
// 取得可變引用
for v: &mut i32 in &mut data {}
// 取得所有權(quán)
for v: i32 in data {}

鏈式調(diào)用

在Rust的設(shè)計中,,利用Adapter可以靈活而高效地通過Iterator來處理集合。

Adapter在Rust中指的是一類函數(shù),,它們接收一個Iterator并且返回一個Iterator,。這樣的接口規(guī)范使用可以通過鏈式調(diào)用的方式組合多個Adapter完成復雜的功能。常見的Adapter包括:map,、filter以及filter_map等等,。

除了Adapter,Rust也提供其它一些函數(shù)用于迭代器的最終處理,。比如:

  • count,,用于計算元素的個數(shù)。

  • collect,,用于收集迭代器中的元素到某個實現(xiàn)了FromIterator的類型中去,,比如Vec、VecDeque和String等等,。

  • reduce,,使用某個函數(shù)對集合進行規(guī)約。類似地,,也可以使用fold進行有初值的規(guī)約,。

可以看到,針對迭代器,,Rust提供了豐富的函數(shù)對其處理,,具體可以參考文檔,。此種編碼風格,與舊風格的C++很不一樣,轉(zhuǎn)到Rust后在需要對集合進行循環(huán)處理的場合,,可以有意識地想想,能不能將邏輯寫成迭代器的形式,,通??梢缘玫礁雍啙嵉拇a,同時,,如前面所說,,也可能獲得性能更高的代碼。

最后提一下,,C++社區(qū)也在積極的采納此種代碼風格,,在C++20中,已經(jīng)將ranges加入標準,。其中提供的Range adaptors與Rust的Adapter的概念基本是一樣的,。如C++的樣例代碼:

  auto const ints = {0,1,2,3,4,5};
  auto even = [](int i) { return 0 == i % 2; };
  auto square = [](int i) { return i * i; };

  // 'pipe' syntax of composing the views:
  for (int i : ints | std::views::filter(even) | std::views::transform(square)) {
    std::cout << i << ' ';
  }

寫成Rust則是:

  let ints = vec![0, 1, 2, 3, 4, 5];
  let even = |i: &i32| 0 == *i % 2;
  let square = |i: i32| i * i;

  for i in ints.into_iter().filter(even).map(square) {
    println!('{}', i);
  }


 
4 
惰性求值—Laziness

最后需要提一下的是,對于使用鏈式調(diào)用的方式將各種Adapter組合的Iterator,其求值是惰性的,。即,,當寫下如下代碼時:

let v = vec![0,1,2,3,4,5];
v.iter().map(|i| println!('{}', i));

其實并不會去調(diào)用println將數(shù)據(jù)輸出。Rust文檔的原文是:

This means that just creating an iterator doesn’t do a whole lot.

Nothing really happens until you call next

即,,只有調(diào)用迭代器的next方法,,才會依次觸發(fā)各級Iterator的求值。這樣做的好處是:

性能

考慮如下代碼:

let v = vec![0,1,2,3,4,5,6,7,8,9];
let even = |i: &i32| 0 == *i % 2;
let square = |i: i32| i * i;
v.into_iter().filter(even).map(square).take(2);

如果是eager evaluation,,前兩個Adapter,,filter(even)和map(square)會分別先執(zhí)行10次和5次,最后才是take(2)取到最開始的兩個元素,。如果這個數(shù)組的長度不是10,,而100萬,那么這里浪費的空間和時間將會是巨大的,。同時也會影響響應(yīng)時間,,因為只有前面兩步都處理完畢之后,才會進行到最后一步,。

而采用lazy evaluation時,,執(zhí)行會由take(2).next()傳導到map(square)再到filter(even), 最終不論數(shù)組的長度是多少,都只會調(diào)用filter(even)3次,,map(square)2次,。沒有產(chǎn)生額外的開銷。

無限迭代

惰性求值的另一個好處是,,使得無限迭代器成為了可能,。考慮如下代碼:

let number = 1..; // 這是一個無限迭代器
for n in number.filter(even).take(5) {
  println!('{}', n)
}

不會因為filter(even)的調(diào)用而陷入死循環(huán),。而是按需取用,。使用此種方法,可以使用遞推公式實現(xiàn)數(shù)列的迭代器, 并支持各種Adapter的組合:

pub struct Fib {
  n0: u64,
  n1: u64,
}

impl Default for Fib {
  fn default() -> Self {
    Self { n0: 0, n1: 1 }
  }
}

impl Iterator for Fib {
  type Item = u64;

  fn next(&mut self) -> Option<Self::Item> {
    let n = self.n0 + self.n1;
    self.n0 = self.n1;
    self.n1 = n;
    Some(self.n0)
  }
}

fn main() {
  let fib = Fib::default();
  let square = |i: u64| i * i;
  for n in fib.map(square).take(10) {
    println!('{}', n);
  }
}


 
5 
總結(jié)

本文主要是記錄自己從C++轉(zhuǎn)向Rust碰到的一些問題,,特別是記錄兩種語言在處理程序設(shè)計中基礎(chǔ)問題的不同套路,。這一篇主要介紹了三個主題:move語義、Option和Iterator,。由于筆者寫的Rust也不多,,所以其中必然會有很多錯誤與不足,發(fā)出來與大家交流,,希望大家包涵并不吝指教。

之后也會以同樣的形式介紹其它主題,,比如當前心里還想著要記錄的有:錯誤處理,、生命周期&借用、interior mutability等。接下來自己爭取將后面的系列完成,。

本文源自公眾號云加社區(qū),,分布式實驗室已獲完整授權(quán)。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式,、誘導購買等信息,,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,,請點擊一鍵舉報,。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多