整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          vue3的宏到底是什么東西?

          vue3的宏到底是什么東西?

          從vue3開始vue引入了宏,比如defineProps、defineEmits等。我們每天寫vue代碼時都會使用到這些宏,但是你有沒有思考過vue中的宏到底是什么?為什么這些宏不需要手動從vue中import?為什么只能在setup頂層中使用這些宏?

          vue 文件如何渲染到瀏覽器上

          要回答上面的問題,我們先來了解一下從一個vue文件到渲染到瀏覽器這一過程經歷了什么?

          我們的vue代碼一般都是寫在后綴名為vue的文件上,顯然瀏覽器是不認識vue文件的,瀏覽器只認識html、css、jss等文件。所以第一步就是通過webpack或者vite將一個vue文件編譯為一個包含render函數的js文件。然后執行render函數生成虛擬DOM,再調用瀏覽器的DOM API根據虛擬DOM生成真實DOM掛載到瀏覽器上。

          vue3的宏是什么?

          我們先來看看vue官方的解釋:

          宏是一種特殊的代碼,由編譯器處理并轉換為其他東西。它們實際上是一種更巧妙的字符串替換形式。

          宏是在哪個階段運行?

          通過前面我們知道了vue 文件渲染到瀏覽器上主要經歷了兩個階段。

          第一階段是編譯時,也就是從一個vue文件經過webpack或者vite編譯變成包含render函數的js文件。此時的運行環境是nodejs環境,所以這個階段可以調用nodejs相關的api,但是沒有在瀏覽器環境內執行,所以不能調用瀏覽器的API。

          第二階段是運行時,此時瀏覽器會執行js文件中的render函數,然后依次生成虛擬DOM和真實DOM。此時的運行環境是瀏覽器環境內,所以可以調用瀏覽器的API,但是在這一階段中是不能調用nodejs相關的api。

          而宏就是作用于編譯時,也就是從vue文件編譯為js文件這一過程。

          舉個defineProps的例子:在編譯時defineProps宏就會被轉換為定義props相關的代碼,當在瀏覽器運行時自然也就沒有了defineProps宏相關的代碼了。所以才說宏是在編譯時執行的代碼,而不是運行時執行的代碼。

          一個defineProps宏的例子

          我們來看一個實際的例子,下面這個是我們的源代碼:

          <template>
            <div>content is {{ content }}</div>
            <div>title is {{ title }}</div>
          </template>
          
          <script setup lang="ts">
          import {ref} from "vue"
          const props=defineProps({
            content: String,
          });
          const title=ref("title")
          </script>
          


          在這個例子中我們使用defineProps宏定義了一個類型為String,屬性名為content的props,并且在template中渲染content的內容。

          我們接下來再看看編譯成js文件后的代碼,代碼我已經進行過簡化:

          import { defineComponent as _defineComponent } from "vue";
          import { ref } from "vue";
          
          const __sfc__=_defineComponent({
            props: {
              content: String,
            },
            setup(__props) {
              const props=__props;
              const title=ref("title");
              const __returned__={ props, title };
              return __returned__;
            },
          });
          
          import {
            toDisplayString as _toDisplayString,
            createElementVNode as _createElementVNode,
            Fragment as _Fragment,
            openBlock as _openBlock,
            createElementBlock as _createElementBlock,
          } from "vue";
          
          function render(_ctx, _cache, $props, $setup) {
            return (
              _openBlock(),
              _createElementBlock(
                _Fragment,
                null,
                [
                  _createElementVNode(
                    "div",
                    null,
                    "content is " + _toDisplayString($props.content),
                    1 /* TEXT */
                  ),
                  _createElementVNode(
                    "div",
                    null,
                    "title is " + _toDisplayString($setup.title),
                    1 /* TEXT */
                  ),
                ],
                64 /* STABLE_FRAGMENT */
              )
            );
          }
          __sfc__.render=render;
          export default __sfc__;
          


          我們可以看到編譯后的js文件主要由兩部分組成,第一部分為執行defineComponent函數生成一個 __sfc__ 對象,第二部分為一個render函數。render函數不是我們這篇文章要講的,我們主要來看看這個__sfc__對象。

          看到defineComponent是不是覺得很眼熟,沒錯這個就是vue提供的API中的 definecomponent函數。這個函數在運行時沒有任何操作,僅用于提供類型推導。這個函數接收的第一個參數就是組件選項對象,返回值就是該組件本身。所以這個__sfc__對象就是我們的vue文件中的script代碼經過編譯后生成的對象,后面再通過__sfc__.render=render將render函數賦值到組件對象的render方法上面。

          我們這里的組件選項對象經過編譯后只有兩個了,分別是props屬性和setup方法。明顯可以發現我們原本在setup里面使用的defineProps宏相關的代碼不在了,并且多了一個props屬性。沒錯這個props屬性就是我們的defineProps宏生成的。

          我們再來看一個不在setup頂層調用defineProps的例子:

          <script setup lang="ts">
          import {ref} from "vue"
          const title=ref("title")
          
          if (title.value) {
            const props=defineProps({
              content: String,
            });
          }
          </script>
          


          運行這個例子會報錯:defineProps is not defined

          我們來看看編譯后的js代碼:

          import { defineComponent as _defineComponent } from "vue";
          import { ref } from "vue";
          
          const __sfc__=_defineComponent({
            setup(__props) {
              const title=ref("title");
              if (title.value) {
                const props=defineProps({
                  content: String,
                });
              }
              const __returned__={ title };
              return __returned__;
            },
          });
          


          明顯可以看到由于我們沒有在setup的頂層調用defineProps宏,在編譯時就不會將defineProps宏替換為定義props相關的代碼,而是原封不動的輸出回來。在運行時執行到這行代碼后,由于我們沒有任何地方定義了defineProps函數,所以就會報錯defineProps is not defined。

          總結

          現在我們能夠回答前面提的三個問題了。

          • vue中的宏到底是什么?
          • vue3的宏是一種特殊的代碼,在編譯時會將這些特殊的代碼轉換為瀏覽器能夠直接運行的指定代碼,根據宏的功能不同,轉換后的代碼也不同。
          • 為什么這些宏不需要手動從vue中import?
          • 因為在編譯時已經將這些宏替換為指定的瀏覽器能夠直接運行的代碼,在運行時已經不存在這些宏相關的代碼,自然不需要從vue中import。
          • 為什么只能在setup頂層中使用這些宏?
          • 因為在編譯時只會去處理setup頂層的宏,其他地方的宏會原封不動的輸出回來。在運行時由于我們沒有在任何地方定義這些宏,當代碼執行到宏的時候當然就會報錯。


          作者:歐陽碼農
          鏈接:https://juejin.cn/post/7335721246931189795

          文鏈接:The Rust Programming Language

          作者:rust 團隊

          譯文首發鏈接:zhuanlan.zhihu.com/p/516660154

          著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

          前言

          中間加了一些對于 JavaScript 開發者有幫助的注解。在學習 Rust 的代碼時,尤其是有一些經驗的開發者,一般都會去看一些相對簡單的庫,或者項目去學習。Rust 的原生語法本身并不復雜,有一定的 TypeScript 經驗的開發者,應該通過看一些教程,都能開始熟悉基本的語法。反而是宏相關內容,雖然就像本文寫的,大部分開發者不需要自己去開發宏,但是大家使用宏,和看懂別人代碼的主體邏輯,幾乎是繞不開宏的。雖然宏在 rust 里屬于“高級”內容,但是因為其和 rust 本身語法的正交性,Hugo 認為,反而應該早一些學習。并且,如果一個 JavaScript 開發者之前接觸過代碼生成器、babel,對這一章的內容反而會比較親切。

          Derive 宏、attribute 宏特別像 rust 里的裝飾器。從作用上,和一般庫提供的接口來看,也特別像。所以如果之前有裝飾器的經驗的開發者,對這一章節應該也會比較親切。

          正文

          整本書經常使用 println! 宏,但是還沒介紹宏這個機制。宏實際上是 rust 的一系列特性的合集:聲明式宏(declarative macro):使用 marco_rules!聲明的代碼和三種過程式宏(procedural macros):

          • 自定義 #[derive] 宏,可以把制定的代碼作用在 struct 和 enum 里
          • 屬性類似的宏,可以把任何屬性定義在任何東西上
          • 函數類似的宏,看起來像是函數調用,但是是作用在它參數的 tokens 上

          我們一個個來討論這些內容,但是首先,我們看既然我們已經有了函數,我們為什么需要這些特性。

          函數和宏的區別

          基本上,宏是指一些代碼可以生成另一些代碼,這一塊的技術一般稱為元編程(Hugo 注:代碼生成器也屬于這一類技術)。在附錄 C,我們討論了 derive 屬性,可以幫助你生成一系列的 trait。整本書我們也在用 println! 和 vec! 宏。這些宏在編譯時,都會展開成為代碼,這樣你就不需要手寫這些代碼。

          元編程可以幫助你減少手寫和維護的代碼量,當然,函數也能幫助你實現類似的功能。但是,宏有函數沒有的威力。

          函數必須聲明如慘的數量和種類。而宏,在另一方面,可以接收任意數量的參數:我們可以調用 println!("hello"),也可以調用 println!("hello {}", name)。并且,宏是在編譯階段展開了代碼,所以一個宏可以在編譯時為一個類型實現一個 trait。一個函數就不可以。因為函數是在運行時調用,而 trait 實現只可以發生在編譯時。(Hugo 注:JS 可以實現運行時生成 trait(這里只是套用 rust 的概念),當然,如果你需要的話。動態語言在某些場景能很簡單實現非常強大的功能。)

          宏不好的地方在于,宏很復雜,因為你要用 rust 代碼寫 rust 代碼(Hugo 注:任何元編程都不是簡單的事兒,包括 JS 里的。)。因為這種間接性,宏的代碼要更難讀、難理解、難維護。(Hugo 注:個人學 rust,感覺最難不是生命周期,因為生命周期的問題,可以通過用一些庫繞過去,或者無腦 clone,如果是應用程序,則可以通過使用 orm 和數據庫來繞過很多生命周期的問題。反而是宏,因為稍微有點規模的代碼的,都有一大堆宏。宏最難的不是語法,而是作者的意圖,因為本質是他造了一套 DSL)

          另一個和函數不一樣的地方是,宏需要先定義或者引入作用域,而函數可以在任何地方定義和使用。

          使用聲明式宏 macro_rules! 進行通用元編程

          在 Rust 中使用最廣泛的宏是聲明式宏。它們有時也被稱為 “macros by example”、“macro_rules!宏” 或者就是 “macros”。聲明式宏寫起來和 Rust 的 match 語法比較像。在第六章里講到,match 語法是一種流程控制語法,接收一個表達式,然后和結果進行模式匹配,然后執行匹配到結果的代碼。宏也會做類似的比較:在這種情況下,傳入的參數是 rust 的合法的語法代碼,然后通過宏的規則,和書寫好的模版,在編譯時轉換成代碼。

          定義聲明式宏的語法是 macro_rule!。下面我來用 vec! 來介紹這一機制。第八章有關于 vec! 的內容。例如,創建一個新的 vector,包含 3 個 integer:

          #![allow(unused)]
          fn main() {
          let v: Vec<u32>=vec![1, 2, 3];
          }

          我們可以通過 vec! 宏來創建任意類型的 vector,例如 2 個 integer,或者五個 string slice.

          我們不能用函數去實現這個功能,因為我們不知道輸入的參數個數。(Hugo 注:這一點和 JS 非常不一樣,我們寫 JS,已經習慣了可以傳任意變量。當然如果你熟悉 TS,和 TS 有一些類似。Rust 雖然也有范型,但是和 TS 的范型非常不一樣。這里不一樣,我主要指關注點,因為 Rust 比較大一部分都是函數式的代碼,每個函數一般都承載非常細粒度的功能,一般每個函數都處理好了自己的輸入、輸出、報錯,所有可能性都寫好了,寫 rust 有一種在填狀態機的錯覺。。TS 有的代碼也有這種感覺。)

          一個簡化的 vec! 宏:

          #[macro_export]
          macro_rules! vec {
              ( $( $x:expr ),* )=> {
                  {
                      let mut temp_vec=Vec::new();
                      $(
                          temp_vec.push($x);
                      )*
                      temp_vec
                  }
              };
          }

          注意:實際的 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。

          現在我們看一下和這個邊匹配的生成代碼的部分:

           {
                  {
                      let mut temp_vec=Vec::new();
                      $(
                          temp_vec.push($x);
                      )*
                      temp_vec
                  }
              };

          在 ()里的tempvec.push(() 里的 temp_vec.push(()里的tempvec.push(x); 就是生成的代碼的部分。* 號仍然表示生成零個和多個,這個匹配的具體個數,要看匹配條件命中的個數。

          當我們調用:vec![1, 2, 3]; 時,生成了這個代碼。(Hugo 注:Cargo 有 expand 插件,對于聲明宏,多看看展開基本就能學會了。)

          {
              let mut temp_vec=Vec::new();
              temp_vec.push(1);
              temp_vec.push(2);
              temp_vec.push(3);
              temp_vec
          }

          你傳任意參數,最后就生成符合上面條件的代碼。

          有一些 macro_rules! 的奇怪的邊界例子。在未來,Rust 會有第二種聲明式宏,和現在的機制類似,但是會解決這些邊界問題。在那一次升級后,macro_rules! 會被棄用。(Hugo 注:Rust 仍然是非常年輕的語言,做好隨時接受改變的準備)記住這些,當然另一個事實是,大部分 Rust 程序員更多是宏的使用者,而不是開發者,我們不會在深入討論 macro_rules!。如果你對這塊特別感興趣。請閱讀《“The Little Book of Rust Macros”》。(Hugo 注:站在入門的角度,能知道機制去使用就 ok 了。在絕大部分入門的情況下,函數以及使用 crates.io 上的宏都能滿足你的需求。)

          從屬性生成代碼的過程宏

          第二種宏是過程宏,表現形式更像函數(過程的一種類型)。過程宏的入參是一些代碼,你可以操作這些代碼,然后襯衫一些代碼。(Hugo:從結果看和聲明宏沒區別,其實站在 JS 的角度,更像是 babel,你可以根據輸入的 token 做變換)

          雖然過程宏有三種:custom derive、attribute-like 和 function-like,但是原理都是一樣的。

          如果要創建過程宏,定義的部分需要在自己的 crate 里,并且要定義特殊的 crate 類型。(Hugo 注:相當于定義了一個 babel 插件,只不過有一套 rust 自己的體系。這些宏會在編譯的時候,按照書寫的規則,轉成對應的代碼。所有的宏,都是代碼生成的手段,輸入是代碼,輸入是代碼。)這種設計,我們有可能會在未來消除。

          下面是一個過程宏的例子:

          use proc_macro;
          
          #[some_attribute]
          pub fn some_name(input: TokenStream) -> TokenStream {
          }

          過程宏接收一個 TokenStream,輸出一個 TokenStream。TokenStream 類型定義在 proc_macro 里,表示一系列的 tokens。這個就是這種宏的核心機制,輸入的代碼(會被 rust) 轉成 TokenStream,然后做一些按照業務邏輯的操作,最后生成 TokenStream。這個函數也可以疊加其他的屬性宏(#[some_attribute], 看起來像裝飾器的邏輯,也可以理解為一種鏈式調用),可以在一個 crate 里定義多個過程。(Hugo 注:搞過 babel 的同學肯定很熟悉,一樣的味道。沒搞過的同學,強烈建議先學學 babel。)

          下面我們來看看不同類型的過程宏。首先從自定義 derive 宏開始,然后我們介紹這種宏和其他幾種的區別。

          如何編寫自定義 derive 宏

          我們創建一個 crate 名字叫 hello_macro,定義一個 HelloMacro 的 trait,關聯的函數名字叫 hello_macro。通過使用這個宏,用戶的結構可以直接獲得默認定義的 hello_macro 函數,而不需要實現這個 trait。默認的 hello_macro 可以打印 Hello, Macro! My name is TypeName!,其中 TypeName 是實現這個 derive 宏的結構的類型名稱。

          use hello_macro::HelloMacro;
          use hello_macro_derive::HelloMacro;
          
          #[derive(HelloMacro)]
          struct Pancakes;
          
          fn main() {
              Pancakes::hello_macro();
          }

          創建這個宏的過程如下,首先

          $ cargo new hello_macro --lib

          然后定義 HelloMacro trait

          pub trait HelloMacro {
              fn hello_macro();
          }

          這樣我們就有了一個 trait,和這個triat 的函數。用戶可以通過這個 trait 直接實現對應的函數。

          use hello_macro::HelloMacro;
          
          struct Pancakes;
          
          impl HelloMacro for Pancakes {
              fn hello_macro() {
                  println!("Hello, Macro! My name is Pancakes!");
              }
          }
          
          fn main() {
              Pancakes::hello_macro();
          }

          但是,用戶需要每次都實現一遍 hello_macro。如果 hello_macro 的實現都差不多,就可以通過 derive 宏來是實現。

          因為 Rust 沒有反射機制,我們不可以在執行時知道對應類型的名字。我們需要在編譯時生成對應的代碼。

          下一步,定義過程宏。在這個文章編寫時,過程宏需要在自己的 crates 里。最終,這個設計可能改變。關于 宏 crate 的約定是:對于一個名為 foo 的 crate,自定義 drive 宏的crate 名字為 foo_derive。我們在 hello_macro 項目中創建 hello_macro_derive crate。

          $ cargo new hello_macro_derive --lib

          我們的兩個的 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]
          proc-macro=true
          
          [dependencies]
          syn="1.0"
          quote="1.0"

          在 lib.rs 里添加下述代碼。注意,這個代碼如果不增加 impl_hello_macro 的實現是通不過編譯的。

          use proc_macro::TokenStream;
          use quote::quote;
          use syn;
          
          #[proc_macro_derive(HelloMacro)]
          pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
              // Construct a representation of Rust code as a syntax tree
              // that we can manipulate
              let ast=syn::parse(input).unwrap();
          
              // Build the trait implementation
              impl_hello_macro(&ast)
          }

          注意,這里把代碼分散成兩部分,一部分在 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。

          DeriveInput {
              // --snip--
          
              ident: Ident {
                  ident: "Pancakes",
                  span: #0 bytes(95..103)
              },
              data: Struct(
                  DataStruct {
                      struct_token: Struct,
                      fields: Unit,
                      semi_token: Some(
                          Semi
                      )
                  }
              )
          }
          

          上述這個結構的意思是:正在處理的是 ident(identifier, 意味著名字)為 Pancakes 的 unit struct。其他的字段表示其余的 Rust 代碼。如果想了解更詳細的內容,請參考。

          接下來,我們就要開始定義 impl_hello_macro。這個函數實現了添加到 Rust 代碼上的函數。在我們做之前,注意 derive macro 的輸出也是 TokenStream。返回的 TokenStream 就是添加完代碼以后的代碼。當編譯 crate 時,最終的代碼,就是處理完成的代碼了。

          你也許也會發現,這里調用 syn::parse 時使用了 unwrap,如果報錯就中斷。這里必須這么做,因為最終返回的是 TokenStream,而不是 Result。這里是為了簡化代碼說明這個問題。在生產代碼,你應該處理好報錯,提供更詳細的報錯信息,例如使用 panic! 或者 expect。

          下面是代碼:

          fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
              // 通過&ast.ident 獲取類型的名字
              let name=&ast.ident;
              // 使用 quote 宏,可以使用 rust 語法來定義要實現的 trait(Hugo 注:JS 要有這個就好了)
              let gen=quote! {
                  // #name 是 quote! 的模版語法,會自動替換為這個變量里的值
                  impl HelloMacro for #name {
                      fn hello_macro() {
                          // 這里把對應的 struct 名字轉換好了,stringfy!把值轉換為字符串
                          println!("Hello, Macro! My name is {}!", stringify!(#name));
                      }
                  }
              };
             // 轉換為最終的 TokenStream
              gen.into()
          }

          通過上面的代碼,cargo build 就可以正常工作了。如果要使用這個代碼,需要把兩個依賴都加上。

          hello_macro={ path="../hello_macro" }
          hello_macro_derive={ path="../hello_macro/hello_macro_derive" }

          現在執行下面的代碼,就可以看到 Hello, Macro! My name is Pancakes!

          use hello_macro::HelloMacro;
          use hello_macro_derive::HelloMacro;
          
          #[derive(HelloMacro)]
          struct Pancakes;
          
          fn main() {
              Pancakes::hello_macro();
          }

          下一步,我們來探索其他類型的過程宏。

          屬性宏(Attribute-like)

          屬性宏和 derive 宏類似,但是可以創造除了 derive 意外的屬性。derive 只能作用于 structs 和 enums,屬性宏可以作用于其他的東西,比如函數。下面是一個屬性宏的例子:例如你制作了一個名為 route 的屬性宏來在一個web 框架中標注函數。

          #[route(GET, "/")]
          fn index() {

          #[route] 是框架定義的過程宏。定義這個宏的函數類似:

          #[proc_macro_attribute]
          pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

          這里,有兩個參數,類型都是 TokenStream。第一個是屬性的內容,GET, "/" 部分,第二個是標注屬性宏傳入的語法部分,在這個例子里,就是剩下的 fn index() {}。

          工作原理和 derive 宏是一樣的。

          函數宏(Function-like)

          函數宏的使用比較像調用一個 rust 函數。函數宏有點像 macro_rules! ,能提供比函數更高的靈活性。例如,可以接受未知個數的參數。但是,macro_rules! 只能使用在上述章節的匹配型的語法。而函數宏接受 TokenStream 參數作為入參,和其他過程宏一樣,可以做任何變換,然后返回 TokenStream。下面是一個函數宏 sql!

          let sql=sql!(SELECT * FROM posts WHERE id=1);

          這個宏接受 SQL 語句,可以檢查這個 SQL 的語法是否正確,這種功能比 macro_rules! 提供的要復雜的多。這個 sql! 的宏可以定義為:

          #[proc_macro]
          pub fn sql(input: TokenStream) -> TokenStream {

          這個定義和自定義 derive 宏類似:接受括號內的 tokens,返回生成的代碼。

          總結

          好了,現在你有了一些可能不常用的 Rust 新工具,但是你要知道的是,在需要的場合,他們的運行原理是什么。我們介紹了一些復雜的話題,當你在錯誤處理或者別人的代碼里看到這些宏時,可以認出這些概念和語法。可以使用這一章的內容作為解決這些問題的索引。

          概述

          在工程規模較小,不是很復雜,與硬件結合緊密,要求移植性的時候,可采用宏定義簡化編程,增強程序可讀性。

          當宏作為常量使用時,C程序員習慣在名字中只使用大寫字母。但是并沒有如何將用于其他目的的宏大寫的統一做法。由于宏(特別是帶參數的宏)可能是程序中錯誤的來源,所以一些程序員更喜歡使用大寫字母來引起注意。

          1. 簡單宏定義

          無參宏的宏名后不帶參數,其定義的一般形式為:

          #define 標識符 字符串

          // 不帶參數的宏定義
          #define MAX 10

          注意:不要在宏定義中放置任何額外的符號,比如"="或者尾部加";"

          使用#define來為常量命名一些優點:

          • 程序會更易讀。一個認真選擇的名字可以幫助讀者理解常量的意義;
          • 程序會更易于修改。我們僅需要改變一個宏定義,就可以改變整個程序中出現的所有該常量的值;
          • 可以幫助避免前后不一致或鍵盤輸入錯誤;
          • 控制條件編譯;
          • 可以對C語法做小的修改;
          1. 帶參數的宏

          帶參數的仍要遵循上述規則,區別只是宏名后面緊跟的圓括號中放置了參數,就像真正的函數那樣。

          #define <宏名>(<參數列表>) <宏體>

          注意參數列表中的參數必須是有效的c標識符,同時以,分隔

          算符優先級問題:

          #define COUNT(M) M*M
          int x=5;
          print(COUNT(x+1));
          print(COUNT(++X));
          //結果輸出:11   和42 而不是函數的輸出36

          注意:

          • 預編譯器只是進行簡單的文本替換,COUNT(x+1)被替換成COUNT(x+1x+1),5+15+1=11,而不是36
          • CUNT(++x)被替換成++x*++x即為6*7=42,而不是想要的6*6=36,連續前置自加加兩次

          解決辦法:

          • 用括號將整個替換文本及每個參數用括號括起來print(COUNT((x+1));
          • 即便是加上括號也不能解決第二種情況,所以解決辦法是盡量不使用++,-等符號;

          分號吞噬問題:

          #define foo(x) bar(x); baz(x)

          假設這樣調用:

          if (!feral)
              foo(wolf);

          將被宏擴展為:

          if (!feral)
              bar(wolf);
          baz(wolf);

          ==baz(wolf);==,不在判斷條件中,顯而易見,這是錯誤。如果用大括號將其包起來依然會有問題,例如

          #define foo(x)  { bar(x); baz(x); }
          if (!feral)
              foo(wolf);
          else
              bin(wolf);

          判斷語言被擴展成:

          if (!feral) {
              bar(wolf);
              baz(wolf);
          }>>++;++<<
          else
              bin(wolf);

          ==else==將不會被執行

          解決方法:通過==do{…}while(0)

          #define foo(x)  do{ bar(x); baz(x); }while(0)
          if (!feral)
              foo(wolf);
          else
              bin(wolf);

          被擴展成:

          #define foo(x)  do{ bar(x); baz(x); }while(0)
          if (!feral)
              do{ bar(x); baz(x); }while(0);
          else
              bin(wolf);

          注意:使用do{…}while(0)構造后的宏定義不會受到大括號、分號等的影響,總是會按你期望的方式調用運行。

          1. #運算符

          #的作用就是將#后邊的宏參數進行字符串的操作,也就是將#后邊的參數兩邊加上一對雙引號使其成為字符串。例如a是一個宏的形參,則替換文本中的#a被系統轉化為"a",這個轉換過程即為字符串化。

          #define TEST(param) #param
          
          char *pStr=TEST(123);
          printf("pSrt=%s\n",pStr);
          //輸出結果為字符  ”123“
          1. ##運算符

          ##運算符也可以用在替換文本中,它的作用起到粘合的作用,即將兩個宏參數連接為一個數

          #define TEST(param1,param2) (param1##param2)
          
          int num=TEST(13,59);
          printf("num=%d\n",num);
          //輸出結果為:num=1359
          1. VA_ARGS

          作用主要是為了方便管理軟件中的打印信息。在寫代碼或DEBUG時通常需要將一些重要參數打印出來,但在軟件發行的時候不希望有這些打印,這時就用到可變參數宏了。

           # define PR(...) printf(_VA_ARGS_)
          2 PR("hello world\n");
          3
          4 輸出結果:hello world

          2 一些建議

          • 雖然宏定義很靈活,并且通過彼此結合可以產生許多變形用法,但是C++/C程序員不要定義很復雜的宏,宏定義應該簡單而清晰。
          • 宏名采用大寫字符組成的單詞或其縮寫序列,并在各單詞之間使用“_”分隔。
          • 如果需要公布某個宏,那么該宏定義應當放置在頭文件中,否則放置在實現文件(.cpp)的頂部。
          • 不要使用宏來定義新類型名,應該使用typedef,否則容易造成錯誤。
          • 給宏添加注釋時請使用塊注釋(/* */),而不要使用行注釋。因為有些編譯器可能會把宏后面的行注釋理解為宏體的一部分。
          • 盡量使用const取代宏來定義符號常量。
          • 對于較長的使用頻率較高的重復代碼片段,建議使用函數或模板而不要使用帶參數的宏定義;而對于較短的重復代碼片段,可以使用帶參數的宏定義,這不僅是出于類型安全的考慮,而且也是優化與折衷的體現。
          • 盡量避免在局部范圍內(如函數內、類型定義內等)定義宏,除非它只在該局部范圍內使用,否則會損害程序的清晰性。

          3 宏的常見用法

          • 防止一個頭文件被重復包含
          #ifndef COMDEF_H
          #define COMDEF_H
          //頭文件內容
          #endif
          • 得到指定地址上的一個字節或字
          #define  MEM_B(x) (*((byte *)(x)))
          #define  MEM_W(x) (*((word *)(x)))
          • 求最大值和最小值
          #define  MAX(x,y) (((x)>(y)) ? (x) : (y))
          #define  MIN(x,y) (((x) < (y)) ? (x) : (y))
          • 得到一個field在結構體(struct)中的偏移量
          #define FPOS(type,field) ((dword)&((type *)0)->field)
          • 得到一個結構體中field所占用的字節數
          #define FSIZ(type,field) sizeof(((type *)0)->field)
          • 按照LSB格式把兩個字節轉化為一個Word
          #define FLIPW(ray) ((((word)(ray)[0]) * 256) + (ray)[1])
          • 得到一個字的高位和低位字節
          #define WORD_LO(xxx)  ((byte) ((word)(xxx) & 255))
          #define WORD_HI(xxx)  ((byte) ((word)(xxx) >> 8))
          • 將一個字母轉換為大寫
          #define UPCASE(c) (((c)>='a' && (c) <='z') ? ((c) – 0×20) : (c))
          • 判斷字符是不是10進制的數字
          #define  DECCHK(c) ((c)>='0' && (c)<='9')
          • 判斷字符是不是16進制的數字
          #define HEXCHK(c) (((c) >='0' && (c)<='9') ((c)>='A' && (c)<='F') \
          ((c)>='a' && (c)<='f'))
          • 防止溢出的一個方法
          #define INC_SAT(val) (val=((val)+1>(val)) ? (val)+1 : (val))
          • 返回數組元素的個數
          #define ARR_SIZE(a)  (sizeof((a))/sizeof((a[0])))

          參考資料

          1. http://www.360doc.com/content/13/0125/13/10906019_262310086.shtml
          2. 高質量程序設計指南C++/C語言第3版
          3. https://www.cnblogs.com/southcyy/p/10155049.html

          主站蜘蛛池模板: 一区二区国产精品| 国内国外日产一区二区| 人妻视频一区二区三区免费| 国产小仙女视频一区二区三区| 一区国产传媒国产精品| 一区二区高清视频在线观看| 国产成人一区二区三区免费视频| 国产精品一区二区毛卡片 | 蜜桃无码一区二区三区| 国产成人精品亚洲一区 | 精品无人区一区二区三区在线| 精彩视频一区二区| 蜜桃传媒一区二区亚洲AV| 亚洲国产一区二区三区青草影视| 国产观看精品一区二区三区| 精品视频在线观看一区二区三区| 国产午夜精品一区二区三区极品 | 免费无码AV一区二区| 99偷拍视频精品一区二区| 亚洲一区二区三区亚瑟| 亚洲一区二区三区亚瑟| 成人精品一区二区户外勾搭野战| 日韩人妻无码一区二区三区久久| 久久精品无码一区二区三区日韩 | 日韩精品无码Av一区二区| 秋霞电影网一区二区三区| 国产一区二区三区樱花动漫| 国产精品一区二区电影| 免费在线观看一区| 黑人大战亚洲人精品一区| 一区二区视频传媒有限公司| 久久久精品一区二区三区 | 亚洲视频一区二区| 亚洲日本一区二区三区在线| 亚洲国产精品第一区二区| 亚洲av无码一区二区三区观看| 国产精品一区在线麻豆| 狠狠综合久久av一区二区| 日韩av片无码一区二区三区不卡 | 精品无码一区在线观看| 麻豆果冻传媒2021精品传媒一区下载 |