<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          前端視角解讀 Why Rust

          共 29175字,需瀏覽 59分鐘

           ·

          2022-11-25 06:32


          大廠技術(shù)  高級(jí)前端  精選文章

          點(diǎn)擊上方 全站前端精選,關(guān)注公眾號(hào)

          回復(fù)1,加入高級(jí)前段交流群

          為什么要學(xué) Rust

          因?yàn)槲覀冃枰褂煤线m的工具解決合適的問(wèn)題

          目前 Rust 對(duì) WebAssembly 的支持是最好的,對(duì)于前端開(kāi)發(fā)來(lái)說(shuō),可以將 CPU 密集型的 JavaScript 邏輯用 Rust 重寫(xiě),然后再用 WebAssembly 來(lái)運(yùn)行,JavaScript 和 Rust 的結(jié)合將會(huì)讓你獲得駕馭一切的力量。

          但是 Rust 被公認(rèn)是很難學(xué)的語(yǔ)言,學(xué)習(xí)曲線很陡峭。(學(xué)不動(dòng)了

          對(duì)于前端而言,所需要經(jīng)歷的思維轉(zhuǎn)變會(huì)比其他語(yǔ)言更多。從命令式(imperative)編程語(yǔ)言轉(zhuǎn)換到函數(shù)式(functional)編程語(yǔ)言、從變量的可變性(mutable)遷移到不可變性(immutable)、從弱類(lèi)型語(yǔ)言遷移到強(qiáng)類(lèi)型語(yǔ)言,以及從手工或者自動(dòng)內(nèi)存管理到通過(guò)生命周期來(lái)管理內(nèi)存,難度逐級(jí)遞增。

          而當(dāng)我們邁過(guò)了這些思維轉(zhuǎn)變后,會(huì)發(fā)現(xiàn) Rust 的確有過(guò)人之處:

          • 從內(nèi)核來(lái)看,它重塑了我們對(duì)一些基本概念的理解。比如 Rust 清晰地定義了變量在一個(gè)作用域下的生命周期,讓開(kāi)發(fā)者在摒棄垃圾回收(GC)前提下,還能夠無(wú)需關(guān)心手動(dòng)內(nèi)存管理,讓內(nèi)存安全和高性能二者兼得
          • 從外觀來(lái)看,它使用起來(lái)感覺(jué)很像 Python/TypeScript 這樣的高級(jí)語(yǔ)言,表達(dá)能力一流,但性能絲毫不輸于 C/C++,從而讓表達(dá)力和高性能二者兼得。

          • 擁有友好的編譯器和清晰明確的錯(cuò)誤提示與完整的文檔,基本可以做到只要編譯通過(guò),即可上線。

          大概了解這些后,那我們開(kāi)始從幾個(gè)簡(jiǎn)單的 Rust demo 開(kāi)始吧~

          Ps:這篇文章并不能帶你直接掌握或者入門(mén) Rust,并不會(huì)涉及到過(guò)多 api 講解,如有需求可直接跳轉(zhuǎn)文末參考資料。

          Rust 初體驗(yàn)

          可使用 Rust Playground[1] 快速體驗(yàn)

          Hello World

          // main()函數(shù)在獨(dú)立可執(zhí)行文件中是不可或缺的,是程序的入口
          fn main() {
              // 創(chuàng)建String類(lèi)型的字符串字面量,使用 let 創(chuàng)建的默認(rèn)是不可變的
              let target = String::from("rust");
              // println!()是一個(gè)宏,用于將參數(shù)輸出到 STDOUT
              println!("Hello World: {}", target);
          }

          函數(shù)抽象示例

          fn apply(value: i32, f: fn(i32) -> i32) -> i32 {
              f(value)
          }

          // 入?yún)⒑头祷仡?lèi)型為i32(有符號(hào),大小在[-2^31, 2^31 - 1]范圍內(nèi)的數(shù)字類(lèi)型)
          fn square(value: i32) -> i32 {
              // 沒(méi)有寫(xiě);代表直接返回,相當(dāng)于 return value * value;
              value * value
          }

          fn cube(value: i32) -> i32 {
              value * value * value
          }

          fn main() {
              // js中相當(dāng)于console.log(`apply square: ${apply(2, square)}`)
              println!("apply square: {}", apply(2, square));
              println!("apply cube: {}", apply(2, cube));
          }

          控制流與枚舉

          // 4種硬幣的值都屬于 Coin 類(lèi)型
          enum Coin {
              Penny,
              Nickel,
              Dime,
              Quarter,
          }

          fn value_in_cents(coin: Coin) -> u8 {
              // 使用 match 進(jìn)行類(lèi)型匹配
              match coin {
                  Coin::Penny => 1,
                  Coin::Nickel => 5,
                  Coin::Dime => 10,
                  Coin::Quarter => 25,
              }
          }

          先聊聊堆和棧

          我們?cè)趯?xiě) js 的時(shí)候,似乎不需要特別關(guān)注堆和棧以及內(nèi)存的分配,js 會(huì)幫忙我們“自動(dòng)”搞定一切。但這個(gè)“自動(dòng)”正是一切混亂的根源,讓我們錯(cuò)誤的感覺(jué)我們可以不關(guān)心內(nèi)存管理。

          我們重新回過(guò)來(lái)看一看這些基礎(chǔ)知識(shí),以及 Rust 是怎么處理內(nèi)存管理的。

          ??臻g

          棧的特點(diǎn)是 “LIFO,即后進(jìn)先出” 。數(shù)據(jù)存儲(chǔ)時(shí)只能從頂部逐個(gè)存入,取出時(shí)也需從頂部逐個(gè)取出。比如一個(gè)乒乓球的盒子,先放進(jìn)去(入棧)的乒乓球就只能后出來(lái)(出棧)。

          在每次調(diào)用函數(shù),都會(huì)在棧的頂端創(chuàng)建一個(gè)棧幀,用來(lái)保存該函數(shù)的上下文數(shù)據(jù)。比如該函數(shù)內(nèi)部聲明的局部變量通常會(huì)保存在棧幀中。當(dāng)該函數(shù)返回時(shí),函數(shù)返回值也保留在該棧幀中。當(dāng)函數(shù)調(diào)用者從棧幀中取得該函數(shù)返回值后,該棧幀被釋放。

          堆空間

          不同于??臻g由操作系統(tǒng)跟蹤管理,堆的特點(diǎn)是 無(wú)序key-value 鍵值對(duì) 存儲(chǔ)方式。

          堆是在程序運(yùn)行時(shí),而不是在程序編譯時(shí),申請(qǐng)某個(gè)大小的內(nèi)存空間。即動(dòng)態(tài)分配內(nèi)存,對(duì)其訪問(wèn)和對(duì)一般內(nèi)存的訪問(wèn)沒(méi)有區(qū)別。對(duì)于堆,我們可以隨心所欲的進(jìn)行增加變量和刪除變量,不用遵循次序。

          可以這么總結(jié):

          1. 棧適合存放存活時(shí)間短的數(shù)據(jù)。
          2. 數(shù)據(jù)要存放于棧中,要求數(shù)據(jù)所屬數(shù)據(jù)類(lèi)型的大小是已知的。
          3. 使用棧的效率要高于使用堆。

          對(duì)于存入棧上的值,它的大小在編譯期就需要確定。棧上存儲(chǔ)的變量生命周期在當(dāng)前調(diào)用棧的作用域內(nèi),無(wú)法跨調(diào)用棧引用。

          堆可以存入大小未知或者動(dòng)態(tài)伸縮的數(shù)據(jù)類(lèi)型。堆上存儲(chǔ)的變量,其生命周期從分配后開(kāi)始,一直到釋放時(shí)才結(jié)束,因此堆上的變量允許在多個(gè)調(diào)用棧之間引用。

          可以將棧理解為將物品放進(jìn)大小合適的紙箱并將紙箱按規(guī)律放進(jìn)儲(chǔ)物間,堆理解為在儲(chǔ)物間隨便找一個(gè)空位置來(lái)放置物品。顯然,以紙箱為單位來(lái)存取物品的效率要高的多,而直接將物品放進(jìn)凌亂的儲(chǔ)物間的效率要低的多,而且儲(chǔ)物間隨意堆放的東西越多,空閑位置就越零碎,存取物品的效率就越低,且空間利用率就越低。

          Rust 如何使用堆和棧

          問(wèn)題來(lái)了,我們先看看 JavaScript 是如何使用堆和棧的

          JavaScript 中的內(nèi)存也分為棧內(nèi)存和堆內(nèi)存。一般來(lái)說(shuō):

          • 棧內(nèi)存中存放的是存儲(chǔ)對(duì)象的地址;
          • 堆內(nèi)存中存放的是存儲(chǔ)對(duì)象的具體內(nèi)容。

          • 對(duì)于原始類(lèi)型的值而言,其地址和具體內(nèi)容都存在于棧內(nèi)存中;

          • 基于引用類(lèi)型的值,其地址存在棧內(nèi)存,其具體內(nèi)容存在堆內(nèi)存中。

          Rust 中各種類(lèi)型的值默認(rèn)都存儲(chǔ)在棧中,除非顯式地使用Box::new()將它們存放在堆上。對(duì)于動(dòng)態(tài)大小的類(lèi)型 (如 Vec、String),則數(shù)據(jù)部分分布在堆中,并在棧中留下胖指針指向?qū)嶋H的數(shù)據(jù),棧中的那個(gè)胖指針結(jié)構(gòu)是靜態(tài)大小的。

          在堆與棧的使用中,各個(gè)語(yǔ)言看起來(lái)是差不多的,主要區(qū)別在于 GC 上。

          在 JavaScript 的 GC 中,因?yàn)闆](méi)有一些高級(jí)語(yǔ)言所擁有的垃圾回收器,js 自動(dòng)尋找是否一些內(nèi)存“不再需要”是很難判定的。因此,js 的垃圾回收實(shí)現(xiàn)只能有限制的解決一般問(wèn)題。

          比如現(xiàn)在對(duì)于引用的垃圾回收,使用的標(biāo)記-清除算法[2],仍然會(huì)存在那些無(wú)法從根對(duì)象查詢(xún)到的對(duì)象都將被清除的限制(盡管這是一個(gè)限制,但實(shí)踐中我們很少會(huì)碰到類(lèi)似的情況,所以開(kāi)發(fā)者不太會(huì)去關(guān)心垃圾回收機(jī)制)。

          而 Rust 不同于其他的高級(jí)語(yǔ)言,它沒(méi)有提供 GC,也無(wú)需手動(dòng)申請(qǐng)和手動(dòng)釋放堆內(nèi)存,但 Rust 可以保證我們當(dāng)前的內(nèi)存是安全的,即不會(huì)出現(xiàn)懸空指針等問(wèn)題。其中一個(gè)原因是因?yàn)?Rust 使用了自己的一套內(nèi)存管理機(jī)制:Rust 中所有的大括號(hào)都是一個(gè)獨(dú)立的作用域,作用域內(nèi)的變量在離開(kāi)作用域時(shí)會(huì)失效,而變量綁定的數(shù)據(jù)(無(wú)論是堆內(nèi)還是棧中數(shù)據(jù))則自動(dòng)被釋放。

          fn main() {
              // 每個(gè)大括號(hào)都是獨(dú)立的作用域
              {
                  let n = 33;
                  println!("{}", n);
              }
              // 變量n在這個(gè)時(shí)候失效
              // println!("{}", n);  // 編譯錯(cuò)誤
          }

          那如果碰到這種情況呢:

          fn main() {
              let v = vec![123];
              println!("{}", v[0]);
          }

          v 變量本身分配在棧中,用一個(gè)胖指針指向了堆中 v 里的三個(gè)元素。當(dāng)函數(shù)退出后,v 的作用域結(jié)束了,它所引用的堆中的元素也會(huì)被自動(dòng)回收,聽(tīng)起來(lái)不錯(cuò)。

          但問(wèn)題來(lái)了,如果想要將 v 的值綁定在另一個(gè)變量 v2 上,會(huì)出現(xiàn)什么情況呢?

          對(duì)于有 GC 的系統(tǒng)來(lái)說(shuō),這不是問(wèn)題,vv2 都引用同一個(gè)堆中的引用,最終由 GC 來(lái)回收就是了。

          對(duì)于沒(méi)有 GC 的 Rust 而言,自然有它的辦法,那就是所有權(quán)特性中的 move 語(yǔ)義,這個(gè)我們?cè)诤竺鏁?huì)講到。

          Rust 語(yǔ)言特性

          所有權(quán)和生命周期的存在使 Rust 成為內(nèi)存安全、沒(méi)有 GC 的高效語(yǔ)言。

          所有權(quán):掌控值的生死大權(quán)

          計(jì)算機(jī)的內(nèi)存資源非常寶貴,所有的程序運(yùn)行的時(shí)候都需要某種方式來(lái)合理地利用計(jì)算機(jī)的內(nèi)存資源,我們?cè)倏匆幌鲁R?jiàn)的幾種語(yǔ)言是如何利用內(nèi)存的:

          語(yǔ)言

          內(nèi)存使用方案

          Java、Go

          垃圾回收機(jī)制,不停地查看一些內(nèi)存是否沒(méi)有在使用了,如果不再需要就將其釋放,占用更多的內(nèi)存和CPU資源

          C、C++

          程序員自己手動(dòng)申請(qǐng)和釋放內(nèi)存,容易出錯(cuò)且難以排查

          JavaScript

          在創(chuàng)建變量(對(duì)象,字符串等)時(shí)自動(dòng)進(jìn)行了分配內(nèi)存,并且在不使用它們時(shí)“自動(dòng)”釋放。這個(gè)“自動(dòng)”是混亂的根源,并讓 JavaScript(和其他高級(jí)語(yǔ)言)開(kāi)發(fā)者錯(cuò)誤的感覺(jué)他們可以不關(guān)心內(nèi)存管理。

          Rust

          所有權(quán)機(jī)制,內(nèi)存由所有權(quán)系統(tǒng)根據(jù)一系列的規(guī)則來(lái)管理,這些規(guī)則只會(huì)在程序編譯期間檢查

          Rust 的流行和受歡迎是因?yàn)樗梢栽诓皇褂美占耐瑫r(shí)保證內(nèi)存安全。而其它諸如 JavaScript、Go 等語(yǔ)言則是使用垃圾收集來(lái)做內(nèi)存管理,垃圾收集器以資源和性能為代價(jià)為開(kāi)發(fā)人員提供了方便,但是一旦碰到問(wèn)題,就會(huì)很難排查。在 rust 世界里,當(dāng)你嚴(yán)格遵循規(guī)則的時(shí)候,就可以拋開(kāi)垃圾收集實(shí)現(xiàn)內(nèi)存安全。

          我們先從一個(gè)變量使用堆棧的行為開(kāi)始,探究 Rust 設(shè)計(jì)所有權(quán)和生命周期的用意。

          變量在函數(shù)調(diào)用時(shí)發(fā)生了什么

          我們先來(lái)看一段代碼:

          fn main() {
              // 定義一個(gè)動(dòng)態(tài)數(shù)組
              let data = vec![104298];
              let v = 42;
              // 使用 if let 進(jìn)行模式匹配。
              // 它和直接 if 判斷的區(qū)別是 if 匹配的是布爾值而 if let 匹配的是模式
              if let Some(pos) = find_pos(data, v) {
                  println!("Found {} at {}", v, pos);
              }
          }

          // 在 data 中查找 v 是否存在,存在則返回 v 在 data 中的下標(biāo),不存在返回 None
          fn find_pos(data: Vec<u32>, v: u32) -> Option<usize> {
              for (pos, item) in data.iter().enumerate() {
                  // 解除 item 的引用,可以訪問(wèn)到 item 的具體值
                  if *item == v {
                      return Some(pos);
                  }
              }

              None
          }

          Option 是 Rust 的系統(tǒng)類(lèi)型,它是一個(gè)枚舉,包含了 SomeNone,用來(lái)表示值不存在的可能,這在編程中是一個(gè)好的實(shí)踐,它強(qiáng)制 Rust 檢測(cè)和處理值不存在的情況。

          這段代碼不難理解,要再?gòu)?qiáng)調(diào)一下的是,動(dòng)態(tài)數(shù)組因?yàn)榇笮≡诰幾g期無(wú)法確定,所以放在堆上,并且在棧上有一個(gè)包含了長(zhǎng)度和容量的胖指針指向堆上的內(nèi)存。

          在調(diào)用 find_pos() 時(shí),main() 函數(shù)中的局部變量 data 和 v 作為參數(shù)傳遞給了 find_pos(),所以它們會(huì)被放在 find_pos() 的參數(shù)區(qū)。

          按照大多數(shù)編程語(yǔ)言的做法,現(xiàn)在堆上的內(nèi)存就有了兩個(gè)引用。不光如此,我們每把 data 作為參數(shù)傳遞一次,堆上的內(nèi)存就會(huì)多一次引用。

          但是,這些引用究竟會(huì)做什么操作,我們不得而知,也無(wú)從限制;而且堆上的內(nèi)存究竟什么時(shí)候能釋放,尤其在多個(gè)調(diào)用棧引用時(shí),很難厘清,取決于最后一個(gè)引用什么時(shí)候結(jié)束。所以,這樣一個(gè)看似簡(jiǎn)單的函數(shù)調(diào)用,給內(nèi)存管理帶來(lái)了極大麻煩。

          所有權(quán)和 Move 語(yǔ)義

          在 Rust 的所有權(quán)規(guī)則下,上述的問(wèn)題將不再是問(wèn)題,所有權(quán)規(guī)則可以總結(jié):

          • 一個(gè)值只能被一個(gè)變量所擁有,這個(gè)變量被稱(chēng)為所有者。
          • 一個(gè)值同一時(shí)刻只能有一個(gè)所有者。

          • 當(dāng)所有者離開(kāi)作用域,其擁有的值被丟棄,內(nèi)存得到釋放。

          在所有權(quán)的規(guī)則下,我們看一下上述的引用問(wèn)題是如何解決的:

          原先 main() 函數(shù)中的 data,被移動(dòng)到 find_pos() 后,就失效了,編譯器會(huì)保證 main() 函數(shù)隨后的代碼無(wú)法訪問(wèn)這個(gè)變量,這樣,就確保了堆上的內(nèi)存依舊只有唯一的引用。

          但為什么 v 沒(méi)有被移動(dòng)反而依舊被復(fù)制了呢?一會(huì)你就明白了。

          所以在所有權(quán)規(guī)則下,解決了誰(shuí)真正擁有值的生死大權(quán)問(wèn)題,讓堆上數(shù)據(jù)的多重引用不復(fù)存在,這是它最大的優(yōu)勢(shì)。

          但是很明顯它也產(chǎn)生了一些問(wèn)題,最大的一個(gè)就是會(huì)讓代碼變得很復(fù)雜,尤其是一些只存儲(chǔ)在棧上的簡(jiǎn)單數(shù)據(jù),如果要避免所有權(quán)轉(zhuǎn)移之后不能訪問(wèn)的情況,我們就需要調(diào)用 clone() 來(lái)進(jìn)行復(fù)制,這樣效率也不會(huì)很高。

          Rust 考慮到了這一點(diǎn),所以在 Move 語(yǔ)義之外,Rust 還提供了 Copy 語(yǔ)義。如果一個(gè)數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)了 Copy trait[3],那么它就會(huì)使用 Copy 語(yǔ)義。這樣,在你賦值或者傳參時(shí),值會(huì)自動(dòng)按位拷貝(淺拷貝)。

          #[derive(Debug)]
          struct Foo;

          let x = Foo;
          let y = x; // unused variable: `y`

          // `x` has moved into `y`, and so cannot be used
          println!("{:?}", x); // error: use of moved value
          #[derive(Debug, Copy, Clone)]
          struct Foo;

          let x = Foo;
          // 變量命名前加_代表這個(gè)變量處于 todo 狀態(tài),編譯器會(huì)忽視 unused 檢查
          let _y = x;

          // `y` is a copy of `x`
          println!("{:?}", x); // A-OK!

          struct:可以視為 es6 中的 class,但建議還是將 struct 視作是純粹的數(shù)據(jù)。

          trait:類(lèi)似于接口,特性與接口相同的地方在于它們都是一種行為規(guī)范,可以用于標(biāo)識(shí)哪些類(lèi)有哪些方法。

          derive:派生,編譯器可以通過(guò) derivetrait 加上一些基本實(shí)現(xiàn),如

          • Copy[4]:使類(lèi)型具有 “復(fù)制語(yǔ)義”而非 “移動(dòng)語(yǔ)義”。
          • Clone[5]:可以明確地創(chuàng)建一個(gè)值的深拷貝,在使用 Copy 的派生時(shí)一般需要把 Clone 加上,因?yàn)?Clone 是 Copy 的超集。

          • Debug[6]:使用 {:?} 可以完整地打印當(dāng)前值。

          回到 v 參數(shù)的那個(gè)問(wèn)題,因?yàn)?vu32 類(lèi)型實(shí)現(xiàn)了 Copy trait,且分配在棧上,調(diào)用 find_pos 時(shí)便會(huì)自動(dòng) Copy 了一份 v' 。

          但如果我們不想使用 copy 語(yǔ)義,避免內(nèi)存過(guò)多的被復(fù)制,我們可以使用“借用”數(shù)據(jù)。

          值的借用

          我們來(lái)看新的一個(gè)例子:

          fn main() {
              let data = vec![1234];
              let data1 = data;
              println!("sum of data1: {}", sum(data1));
              println!("data1: {:?}", data1); // error1
              println!("sum of data: {}", sum(data)); // error2
          }

          fn sum(data: Vec<u32>) -> u32 {
              // 創(chuàng)建一個(gè)迭代器,fold 方法用法類(lèi)似 reduce
              data.iter().fold(0, |acc, x| acc + x)
          }

          很明顯上述代碼無(wú)法通過(guò)編譯,data 和 data1 在執(zhí)行賦值語(yǔ)句和執(zhí)行 sum 方法的時(shí)候,所有權(quán)均被 move 過(guò)去了,在后面再調(diào)用他們自然會(huì)報(bào)錯(cuò)。

          編譯器也非常智能地提示了我們錯(cuò)誤所在:

          error[E0382]: borrow of moved value: `data1`
          --> src/main.rs:5:29
          |
          3 | let data1 = data;
          | ----- move occurs because `data1` has type `Vec<u32>`, which does not implement the `Copy` trait
          4 | println!("sum of data1: {}", sum(data1));
          | ----- value moved here
          5 | println!("data1: {:?}", data1); // error1
          | ^^^^^ value borrowed here after move

          error[E0382]: use of moved value: `data`
          --> src/main.rs:6:37
          |
          2 | let data = vec![1, 2, 3, 4];
          | ---- move occurs because `data` has type `Vec<u32>`, which does not implement the `Copy` trait
          3 | let data1 = data;
          | ---- value moved here
          ...
          6 | println!("sum of data: {}", sum(data)); // error2
          | ^^^^ value used here after move

          For more information about this error, try `rustc --explain E0382`.
          error: could not compile `playground` due to 2 previous errors

          但我們只需要這樣改一下,可以不使用 copy 的情況下通過(guò)編譯:

          fn main() {
              let data = vec![1234];
              let data1 = &data;
              println!("sum of data1: {}", sum(&data1));
              println!("data1: {:?}", data1);
              println!("sum of data: {}", sum(&data));
          }

          fn sum(data: &Vec<u32>) -> u32 {
              data.iter().fold(0, |acc, x| acc + x)
          }

          使用 & 可以來(lái)實(shí)現(xiàn) Borrow 語(yǔ)義。顧名思義,Borrow 語(yǔ)義允許一個(gè)值的所有權(quán),在不發(fā)生轉(zhuǎn)移的情況下,被其它上下文使用。

          在 Rust 中,“借用”和“引用”是一個(gè)概念,同時(shí)在 Rust 下,所有的引用都只是借用了“臨時(shí)使用權(quán)”,它并不破壞值的單一所有權(quán)約束。

          所以,默認(rèn)情況下,Rust 的“借用”都是只讀的

          當(dāng)然,我們對(duì)值的借用也得有一個(gè)限制:借用不能超過(guò)值的生命周期

          生命周期我們熟悉,寫(xiě) React 或者 Vue 的時(shí)候,每個(gè)組件都有從創(chuàng)建到銷(xiāo)毀的生命周期,那在 Rust 里,值的生命周期是怎么樣的呢,值的借用限制什么和生命周期有關(guān)呢,我們接著往下看。

          生命周期:我們創(chuàng)建的值可以活多久

          在任何語(yǔ)言里,棧上的值都有自己的生命周期,它和幀的生命周期一致。

          在 Rust 中,堆上的內(nèi)存也引入生命周期的概念:除非顯式地做 Box::leak() 等動(dòng)作,一般來(lái)說(shuō),堆內(nèi)存的生命周期,會(huì)默認(rèn)和其棧內(nèi)存的生命周期綁定在一起。

          Box:使用 Box<T> 允許你將一個(gè)值放在堆上而不是棧上,留在棧上的則是指向堆數(shù)據(jù)的指針。除了數(shù)據(jù)被儲(chǔ)存在堆上而不是棧上之外,box 沒(méi)有性能損失,它們多用于如下場(chǎng)景:

          • 當(dāng)有一個(gè)在編譯時(shí)未知大小的類(lèi)型,而又想要在需要確切大小的上下文中使用這個(gè)類(lèi)型值的時(shí)候(比如遞歸類(lèi)型
          • 當(dāng)有大量數(shù)據(jù)并希望在確保數(shù)據(jù)不被拷貝的情況下轉(zhuǎn)移所有權(quán)的時(shí)候

          我們先來(lái)看一個(gè)例子:

          fn main() {
              let r;

              {
                  let x = 5;
                  r = &x;
              }

              println!("r: {}", r);
          }

          這段代碼并不會(huì)通過(guò)編譯,因?yàn)?r 所引用的值已經(jīng)在使用之前被釋放。

          error[E0597]: `x` does not live long enough
          --> src/main.rs:6:13
          |
          6 | r = &x;
          | ^^ borrowed value does not live long enough
          7 | }
          | - `x` dropped here while still borrowed
          8 |
          9 | println!("r: {}", r);
          | - borrow later used here

          Rust 編譯器有一個(gè)借用檢查器,它比較作用域來(lái)確保所有的借用都是有效的,比如上述例子,我們加上生命周期的注釋再看一下:

          fn main() {
              let r;                // ---------+-- 'a
                                    //          |
              {                     //          |
                  let x = 5;        // -+-- 'b  |
                  r = &x;           //  |       |
              }                     // -+       |
                                    //          |
              println!("r: {}", r); //          |
          }                         // ---------+

          如你所見(jiàn),內(nèi)部的 'b 塊要比外部的生命周期 'a 小得多。在編譯時(shí),Rust 比較這兩個(gè)生命周期的大小,并發(fā)現(xiàn) r 擁有生命周期 'a,不過(guò)它引用了一個(gè)擁有生命周期 'b 的對(duì)象。程序被拒絕編譯,因?yàn)樯芷?'b 比生命周期 'a 要小:被引用的對(duì)象比它的引用者存在的時(shí)間更短。

          由此,我們也解釋了 Rust 在值的借用中的那個(gè)規(guī)則:借用不能超過(guò)值的生命周期。

          在 Rust 中,值的生命周期可分為:

          • 靜態(tài)生命周期:如果一個(gè)值的生命周期貫穿整個(gè)進(jìn)程的生命周期,那么我們就稱(chēng)這種生命周期為靜態(tài)生命周期。
          • 動(dòng)態(tài)生命周期:如果一個(gè)值是在某個(gè)作用域中定義的,也就是說(shuō)它被創(chuàng)建在棧上或者堆上,那么其生命周期是動(dòng)態(tài)的。
          • 分配在堆和棧上的內(nèi)存有其各自的作用域,生命周期是動(dòng)態(tài)的。
          • 全局變量、靜態(tài)變量、字符串字面量、代碼等內(nèi)容,在編譯時(shí),會(huì)被編譯到可執(zhí)行文件中,加載入內(nèi)存。生命周期和進(jìn)程的生命周期一致,生命周期是靜態(tài)的。

          • 函數(shù)指針的生命周期也是靜態(tài)的,因?yàn)楹瘮?shù)在 Text 段中,只要進(jìn)程活著,其內(nèi)存一直存在。

          有了這些概念,我們?cè)賮?lái)看一個(gè)例子:

          fn main() {
              let s1 = String::from("Lindsey");
              let s2 = String::from("Rosie");

              let result = max(&s1, &s2);

              println!("bigger one: {}", result);
          }

          fn max(s1: &str, s2: &str) -> &str {
              if s1 > s2 {
                  s1
              } else {
                  s2
              }
          }

          同樣,這段代碼也無(wú)法通過(guò)編譯,編譯器報(bào)錯(cuò)信息如下:

          error[E0106]: missing lifetime specifier
          --> src/main.rs:10:31
          |
          10 | fn max(s1: &str, s2: &str) -> &str {
          | ---- ---- ^ expected named lifetime parameter
          |
          = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `s1` or `s2`
          help: consider introducing a named lifetime parameter
          |
          10 | fn max<'a>(s1: &'a str, s2: &'a str) -> &'a str {
          | ++++ ++ ++ ++

          For more information about this error, try `rustc --explain E0106`.
          error: could not compile `playground` due to previous error

          Rust 的編譯器始終是一個(gè)良師益友,十分嚴(yán)格且優(yōu)秀,引導(dǎo)我寫(xiě)出更可靠而高效的代碼。

          missing lifetime specifier 意思是編譯器在編譯 max() 函數(shù)時(shí),無(wú)法判斷 s1、s2 和返回值的生命周期。

          編譯器也給了我們解決方法:手動(dòng)添加生命周期注釋?zhuān)瑏?lái)告訴編譯器 s1s2 的生命周期。

          fn max<'a>(s1: &'a str, s2: &'a str) -> &'a str {
              if s1 > s2 {
                  s1
              } else {
                  s2
              }
          }

          這個(gè)例子或許大家看起來(lái)會(huì)很疑惑,s1s2 的生命周期明明一致,為什么編譯器會(huì)無(wú)法判斷他們的生命周期呢?

          其實(shí)很簡(jiǎn)單,剛剛我們提到過(guò),字符串字面量的生命周期是靜態(tài)的,而 s1 是動(dòng)態(tài)的,它們的生命周期是不一致的。

          當(dāng)出現(xiàn)多個(gè)參數(shù)的時(shí)候,它們的生命周期不一致,返回的值的生命周期自然也不好確定,所以這個(gè)時(shí)候,我們需要進(jìn)行生命周期標(biāo)注,告訴編譯器這些引用間生命周期的約束。

          Rust 與 Webassembly

          WebAssembly(wasm)可以在現(xiàn)代的網(wǎng)絡(luò)瀏覽器中運(yùn)行——它是一種低級(jí)的類(lèi)匯編語(yǔ)言,具有緊湊的二進(jìn)制格式,可以接近原生的性能運(yùn)行。

          簡(jiǎn)而言之,對(duì)于網(wǎng)絡(luò)平臺(tái)而言,WebAssembly 它提供了一條途徑,以使得以各種語(yǔ)言編寫(xiě)的代碼都可以以接近原生的速度在 Web 中運(yùn)行。

          對(duì)于前端而言,wasm 技術(shù)幫助解決一些場(chǎng)景下的性能瓶頸。

          比如在瀏覽器中:

          • 運(yùn)行 VR、圖像視頻編輯、3D 游戲
          • 可以更好的讓一些語(yǔ)言和工具(如 AutoCAD)可以編譯到 Web 平臺(tái)
          • 語(yǔ)言編譯器或虛擬機(jī)等

          脫離瀏覽器的情況下:

          • 游戲分發(fā)服務(wù)(便攜、安全)
          • 服務(wù)端執(zhí)行不可信任的代碼。

          • 服務(wù)端應(yīng)用

          • 移動(dòng)混合原生應(yīng)用

          接下來(lái),我們從一個(gè)圖片處理的例子入手,看一下如何從零構(gòu)建一個(gè) web-wasm 應(yīng)用。

          環(huán)境準(zhǔn)備

          Rust[7]

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

          在安裝 Rustup 時(shí),也會(huì)安裝 Rust 構(gòu)建工具和包管理器的最新穩(wěn)定版,即 Cargo。

          Cargo 可以做很多事情,如:

          • cargo build 可以構(gòu)建項(xiàng)目
          • cargo run 可以運(yùn)行項(xiàng)目

          • cargo test 可以測(cè)試項(xiàng)目

          • cargo doc 可以為項(xiàng)目構(gòu)建文檔

          • cargo publish 可以將庫(kù)發(fā)布到 crates.io[8]。

          很明顯,Cargo 在 Rust 中扮演的 Npm 在 Node 中的角色。

          Wasm-pack[9]

          curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

          wasm-pack 用于構(gòu)建和使用我們希望與 JavaScript,瀏覽器或 Node.js 互操作的 Rust 生成的 WebAssembly。

          Vite[10]

          這里的前端構(gòu)建工具我使用的是 Vite,在 Vite 中還需要增加一個(gè)插件:

          vite-plugin-rsw[11]:集成了 wasm-pack 的 CLI

          • 支持 rust 包文件熱更新,監(jiān)聽(tīng) src 目錄和 Cargo.toml 文件變更,自動(dòng)構(gòu)建
          • vite 啟動(dòng)優(yōu)化,如果之前構(gòu)建過(guò),再次啟動(dòng) npm run dev,則會(huì)跳過(guò) wasm-pack 構(gòu)建

          快速開(kāi)始

          創(chuàng)建一個(gè) vite 項(xiàng)目

          yarn create vite vite-webassembly

          添加 vite-plugin-rsw 插件

          yarn add vite-plugin-rsw -D

          并增加相應(yīng)配置:

          // vite.config.ts

          import { defineConfig } from 'vite'
          import react from '@vitejs/plugin-react'
          import ViteRsw from 'vite-plugin-rsw'

          export default defineConfig({
            plugins: [
              react(),
              // 查看更多:https://github.com/lencx/vite-plugin-rsw
              ViteRsw({
                // 如果包在`unLinks`和`crates`都配置過(guò)
                // 會(huì)執(zhí)行,先卸載(npm unlink),再安裝(npm link)
                // 例如下面會(huì)執(zhí)行
                // `npm unlink picture-wasm`
                unLinks: ['picture-wasm'],
                // 項(xiàng)目根路徑下的rust項(xiàng)目
                // `@`開(kāi)頭的為npm組織
                // 例如下面會(huì)執(zhí)行:
                // `npm link picture-wasm`
                // 因?yàn)閳?zhí)行順序原因,雖然上面的unLinks會(huì)把`picture-wasm`卸載
                // 但是這里會(huì)重新進(jìn)行安裝
                crates: [ picture-wasm ],
              }),
            ],
          })

          使用 cargo 初始化一個(gè) rust 項(xiàng)目

          在當(dāng)前目錄下執(zhí)行:

          cargo new picture-wasm

          我們先來(lái)看一下現(xiàn)在的目錄結(jié)構(gòu)

          [my-wasm-app] # 項(xiàng)目根路徑
          |- [picture-wasm] # npm包 `wasm-hey`
          | |- [pkg] # 生成wasm包的目錄
          | | |- picture-wasm_bg.wasm # wasm文件
          | | |- picture-wasm.js # 包入口文件
          | | |- picture-wasm_bg.wasm.d.ts # ts聲明文件
          | | |- picture-wasm.d.ts # ts聲明文件
          | | |- package.json
          | | - ...
          | |- [src] rust源代碼
          | | # 了解更多: https://doc.rust-lang.org/cargo/reference/cargo-targets.html
          | |- [target] # 項(xiàng)目依賴(lài),類(lèi)似于npm的 `node_modules`
          | | # 了解更多: https://doc.rust-lang.org/cargo/reference/manifest.html
          | |- Cargo.toml # rust包管理清單
          | - ...
          |- [node_modules] # 前端的項(xiàng)目包依賴(lài)
          |- [src] # 前端源代碼(可以是vue, react, 或其他)
          | # 了解更多: https://nodejs.dev/learn/the-package-json-guide
          |- package.json # `yarn` 包管理清單
          | # 了解更多: https://vitejs.dev/config
          |- vite.config.ts # vite配置文件
          | # 了解更多: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html
          |- tsconfig.json # typescript配置文件

          可以看到,生成的 picture-wasm 項(xiàng)目中,有一個(gè) Cargo.toml 文件,它就類(lèi)似于我們的 package.json ,是用作 Rust 的包管理的一個(gè)清單。

          Cargo.toml 中增加配置

          [package]
          name = "picture-wasm"
          version = "0.1.0"
          edition = "2021"

          [lib]
          crate-type = ["cdylib", "rlib"]

          [dependencies]
          wasm-bindgen = "0.2.70"
          base64 = "0.12.1"
          image = { version = "0.23.4", default-features = false, features = ["jpeg", "png"] }
          console_error_panic_hook = { version = "0.1.1", optional = true }
          wee_alloc = { version = "0.4.2", optional = true }

          [dependencies.web-sys]
          version = "0.3.4"
          features = [
          'Document',
          'Element',
          'HtmlElement',
          'Node',
          'Window',
          ]
          • dependencies:依賴(lài)列表
          • package :對(duì)于包的定義

          • lib:我們當(dāng)前是屬于庫(kù)工程(src 下是 lib.rs )而不是可執(zhí)行工程(src 下是 main.rs),需要對(duì)其進(jìn)行額外設(shè)置。

            • rlib:Rust Library 特定靜態(tài)中間庫(kù)格式。如果只是純 Rust 代碼項(xiàng)目之間的依賴(lài)和調(diào)用,那么,用 rlib 就能完全滿足使用需求(默認(rèn))。
          • cdylib:c 規(guī)范的動(dòng)態(tài)庫(kù),它可以公開(kāi)了 FFI 的一些功能,并可以被其他語(yǔ)言所調(diào)用。

          添加 Rust 代碼

          // picture-wasm/src/lib.rs

          // 鏈接到 `image` 和 `base64` 庫(kù),導(dǎo)入其中的項(xiàng)
          extern crate image;
          extern crate base64;
          // 使用 `use` 從 image 的命名空間導(dǎo)入對(duì)應(yīng)的方法
          use image::DynamicImage;
          use image::ImageFormat;
          // 從 std(基礎(chǔ)庫(kù))的命名空間導(dǎo)入對(duì)應(yīng)方法,可用解構(gòu)的方式
          use std::io::{Cursor, Read, Seek, SeekFrom};
          use std::panic;
          use base64::{encode};

          // 引入 wasm_bindgen 下 prelude 所有模塊,用作 在 Rust 與 JavaScript 之間通信
          use wasm_bindgen::prelude::*;

          // 當(dāng)`wee_alloc`特性啟用的時(shí)候,使用`wee_alloc`作為全局分配器。
          #[cfg(feature = "wee_alloc")]
          #[global_allocator]
          static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

          // #[wasm_bindgen] 屬性表明它下面的函數(shù)可以在JavaScript和Rust中訪問(wèn)。
          #[wasm_bindgen]
          extern "C"  {
              // 該 extern 塊將外部 JavaScript 函數(shù) console.log 導(dǎo)入 Rust。
              // 通過(guò)以這種方式聲明它,wasm-bindgen 將創(chuàng)建 JavaScript 存根 console
              // 允許我們?cè)?Rust 和 JavaScript 之間來(lái)回傳遞字符串。
              #[wasm_bindgen(js_namespace = console)]
              fn log(s: &str);
          }

          fn load_image_from_array(_array: &[u8]) -> DynamicImage {
              // 使用 match 進(jìn)行兜底報(bào)錯(cuò)匹配
              let img = match image::load_from_memory_with_format(_array, ImageFormat::Png) {
                  Ok(img) => img,
                  Err(error) => {
                      panic!("There was a problem opening the file: {:?}", error)
                  }
              };
              img
          }

          fn get_image_as_base64(_img: DynamicImage) -> String {
              // 使用 mut 聲明可變變量,類(lèi)似 js 中的 let,不使用 mut 為不可變
              // 使用 Cursor 創(chuàng)建一個(gè)內(nèi)存緩存區(qū),里面是動(dòng)態(tài)數(shù)組類(lèi)型
              let mut c = Cursor::new(Vec::new());
              // 寫(xiě)入圖片
              match _img.write_to(&mut c, ImageFormat::Png) {
                  Ok(c) => c,
                  Err(error) => {
                      panic!(
                          "There was a problem writing the resulting buffer: {:?}",
                          error
                      )
                  }
              };
              // 尋找以字節(jié)為單位的偏移量,直接用 unwrap 隱式處理 Option 類(lèi)型,直接返回值或者報(bào)錯(cuò)
              c.seek(SeekFrom::Start(0)).unwrap();

              // 聲明一個(gè)可變的動(dòng)態(tài)數(shù)組作輸出
              let mut out = Vec::new();
              c.read_to_end(&mut out).unwrap();
              // 使用 encode 轉(zhuǎn)換
              let stt = encode(&mut out);
              let together = format!("{}{}""data:image/png;base64,", stt);
              together
          }

          #[wasm_bindgen]
          pub fn grayscale(_array: &[u8]) -> Result<(), JsValue> {
              let mut img = load_image_from_array(_array);
              img = img.grayscale();
              let base64_str = get_image_as_base64(img);
              append_img(base64_str)
          }

          pub fn append_img(image_src: String) -> Result<(), JsValue> {
              // 使用 `web_sys` 來(lái)獲取 window 對(duì)象
              let window = web_sys::window().expect("no global `window` exists");
              let document = window.document().expect("should have a document on window");
              let body = document.body().expect("document should have a body");

              // 創(chuàng)建 img 元素
              // 使用 `?` 在出現(xiàn)錯(cuò)誤的時(shí)候會(huì)直接返回 Err
              let val = document.create_element("img")?;
              // val.set_inner_html("Hello from Rust!");
              val.set_attribute("src", &image_src)?;
              val.set_attribute("style""height: 200px")?;
              body.append_child(&val)?;
              log("success!");

              Ok(())
          }

          React 項(xiàng)目中調(diào)用 Wasm 方法

          // src/App.tsx

          import React, { useEffect } from "react" ;
          import init, { grayscale } from "picture-wasm" ;

          import logo from "./logo.svg";
          import "./App.css";

          function App({
            useEffect(() => {
              // wasm初始化,在調(diào)用`picture-wasm`包方法時(shí)
              // 必須先保證已經(jīng)進(jìn)行過(guò)初始化,否則會(huì)報(bào)錯(cuò)
              // 如果存在多個(gè)wasm包,則必須對(duì)每一個(gè)wasm包進(jìn)行初始化
              init();
            }, []);

            const fileImport = (e: any) => {
              const selectedFile = e.target.files[0];
              //獲取讀取我文件的File對(duì)象
              // var selectedFile = document.getElementById( files ).files[0];
              var reader = new FileReader(); //這是核心,讀取操作就是由它完成.
              reader.readAsArrayBuffer(selectedFile); //讀取文件的內(nèi)容,也可以讀取文件的URL
              reader.onload = (res: any) => {
                var uint8Array = new Uint8Array(res.target.result as ArrayBuffer);
                grayscale(uint8Array);
              };
            };

            return (
              <div className="App">
                <header className="App-header">
                  <img src={logo} className="App-logo" alt="logo" />
                  <p>Hello WebAssembly!</p>
                  <p>Vite + Rust + React</
          p>
                  <input type="file" id="files" onChange={fileImport} />
                </header>
              </
          div>
            );
          }

          export default App;

          執(zhí)行!

          在根目錄下執(zhí)行 yarn dev ,rsw 插件會(huì)打包 rust 項(xiàng)目并軟鏈接過(guò)來(lái),這樣一個(gè)本地彩色圖片轉(zhuǎn)換為黑白圖片的 web-wasm 應(yīng)用就完成了。


          參考資料

          [1] Rust Playground: https://play.rust-lang.org/

          [2] 標(biāo)記-清除算法: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management#垃圾回收

          [3] Copy trait: https://doc.rust-lang.org/std/marker/trait.Copy.html

          [4] Copy: https://rustwiki.org/zh-CN/core/marker/trait.Copy.html

          [5] Clone: https://rustwiki.org/zh-CN/std/clone/trait.Clone.html

          [6] Debug: https://rustwiki.org/zh-CN/std/fmt/trait.Debug.html

          [7] Rust: https://www.rust-lang.org/zh-CN/

          [8] crates.io: https://crates.io/

          [9] Wasm-pack: https://github.com/rustwasm/wasm-pack

          [10] Vite: https://cn.vitejs.dev/

          [11] vite-plugin-rsw: https://github.com/lencx/vite-plugin-rsw


          其他參考

          陳天 · Rust 編程第一課
          https://time.geekbang.org/column/article/408400
          Rust 入門(mén)第一課
          https://rust-book.junmajinlong.com/ch1/00.html
          2021年 Rust 行業(yè)調(diào)研報(bào)告-InfoQ
          https://www.infoq.cn/article/umqbighceoa81yij7uyg 24 days from node.js to Rust
          通過(guò)例子學(xué) Rust
          https://rustwiki.org/zh-CN/rust-by-example/hello.html
          客戶(hù)端視角認(rèn)識(shí)與感受 Rust 的紅與黑
          https://tech.bytedance.net/articles/7036575152028516365
          實(shí)現(xiàn)一個(gè)簡(jiǎn)單的基于 WebAssembly 的圖片處理應(yīng)用https://juejin.cn/post/6844904205417709581
          Rust 和 WebAssembly
          https://rustmagazine.github.io/rust_magazine_2021/chapter_2/rust_wasm_frontend.html

          24 days from node.js to Rust


          24 days from node.js to Rust (vino.dev)
          瀏覽 46
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  无码人妻精品一区二区蜜桃视频 | 精品卡一卡二 | 99三级视频| 中文字幕不卡+婷婷五月 | 国产成人高精内射 |