A. [from js to rust 系列][宏-01][官網文檔 19.5]高級特性:宏[譯文]
原文鏈接:The Rust Programming Language
作者:rust 團隊
譯文首發鏈接:zhuanlan.hu.com/p/516660154
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請註明出處。
中間加了一些對於 javaScript 開發者有幫助的註解。在學習 Rust 的代碼時,尤其是有一些經驗的開發者,一般都會去看一些相對簡單的庫,或者項目去學習。Rust 的原生語法本身並不復雜,有一定的 TypeScript 經驗的開發者,應該通過看一些教程,都能開始熟悉基本的語法。反而是宏相關內容,雖然就像本文寫的,大部分開發者不需要自己去開發宏,但是大家使用宏,和看懂別人代碼的主體邏輯,幾乎是繞不開宏的。雖然宏在 rust 里屬於「高級」內容,但是因為其和 rust 本身語法的正交性,Hugo 認為,反而應該早一些學習。並且,如果一個 JavaScript 開發者之前接觸過代碼生成器、babel,對這一章的內容反而會比較親切。
Derive 宏、attribute 宏特別像 rust 里的裝飾器。從作用上,和一般庫提供的介面來看,也特別像。所以如果之前有裝飾器的經驗的開發者,對這一章節應該也會比較親切。
我們一個個來討論這些內容,但是首先,我們看既然我們已經有了函數,我們為什麼需要這些特性。
基本上,宏是指一些代碼可以生成另一些代碼,這一塊的技術一般稱為元編程(Hugo 註:代碼生成器也屬於這一類技術)。在附賀顫錄 C,我們討論了 derive 屬性,可以幫助你生成一系列的 trait。整本書我們也在用 println! 和 vec! 宏。這些宏在編譯時,都會展開成為代碼,這樣你雹拍洞就不需要手寫這些代碼。
元編程可以幫助你減少手寫和維護的代碼量,當然,函數也能幫助你實現類似的功能。但是,宏有函數沒有的威力。
宏不好的地方在於,宏很復雜,因為你要用 rust 代碼寫 rust 代碼(Hugo 註:任何元編程都不是簡單的事兒,包括 JS 里的。)。因為這種間接性,宏的代碼要更難讀、難理解、難維護。(Hugo 註:個人學 rust,感覺最難不是生命周期,因為生命周期的問題,可以通過用一些庫繞過去,或者無腦 clone,如果是應用程序,則可以通過使用 orm 和資料庫來繞過很多生命周期的問題。反而是宏,因為稍微有點規模的代碼的,都有一大堆宏。宏最難的不是語法,而是作者的意圖,因為本質是他造了一套 DSL)
另一個和函數不一樣的地方是,宏需要先定義或者引入作用域,而函數可以在任何地方定義和使用。
我們可以通過 vec! 宏來創建任意類型的 vector,例如 2 個 integer,或者五個 string slice.
一個簡化的 vec! 宏:
#[macro_export] 標注指明了這個宏在 crate 的作用域里可用。沒有這個標注,宏不會被帶入到作用域里。
macro_rules! 後面就是宏的名字。這里只有一種模式匹配的邊(arm):( (( (x:expr ),* ) ,=> 後面是這個模式對應要生成的代碼。如果這個模式匹配成功,對應的代碼和輸入的參數組成的代碼就會生成在最終的代碼中。因為這里只有一種邊,所以只有這一種可以匹配的條件。不符合這個條件的輸入,都會報錯。一般復雜的宏,都會有多個邊。
這里匹配的規則和 match 是不一樣的,因為這里的語法匹配的是 rust 的語法,而不是 rust 的類型,或者值。更全的宏匹配語法,見文檔。
對於 宏的輸入條件 ( (( (x:expr ),* ),()內部是匹配的語法,expr表示所有Rust的表達式。() 內部是匹配的語法,expr 表示所有 Rust 的表達式。()內部是匹配的語法,expr表示所有Rust的表達式。() 後面的都喊表示這個變數後面有可能有逗號,* 表示前面的模式會出現一次或者多次。(Hugo 註:像不像正則?源枯宏語法其實挺簡單的,不要被高級唬住了。當然,宏還是難的,宏要考慮的問題本身是一個復雜的問題。)
當我們調用:vec![1, 2, 3]; 時,$x 模式會匹配 3 個表達式 1 , 2 和 3。
現在我們看一下和這個邊匹配的生成代碼的部分:
在 ()里的tempvec.push(() 里的 temp_vec.push(()里的tempvec.push(x); 就是生成的代碼的部分。* 號仍然表示生成零個和多個,這個匹配的具體個數,要看匹配條件命中的個數。
你傳任意參數,最後就生成符合上面條件的代碼。
雖然過程宏有三種:custom derive、attribute-like 和 function-like,但是原理都是一樣的。
如果要創建過程宏,定義的部分需要在自己的 crate 里,並且要定義特殊的 crate 類型。(Hugo 註:相當於定義了一個 babel 插件,只不過有一套 rust 自己的體系。這些宏會在編譯的時候,按照書寫的規則,轉成對應的代碼。所有的宏,都是代碼生成的手段,輸入是代碼,輸入是代碼。)這種設計,我們有可能會在未來消除。
下面是一個過程宏的例子:
過程宏接收一個 TokenStream,輸出一個 TokenStream。TokenStream 類型定義在 proc_macro 里,表示一系列的 tokens。這個就是這種宏的核心機制,輸入的代碼(會被 rust) 轉成 TokenStream,然後做一些按照業務邏輯的操作,最後生成 TokenStream。這個函數也可以疊加其他的屬性宏(#[some_attribute], 看起來像裝飾器的邏輯,也可以理解為一種鏈式調用),可以在一個 crate 里定義多個過程。(Hugo 註:搞過 babel 的同學肯定很熟悉,一樣的味道。沒搞過的同學,強烈建議先學學 babel。)
下面我們來看看不同類型的過程宏。首先從自定義 derive 宏開始,然後我們介紹這種宏和其他幾種的區別。
我們創建一個 crate 名字叫 hello_macro,定義一個 HelloMacro 的 trait,關聯的函數名字叫 hello_macro。通過使用這個宏,用戶的結構可以直接獲得默認定義的 hello_macro 函數,而不需要實現這個 trait。默認的 hello_macro 可以列印 Hello, Macro! My name is TypeName!,其中 TypeName 是實現這個 derive 宏的結構的類型名稱。
創建這個宏的過程如下,首先
然後定義 HelloMacro trait
這樣我們就有了一個 trait,和這個triat 的函數。用戶可以通過這個 trait 直接實現對應的函數。
但是,用戶需要每次都實現一遍 hello_macro。如果 hello_macro 的實現都差不多,就可以通過 derive 宏來是實現。
因為 Rust 沒有反射機制,我們不可以在執行時知道對應類型的名字。我們需要在編譯時生成對應的代碼。
下一步,定義過程宏。在這個文章編寫時,過程宏需要在自己的 crates 里。最終,這個設計可能改變。關於 宏 crate 的約定是:對於一個名為 foo 的 crate,自定義 drive 宏的crate 名字為 foo_derive。我們在 hello_macro 項目中創建 hello_macro_derive crate。
我們的兩個的 crate 關聯緊密,所以我們在 hello_macro crate 里創建這個 crate。如果我們要改變 hello_macro 的定義,我們同樣也要更改 hello_macro_derive 的定義。這兩個 crates 要隔離發布。當用戶使用時,要同時添加這兩個依賴。為了簡化依賴,我們可以讓 hello_macro 使用 hello_macro_derive 作為依賴,然後導出這個依賴。但是,這樣,如果用戶不想使用 hello_macro_derive,也會自動添加上這個依賴。
下面開始創建 hello_macro_derive,作為一個過程宏 crate。需要添加依賴 syn 和 quote。下面是這個 crate 的 Cargo.toml。
在 lib.rs 里添加下述代碼。注意,這個代碼如果不增加 impl_hello_macro 的實現是通不過編譯的。
注意,這里把代碼分散成兩部分,一部分在 hello_macro_derive 函數里,這個函數主要負責處理 TokenStream,另一部分在 impl_hello_macro,這里負責轉換語法樹:這樣編寫過程宏可以簡單一些。在絕大部分過程宏立,對於前者的過程一般都是一樣的。一般來說,真正的區別在 impl_hello_macro,這里的邏輯一般是一個過程宏的業務決定的。
我們引入了三個 crates: proc_macro, syn 和 quote。proc_macro 內置在 rust 立,不需要在 Cargo.toml 中引入。proc_macro 實際是 rust 編譯器的一個介面,用來讀取和操作 Rust 代碼。
syn crate 把 Rust 代碼從字元串轉換為可以操作的結構體。quote crate 把 syn 數據在轉回 Rust 代碼。這些 Crate 可以極大簡化過程宏的編寫:寫一個 Rust 代碼的 full parser 可不是容易的事兒!
當在一個類型上標注 [derive(HelloMacro)] 時,會調用 hello_macro_derive 函數。之所以會有這樣的行為,是因為在定義 hello_macro_derive 時,標注了 #[proc_macro_derive(HelloMacro)] 在函數前面。
hello_macro_derive 會把輸入從 TokenStream 轉換為一個我們可以操作的數據結構。這就是為什麼需要引入 syn 。sync 的 parse 函數會把 TokenStream 轉換為 DeriveInput。
上述這個結構的意思是:正在處理的是 ident(identifier, 意味著名字)為 Pancakes 的 unit struct。其他的欄位表示其餘的 Rust 代碼。如果想了解更詳細的內容,請參考。
接下來,我們就要開始定義 impl_hello_macro。這個函數實現了添加到 Rust 代碼上的函數。在我們做之前,注意 derive macro 的輸出也是 TokenStream。返回的 TokenStream 就是添加完代碼以後的代碼。當編譯 crate 時,最終的代碼,就是處理完成的代碼了。
你也許也會發現,這里調用 syn::parse 時使用了 unwrap,如果報錯就中斷。這里必須這么做,因為最終返回的是 TokenStream,而不是 Result。這里是為了簡化代碼說明這個問題。在生產代碼,你應該處理好報錯,提供更詳細的報錯信息,例如使用 panic! 或者 expect。
下面是代碼:
通過上面的代碼,cargo build 就可以正常工作了。如果要使用這個代碼,需要把兩個依賴都加上。
現在執行下面的代碼,就可以看到 Hello, Macro! My name is Pancakes!
下一步,我們來 探索 其他類型的過程宏。
屬性宏和 derive 宏類似,但是可以創造除了 derive 意外的屬性。derive 只能作用於 structs 和 enums,屬性宏可以作用於其他的東西,比如函數。下面是一個屬性宏的例子:例如你製作了一個名為 route 的屬性宏來在一個web 框架中標注函數。
#[route] 是框架定義的過程宏。定義這個宏的函數類似:
這里,有兩個參數,類型都是 TokenStream。第一個是屬性的內容,GET, "/" 部分,第二個是標注屬性宏傳入的語法部分,在這個例子里,就是剩下的 fn index() {}。
工作原理和 derive 宏是一樣的。
函數宏的使用比較像調用一個 rust 函數。函數宏有點像 macro_rules! ,能提供比函數更高的靈活性。例如,可以接受未知個數的參數。但是,macro_rules! 只能使用在上述章節的匹配型的語法。而函數宏接受 TokenStream 參數作為入參,和其他過程宏一樣,可以做任何變換,然後返回 TokenStream。下面是一個函數宏 sql!
這個宏接受 SQL 語句,可以檢查這個 SQL 的語法是否正確,這種功能比 macro_rules! 提供的要復雜的多。這個 sql! 的宏可以定義為:
這個定義和自定義 derive 宏類似:接受括弧內的 tokens,返回生成的代碼。
好了,現在你有了一些可能不常用的 Rust 新工具,但是你要知道的是,在需要的場合,他們的運行原理是什麼。我們介紹了一些復雜的話題,當你在錯誤處理或者別人的代碼里看到這些宏時,可以認出這些概念和語法。可以使用這一章的內容作為解決這些問題的索引。
B. rust可以代替javascript嗎
不會取代的。javascript是一種頁面腳本,通過執行程序腳本片段,我們可以對頁面及頁面上的元素進行操作,實現特定的功能與效果。
而Rust是Mozilla開發的注重安全、性能和並發性的編程語言。
創建這個新語言的目的是為了解決一個頑疾:軟體的演進速度大大低於硬體的演進,軟體在語言級別上無法真正利用多核計算帶來的性能提升。Rust是針對多核體系提出的語言,並且吸收一些其他動態語言的重要特性,比如不需要管理內存,比如不會出現Null指針等等。
rust將來有可能取代c或者c++,但是無法撼動javascript在業界的地位。
C. 深入淺出Rust(第三部分-1)
傳送門:
深數芹入淺出Rust(第一部分-1)
深入淺出Rust(第一部分-2)
深入淺出Rust(第二部分-1)
深入淺出Rust(第二部分-2)
深入淺出Rust(第三部分-1)
深入淺出Rust(第三部分-2)
深入淺出Rust(第四部分)
深入淺出Rust(第五部分)
看了引入泛型,就要考慮的方方面面,怪不得飢咐Go遲遲拿不出方案了...
Rust的泛型和java的不同,java只是在編譯器進行檢查,運行是進行類型擦除.而Rust是在編譯器時進行檢查和類型綁定.
閱讀下來,語義上閉包和js的閉包區別不大,語法小薯肢畢有區別
語法:
簡寫:
省略類型,{},return,這些和java,js倒是一樣的.
D. 減少rust編譯後程序體積
第一步:
編敬祥譯release版本
第二步:
strip 命令
擴展
整優化等級
通過修改默認優化亮液搏等級方式減少體積,以cpu換空間,如果不是必要,建議不要改
在Cargo.toml中新增下面配置
開啟 LTO
減少體積,增加鏈接時埋孫間也是一個取捨問題
在Cargo.toml中新增下面配置
E. Electron替代方案,rust跨平台GUI框架TAURI之hello world
tauri 是一個新興的配明判跨平台GUI框架。與electron的基本思想相似,tauri的前端實現也是基於html系列語言。tauri的後端使用rust。官方形容,tauri可以創建體積更小、運行更快、更加安全的跨平台桌面應用。
詳細的介紹可以自行去官網查看:
官網
Github
本人使用windows10系統。本hello world,實現了以tauri搭建桌面程序,在html頁面點擊按鍵後,槐陵由後台rust反饋信息。
效果如下:
tauri 需要用到rust、nodejs,編譯器可使用vscode
官方文檔有比較詳培改細的環境搭建步驟,可參閱:
https://tauri.studio/docs/getting-started/intro
其中,當搭建完環境,使用命令
yarn add tauri
安裝tauri包時,可能會出現報錯:
pngquant failed to build, make sure that libpng-dev is installed
此錯誤並不影響使用,可忽略。
初始化完成的tauri程序結構如上圖所示。默認情況下dist菜單用於存放實際的頁面文件。具體可在tauri.conf.json文件中進行設置。
具體實現步驟如下:
F. rust與js的語法
上有較大的不同,瞎搜rust遵循靜態類型,代碼執行過程磨散歷中間沒有變數的掘圓類型改變,並且rust的語法機制更加嚴格,更注重於類型的正確推斷,而javascript則更加靈活,運行時根據賦值情況會更改變數類型。
G. 對比 Go 語言,Rust 有什麼優勢和劣勢
我並沒有什麼編程的經驗,覺得編程實在是太復雜了,不喜歡去研究太多,對這個也不怎麼懂,只能說自己是個半吊子,就是所掌握的知識,也是東拼西湊的,朋友和我說點兒,自己去書上看一點兒,只能說根據自己的體驗給出一些體會吧。
其實我覺得什麼代碼啊編程啊這些東西還是比較適合理工的學生去研究,我一看腦袋就大,完全不明白在講什麼。我大概了解的就是這些,語言的話大家可以多方面的去了解,也不是說有缺點就是不好,看配置看個人吧,每個人習慣不一樣,也許有的人用不穩定的還覺得挺好呢,有的人就喜歡比較完美的,在我看來編程這個東西真的是很復雜,會有很多的代碼,這些代碼弄得我自己頭都大了,有的時候還得去惡補一下。
H. 對比 Go 語言,Rust 有什麼優勢和劣勢
Go語言是谷歌2009發布的第二款開源編程語言。Go語言專門針對陵肆物多處理器系統應用程序的編程進行了優化,使用Go編譯的程序可以媲美C或C++代碼的速度,而且更加安全、支持並行進程。
Rust是Mozilla開發的注重安全、性能和並發性雹悉的編程語言。"Rust",由web語言的領軍人物Brendan Eich(js之父),Dave Herman以及Mozilla公司的Graydon Hoare 合力開發。Rust是針對多核體系提出的語言,並且吸收一些其他動態語言的重要特性,比如不需要管理內存,比如不會出現Null指針等等。
不管是GO語言還是ruts都是各有各的長處,各有各的缺點的,每個都有自己存在的意義和用處,可以互不打擾的,選擇適合自己的語言去使用,讓他發揮到自己的用處才是他所存在的意義,也不能太過於可以的去比較他們之間互相的好與壞。
I. rust條件編譯
比如
自定義條件的屬性標注方式如下:
target_os等rustc已經支持的條件可以隱式判斷進行編譯,而自定義陸指條件,需要在編譯需要早判配顯式指定編譯條件。
以上編譯方式會報錯,因為直接執行沖指編譯找不到條件下的conditional_function方法
需要使用以下方法編譯:
直接執行cargo build會報錯找不到方法
正確方法應該使用以下方式編譯:
J. rust析構函數沒調用
首先,我們設計一個Node類型,它裡麵包含一個指針,可以指向其它的Node實例:
struct Node { next : Box<Node> }
下面我們嘗試一下創建兩個實例,將它們首尾相連:
fn main() { let node1 = Node { next : Box::new(...) } }
額,這里寫不下去了,Rust中要求,Box指針必須被合理初始化,而初始化Box的時候又必須先傳入一個Node的實例,這個Node的實例又要求創鬧肢租建一個Box指針。成液兆了「雞生蛋蛋生雞」的無限循環。
要打破這個循環,我們需要使用「可空的指針」。在初始化Node的時候,指針應該是「空」狀態,後面再把它們連接起來。我們把代碼改進成以下這樣,為了能修改node的值,還需要使用mut:
struct Node { next : Option<Box<Node>> } fn main() { let mut node1 = Box::new (Node { next : None }); let mut node2 = Box::new (Node { next : None }); node1.next = Some(node2); node2.next = Some(node1); }
編譯,發生錯誤:「error: use of moved value: node2」。
從編譯信息中可以看到,在node1.next = Some(node2);這條語句中,發生了move語義,從此句往後,node2變數的生命周期已經結束了。因此後面一句中使用node2的時候發生了錯誤。那我們需要繼續改進,不使用node2,換而使用node1.next,代碼改成這樣:
fn main() { let mut node1 = Box::new (Node { next : None }); let mut node2 = Box::new (Node { next : None }); node1.next = Some(node2); match node1.next { Some(mut n) => n.next = Some(node1), None => {} } }
編譯,又發生了錯誤,錯誤信息為:「error: use of partially moved value: node1」。
這是因為在match語句中,我們把node1.next的所有權轉移到了局部變數n中,這個n實際上就是node2的實例,在執行賦值操作n.next = Some(node1)的過程中,編譯器認為此時node1的一部分已經被轉移出去了,它不能再被用於賦值號的右邊。
看來,這是因為我們選擇使用的指針類型不對,Box類型的指針對所管理的內存擁有所有權,只使用Box指針沒有辦法構造一個循環引用的結構出來。於是,我們想到,使用Rc指針。同時,我們還用了Drop trait,來驗證這個對象是否真正被釋放了。
use std::rc::Rc; struct Node { next : Option<Rc<Node>> } impl Drop for Node { fn drop(&mut self) { println!("drop"); } } fn main() { let mut node1 = Node { next : None }; let mut node2 = Node { next : None }; let mut node3 = Node { next : None }; node1.next = Some(Rc::new(node2)); node2.next = Some(Rc::new(node3)); node3.next = Some(Rc::new(node1)); }
編飢信譯依然沒有通過,錯誤信息為:「error: partial reinitialization of uninitialized structure node2」。還是沒有達到我們的目的,繼續改進,我們將原先「棧」上分配內存改為在「堆」上分配內存:
use std::rc::Rc; struct Node { next : Option<Rc<Node>> } impl Drop for Node { fn drop(&mut self) { println!("drop"); } } fn main() { let mut node1 = Rc::new(Node { next : None }); let mut node2 = Rc::new(Node { next : None }); let mut node3 = Rc::new(Node { next : None }); node1.next = Some(node2); node2.next = Some(node3); node3.next = Some(node1); }
編譯,再次不通過,錯誤信息為:「error: cannot assign to immutable field」。通過這個錯誤信息,我們現在應該能想到,Rc類型包含的數據是不可變的,通過Rc指針訪問內部數據並做修改是不行的,必須用上RefCell把它們包裹起來才可以。繼續修改:
use std::rc::Rc; use std::cell::RefCell; struct Node { next : Option<Rc<RefCell<Node>>> } impl Node { fn new() -> Node { Node { next : None} } } impl Drop for Node { fn drop(&mut self) { println!("drop"); } } fn alloc_objects() { let node1 = Rc::new(RefCell::new(Node::new())); let node2 = Rc::new(RefCell::new(Node::new())); let node3 = Rc::new(RefCell::new(Node::new())); node1.borrow_mut().next = Some(node2.clone()); node2.borrow_mut().next = Some(node3.clone()); node3.borrow_mut().next = Some(node1.clone()); } fn main() { alloc_objects(); println!("program finished."); }