Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
從vue3開始vue引入了宏,比如defineProps、defineEmits等。我們每天寫vue代碼時(shí)都會(huì)使用到這些宏,但是你有沒有思考過vue中的宏到底是什么?為什么這些宏不需要手動(dòng)從vue中import?為什么只能在setup頂層中使用這些宏?
要回答上面的問題,我們先來了解一下從一個(gè)vue文件到渲染到瀏覽器這一過程經(jīng)歷了什么?
我們的vue代碼一般都是寫在后綴名為vue的文件上,顯然瀏覽器是不認(rèn)識(shí)vue文件的,瀏覽器只認(rèn)識(shí)html、css、jss等文件。所以第一步就是通過webpack或者vite將一個(gè)vue文件編譯為一個(gè)包含render函數(shù)的js文件。然后執(zhí)行render函數(shù)生成虛擬DOM,再調(diào)用瀏覽器的DOM API根據(jù)虛擬DOM生成真實(shí)DOM掛載到瀏覽器上。
我們先來看看vue官方的解釋:
宏是一種特殊的代碼,由編譯器處理并轉(zhuǎn)換為其他東西。它們實(shí)際上是一種更巧妙的字符串替換形式。
通過前面我們知道了vue 文件渲染到瀏覽器上主要經(jīng)歷了兩個(gè)階段。
第一階段是編譯時(shí),也就是從一個(gè)vue文件經(jīng)過webpack或者vite編譯變成包含render函數(shù)的js文件。此時(shí)的運(yùn)行環(huán)境是nodejs環(huán)境,所以這個(gè)階段可以調(diào)用nodejs相關(guān)的api,但是沒有在瀏覽器環(huán)境內(nèi)執(zhí)行,所以不能調(diào)用瀏覽器的API。
第二階段是運(yùn)行時(shí),此時(shí)瀏覽器會(huì)執(zhí)行js文件中的render函數(shù),然后依次生成虛擬DOM和真實(shí)DOM。此時(shí)的運(yùn)行環(huán)境是瀏覽器環(huán)境內(nèi),所以可以調(diào)用瀏覽器的API,但是在這一階段中是不能調(diào)用nodejs相關(guān)的api。
而宏就是作用于編譯時(shí),也就是從vue文件編譯為js文件這一過程。
舉個(gè)defineProps的例子:在編譯時(shí)defineProps宏就會(huì)被轉(zhuǎn)換為定義props相關(guān)的代碼,當(dāng)在瀏覽器運(yùn)行時(shí)自然也就沒有了defineProps宏相關(guān)的代碼了。所以才說宏是在編譯時(shí)執(zhí)行的代碼,而不是運(yùn)行時(shí)執(zhí)行的代碼。
我們來看一個(gè)實(shí)際的例子,下面這個(gè)是我們的源代碼:
<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>
在這個(gè)例子中我們使用defineProps宏定義了一個(gè)類型為String,屬性名為content的props,并且在template中渲染content的內(nèi)容。
我們接下來再看看編譯成js文件后的代碼,代碼我已經(jīng)進(jìn)行過簡(jiǎn)化:
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文件主要由兩部分組成,第一部分為執(zhí)行defineComponent函數(shù)生成一個(gè) __sfc__ 對(duì)象,第二部分為一個(gè)render函數(shù)。render函數(shù)不是我們這篇文章要講的,我們主要來看看這個(gè)__sfc__對(duì)象。
看到defineComponent是不是覺得很眼熟,沒錯(cuò)這個(gè)就是vue提供的API中的 definecomponent函數(shù)。這個(gè)函數(shù)在運(yùn)行時(shí)沒有任何操作,僅用于提供類型推導(dǎo)。這個(gè)函數(shù)接收的第一個(gè)參數(shù)就是組件選項(xiàng)對(duì)象,返回值就是該組件本身。所以這個(gè)__sfc__對(duì)象就是我們的vue文件中的script代碼經(jīng)過編譯后生成的對(duì)象,后面再通過__sfc__.render=render將render函數(shù)賦值到組件對(duì)象的render方法上面。
我們這里的組件選項(xiàng)對(duì)象經(jīng)過編譯后只有兩個(gè)了,分別是props屬性和setup方法。明顯可以發(fā)現(xiàn)我們?cè)驹趕etup里面使用的defineProps宏相關(guān)的代碼不在了,并且多了一個(gè)props屬性。沒錯(cuò)這個(gè)props屬性就是我們的defineProps宏生成的。
我們?cè)賮砜匆粋€(gè)不在setup頂層調(diào)用defineProps的例子:
<script setup lang="ts">
import {ref} from "vue"
const title=ref("title")
if (title.value) {
const props=defineProps({
content: String,
});
}
</script>
運(yùn)行這個(gè)例子會(huì)報(bào)錯(cuò):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的頂層調(diào)用defineProps宏,在編譯時(shí)就不會(huì)將defineProps宏替換為定義props相關(guān)的代碼,而是原封不動(dòng)的輸出回來。在運(yùn)行時(shí)執(zhí)行到這行代碼后,由于我們沒有任何地方定義了defineProps函數(shù),所以就會(huì)報(bào)錯(cuò)defineProps is not defined。
現(xiàn)在我們能夠回答前面提的三個(gè)問題了。
作者:歐陽碼農(nóng)
鏈接:https://juejin.cn/post/7335721246931189795
文鏈接:The Rust Programming Language
作者:rust 團(tuán)隊(duì)
譯文首發(fā)鏈接:zhuanlan.zhihu.com/p/516660154
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
中間加了一些對(duì)于 JavaScript 開發(fā)者有幫助的注解。在學(xué)習(xí) Rust 的代碼時(shí),尤其是有一些經(jīng)驗(yàn)的開發(fā)者,一般都會(huì)去看一些相對(duì)簡(jiǎn)單的庫,或者項(xiàng)目去學(xué)習(xí)。Rust 的原生語法本身并不復(fù)雜,有一定的 TypeScript 經(jīng)驗(yàn)的開發(fā)者,應(yīng)該通過看一些教程,都能開始熟悉基本的語法。反而是宏相關(guān)內(nèi)容,雖然就像本文寫的,大部分開發(fā)者不需要自己去開發(fā)宏,但是大家使用宏,和看懂別人代碼的主體邏輯,幾乎是繞不開宏的。雖然宏在 rust 里屬于“高級(jí)”內(nèi)容,但是因?yàn)槠浜?rust 本身語法的正交性,Hugo 認(rèn)為,反而應(yīng)該早一些學(xué)習(xí)。并且,如果一個(gè) JavaScript 開發(fā)者之前接觸過代碼生成器、babel,對(duì)這一章的內(nèi)容反而會(huì)比較親切。
Derive 宏、attribute 宏特別像 rust 里的裝飾器。從作用上,和一般庫提供的接口來看,也特別像。所以如果之前有裝飾器的經(jīng)驗(yàn)的開發(fā)者,對(duì)這一章節(jié)應(yīng)該也會(huì)比較親切。
整本書經(jīng)常使用 println! 宏,但是還沒介紹宏這個(gè)機(jī)制。宏實(shí)際上是 rust 的一系列特性的合集:聲明式宏(declarative macro):使用 marco_rules!聲明的代碼和三種過程式宏(procedural macros):
我們一個(gè)個(gè)來討論這些內(nèi)容,但是首先,我們看既然我們已經(jīng)有了函數(shù),我們?yōu)槭裁葱枰@些特性。
基本上,宏是指一些代碼可以生成另一些代碼,這一塊的技術(shù)一般稱為元編程(Hugo 注:代碼生成器也屬于這一類技術(shù))。在附錄 C,我們討論了 derive 屬性,可以幫助你生成一系列的 trait。整本書我們也在用 println! 和 vec! 宏。這些宏在編譯時(shí),都會(huì)展開成為代碼,這樣你就不需要手寫這些代碼。
元編程可以幫助你減少手寫和維護(hù)的代碼量,當(dāng)然,函數(shù)也能幫助你實(shí)現(xiàn)類似的功能。但是,宏有函數(shù)沒有的威力。
函數(shù)必須聲明如慘的數(shù)量和種類。而宏,在另一方面,可以接收任意數(shù)量的參數(shù):我們可以調(diào)用 println!("hello"),也可以調(diào)用 println!("hello {}", name)。并且,宏是在編譯階段展開了代碼,所以一個(gè)宏可以在編譯時(shí)為一個(gè)類型實(shí)現(xiàn)一個(gè) trait。一個(gè)函數(shù)就不可以。因?yàn)楹瘮?shù)是在運(yùn)行時(shí)調(diào)用,而 trait 實(shí)現(xiàn)只可以發(fā)生在編譯時(shí)。(Hugo 注:JS 可以實(shí)現(xiàn)運(yùn)行時(shí)生成 trait(這里只是套用 rust 的概念),當(dāng)然,如果你需要的話。動(dòng)態(tài)語言在某些場(chǎng)景能很簡(jiǎn)單實(shí)現(xiàn)非常強(qiáng)大的功能。)
宏不好的地方在于,宏很復(fù)雜,因?yàn)槟阋?rust 代碼寫 rust 代碼(Hugo 注:任何元編程都不是簡(jiǎn)單的事兒,包括 JS 里的。)。因?yàn)檫@種間接性,宏的代碼要更難讀、難理解、難維護(hù)。(Hugo 注:個(gè)人學(xué) rust,感覺最難不是生命周期,因?yàn)樯芷诘膯栴},可以通過用一些庫繞過去,或者無腦 clone,如果是應(yīng)用程序,則可以通過使用 orm 和數(shù)據(jù)庫來繞過很多生命周期的問題。反而是宏,因?yàn)樯晕⒂悬c(diǎn)規(guī)模的代碼的,都有一大堆宏。宏最難的不是語法,而是作者的意圖,因?yàn)楸举|(zhì)是他造了一套 DSL)
另一個(gè)和函數(shù)不一樣的地方是,宏需要先定義或者引入作用域,而函數(shù)可以在任何地方定義和使用。
在 Rust 中使用最廣泛的宏是聲明式宏。它們有時(shí)也被稱為 “macros by example”、“macro_rules!宏” 或者就是 “macros”。聲明式宏寫起來和 Rust 的 match 語法比較像。在第六章里講到,match 語法是一種流程控制語法,接收一個(gè)表達(dá)式,然后和結(jié)果進(jìn)行模式匹配,然后執(zhí)行匹配到結(jié)果的代碼。宏也會(huì)做類似的比較:在這種情況下,傳入的參數(shù)是 rust 的合法的語法代碼,然后通過宏的規(guī)則,和書寫好的模版,在編譯時(shí)轉(zhuǎn)換成代碼。
定義聲明式宏的語法是 macro_rule!。下面我來用 vec! 來介紹這一機(jī)制。第八章有關(guān)于 vec! 的內(nèi)容。例如,創(chuàng)建一個(gè)新的 vector,包含 3 個(gè) integer:
#![allow(unused)]
fn main() {
let v: Vec<u32>=vec![1, 2, 3];
}
我們可以通過 vec! 宏來創(chuàng)建任意類型的 vector,例如 2 個(gè) integer,或者五個(gè) string slice.
我們不能用函數(shù)去實(shí)現(xiàn)這個(gè)功能,因?yàn)槲覀儾恢垒斎氲膮?shù)個(gè)數(shù)。(Hugo 注:這一點(diǎn)和 JS 非常不一樣,我們寫 JS,已經(jīng)習(xí)慣了可以傳任意變量。當(dāng)然如果你熟悉 TS,和 TS 有一些類似。Rust 雖然也有范型,但是和 TS 的范型非常不一樣。這里不一樣,我主要指關(guān)注點(diǎn),因?yàn)?Rust 比較大一部分都是函數(shù)式的代碼,每個(gè)函數(shù)一般都承載非常細(xì)粒度的功能,一般每個(gè)函數(shù)都處理好了自己的輸入、輸出、報(bào)錯(cuò),所有可能性都寫好了,寫 rust 有一種在填狀態(tài)機(jī)的錯(cuò)覺。。TS 有的代碼也有這種感覺。)
一個(gè)簡(jiǎn)化的 vec! 宏:
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* )=> {
{
let mut temp_vec=Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
注意:實(shí)際的 vec! 的聲明,還包括了提前分配合適的內(nèi)存。這里簡(jiǎn)化這個(gè)代碼,為了更好的講聲明式宏的概念。
#[macro_export] 標(biāo)注指明了這個(gè)宏在 crate 的作用域里可用。沒有這個(gè)標(biāo)注,宏不會(huì)被帶入到作用域里。
macro_rules! 后面就是宏的名字。這里只有一種模式匹配的邊(arm):( (( (x:expr ),* ) ,=> 后面是這個(gè)模式對(duì)應(yīng)要生成的代碼。如果這個(gè)模式匹配成功,對(duì)應(yīng)的代碼和輸入的參數(shù)組成的代碼就會(huì)生成在最終的代碼中。因?yàn)檫@里只有一種邊,所以只有這一種可以匹配的條件。不符合這個(gè)條件的輸入,都會(huì)報(bào)錯(cuò)。一般復(fù)雜的宏,都會(huì)有多個(gè)邊。
這里匹配的規(guī)則和 match 是不一樣的,因?yàn)檫@里的語法匹配的是 rust 的語法,而不是 rust 的類型,或者值。更全的宏匹配語法,見文檔。
對(duì)于 宏的輸入條件 ( (( (x:expr ),* ),()內(nèi)部是匹配的語法,expr表示所有Rust的表達(dá)式。() 內(nèi)部是匹配的語法,expr 表示所有 Rust 的表達(dá)式。()內(nèi)部是匹配的語法,expr表示所有Rust的表達(dá)式。() 后面的都喊表示這個(gè)變量后面有可能有逗號(hào),* 表示前面的模式會(huì)出現(xiàn)一次或者多次。(Hugo 注:像不像正則?宏語法其實(shí)挺簡(jiǎn)單的,不要被高級(jí)唬住了。當(dāng)然,宏還是難的,宏要考慮的問題本身是一個(gè)復(fù)雜的問題。)
當(dāng)我們調(diào)用:vec![1, 2, 3]; 時(shí),$x 模式會(huì)匹配 3 個(gè)表達(dá)式 1 , 2 和 3。
現(xiàn)在我們看一下和這個(gè)邊匹配的生成代碼的部分:
{
{
let mut temp_vec=Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
在 ()里的tempvec.push(() 里的 temp_vec.push(()里的tempvec.push(x); 就是生成的代碼的部分。* 號(hào)仍然表示生成零個(gè)和多個(gè),這個(gè)匹配的具體個(gè)數(shù),要看匹配條件命中的個(gè)數(shù)。
當(dāng)我們調(diào)用:vec![1, 2, 3]; 時(shí),生成了這個(gè)代碼。(Hugo 注:Cargo 有 expand 插件,對(duì)于聲明宏,多看看展開基本就能學(xué)會(huì)了。)
{
let mut temp_vec=Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
你傳任意參數(shù),最后就生成符合上面條件的代碼。
有一些 macro_rules! 的奇怪的邊界例子。在未來,Rust 會(huì)有第二種聲明式宏,和現(xiàn)在的機(jī)制類似,但是會(huì)解決這些邊界問題。在那一次升級(jí)后,macro_rules! 會(huì)被棄用。(Hugo 注:Rust 仍然是非常年輕的語言,做好隨時(shí)接受改變的準(zhǔn)備)記住這些,當(dāng)然另一個(gè)事實(shí)是,大部分 Rust 程序員更多是宏的使用者,而不是開發(fā)者,我們不會(huì)在深入討論 macro_rules!。如果你對(duì)這塊特別感興趣。請(qǐng)閱讀《“The Little Book of Rust Macros”》。(Hugo 注:站在入門的角度,能知道機(jī)制去使用就 ok 了。在絕大部分入門的情況下,函數(shù)以及使用 crates.io 上的宏都能滿足你的需求。)
第二種宏是過程宏,表現(xiàn)形式更像函數(shù)(過程的一種類型)。過程宏的入?yún)⑹且恍┐a,你可以操作這些代碼,然后襯衫一些代碼。(Hugo:從結(jié)果看和聲明宏沒區(qū)別,其實(shí)站在 JS 的角度,更像是 babel,你可以根據(jù)輸入的 token 做變換)
雖然過程宏有三種:custom derive、attribute-like 和 function-like,但是原理都是一樣的。
如果要?jiǎng)?chuàng)建過程宏,定義的部分需要在自己的 crate 里,并且要定義特殊的 crate 類型。(Hugo 注:相當(dāng)于定義了一個(gè) babel 插件,只不過有一套 rust 自己的體系。這些宏會(huì)在編譯的時(shí)候,按照書寫的規(guī)則,轉(zhuǎn)成對(duì)應(yīng)的代碼。所有的宏,都是代碼生成的手段,輸入是代碼,輸入是代碼。)這種設(shè)計(jì),我們有可能會(huì)在未來消除。
下面是一個(gè)過程宏的例子:
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
過程宏接收一個(gè) TokenStream,輸出一個(gè) TokenStream。TokenStream 類型定義在 proc_macro 里,表示一系列的 tokens。這個(gè)就是這種宏的核心機(jī)制,輸入的代碼(會(huì)被 rust) 轉(zhuǎn)成 TokenStream,然后做一些按照業(yè)務(wù)邏輯的操作,最后生成 TokenStream。這個(gè)函數(shù)也可以疊加其他的屬性宏(#[some_attribute], 看起來像裝飾器的邏輯,也可以理解為一種鏈?zhǔn)秸{(diào)用),可以在一個(gè) crate 里定義多個(gè)過程。(Hugo 注:搞過 babel 的同學(xué)肯定很熟悉,一樣的味道。沒搞過的同學(xué),強(qiáng)烈建議先學(xué)學(xué) babel。)
下面我們來看看不同類型的過程宏。首先從自定義 derive 宏開始,然后我們介紹這種宏和其他幾種的區(qū)別。
我們創(chuàng)建一個(gè) crate 名字叫 hello_macro,定義一個(gè) HelloMacro 的 trait,關(guān)聯(lián)的函數(shù)名字叫 hello_macro。通過使用這個(gè)宏,用戶的結(jié)構(gòu)可以直接獲得默認(rèn)定義的 hello_macro 函數(shù),而不需要實(shí)現(xiàn)這個(gè) trait。默認(rèn)的 hello_macro 可以打印 Hello, Macro! My name is TypeName!,其中 TypeName 是實(shí)現(xiàn)這個(gè) derive 宏的結(jié)構(gòu)的類型名稱。
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
創(chuàng)建這個(gè)宏的過程如下,首先
$ cargo new hello_macro --lib
然后定義 HelloMacro trait
pub trait HelloMacro {
fn hello_macro();
}
這樣我們就有了一個(gè) trait,和這個(gè)triat 的函數(shù)。用戶可以通過這個(gè) trait 直接實(shí)現(xiàn)對(duì)應(yīng)的函數(shù)。
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();
}
但是,用戶需要每次都實(shí)現(xiàn)一遍 hello_macro。如果 hello_macro 的實(shí)現(xiàn)都差不多,就可以通過 derive 宏來是實(shí)現(xiàn)。
因?yàn)?Rust 沒有反射機(jī)制,我們不可以在執(zhí)行時(shí)知道對(duì)應(yīng)類型的名字。我們需要在編譯時(shí)生成對(duì)應(yīng)的代碼。
下一步,定義過程宏。在這個(gè)文章編寫時(shí),過程宏需要在自己的 crates 里。最終,這個(gè)設(shè)計(jì)可能改變。關(guān)于 宏 crate 的約定是:對(duì)于一個(gè)名為 foo 的 crate,自定義 drive 宏的crate 名字為 foo_derive。我們?cè)?hello_macro 項(xiàng)目中創(chuàng)建 hello_macro_derive crate。
$ cargo new hello_macro_derive --lib
我們的兩個(gè)的 crate 關(guān)聯(lián)緊密,所以我們?cè)?hello_macro crate 里創(chuàng)建這個(gè) crate。如果我們要改變 hello_macro 的定義,我們同樣也要更改 hello_macro_derive 的定義。這兩個(gè) crates 要隔離發(fā)布。當(dāng)用戶使用時(shí),要同時(shí)添加這兩個(gè)依賴。為了簡(jiǎn)化依賴,我們可以讓 hello_macro 使用 hello_macro_derive 作為依賴,然后導(dǎo)出這個(gè)依賴。但是,這樣,如果用戶不想使用 hello_macro_derive,也會(huì)自動(dòng)添加上這個(gè)依賴。
下面開始創(chuàng)建 hello_macro_derive,作為一個(gè)過程宏 crate。需要添加依賴 syn 和 quote。下面是這個(gè) crate 的 Cargo.toml。
[lib]
proc-macro=true
[dependencies]
syn="1.0"
quote="1.0"
在 lib.rs 里添加下述代碼。注意,這個(gè)代碼如果不增加 impl_hello_macro 的實(shí)現(xiàn)是通不過編譯的。
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 函數(shù)里,這個(gè)函數(shù)主要負(fù)責(zé)處理 TokenStream,另一部分在 impl_hello_macro,這里負(fù)責(zé)轉(zhuǎn)換語法樹:這樣編寫過程宏可以簡(jiǎn)單一些。在絕大部分過程宏立,對(duì)于前者的過程一般都是一樣的。一般來說,真正的區(qū)別在 impl_hello_macro,這里的邏輯一般是一個(gè)過程宏的業(yè)務(wù)決定的。
我們引入了三個(gè) crates: proc_macro, syn 和 quote。proc_macro 內(nèi)置在 rust 立,不需要在 Cargo.toml 中引入。proc_macro 實(shí)際是 rust 編譯器的一個(gè)接口,用來讀取和操作 Rust 代碼。
syn crate 把 Rust 代碼從字符串轉(zhuǎn)換為可以操作的結(jié)構(gòu)體。quote crate 把 syn 數(shù)據(jù)在轉(zhuǎn)回 Rust 代碼。這些 Crate 可以極大簡(jiǎn)化過程宏的編寫:寫一個(gè) Rust 代碼的 full parser 可不是容易的事兒!
當(dāng)在一個(gè)類型上標(biāo)注 [derive(HelloMacro)] 時(shí),會(huì)調(diào)用 hello_macro_derive 函數(shù)。之所以會(huì)有這樣的行為,是因?yàn)樵诙x hello_macro_derive 時(shí),標(biāo)注了 #[proc_macro_derive(HelloMacro)] 在函數(shù)前面。
hello_macro_derive 會(huì)把輸入從 TokenStream 轉(zhuǎn)換為一個(gè)我們可以操作的數(shù)據(jù)結(jié)構(gòu)。這就是為什么需要引入 syn 。sync 的 parse 函數(shù)會(huì)把 TokenStream 轉(zhuǎn)換為 DeriveInput。
DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
上述這個(gè)結(jié)構(gòu)的意思是:正在處理的是 ident(identifier, 意味著名字)為 Pancakes 的 unit struct。其他的字段表示其余的 Rust 代碼。如果想了解更詳細(xì)的內(nèi)容,請(qǐng)參考。
接下來,我們就要開始定義 impl_hello_macro。這個(gè)函數(shù)實(shí)現(xiàn)了添加到 Rust 代碼上的函數(shù)。在我們做之前,注意 derive macro 的輸出也是 TokenStream。返回的 TokenStream 就是添加完代碼以后的代碼。當(dāng)編譯 crate 時(shí),最終的代碼,就是處理完成的代碼了。
你也許也會(huì)發(fā)現(xiàn),這里調(diào)用 syn::parse 時(shí)使用了 unwrap,如果報(bào)錯(cuò)就中斷。這里必須這么做,因?yàn)樽罱K返回的是 TokenStream,而不是 Result。這里是為了簡(jiǎn)化代碼說明這個(gè)問題。在生產(chǎn)代碼,你應(yīng)該處理好報(bào)錯(cuò),提供更詳細(xì)的報(bào)錯(cuò)信息,例如使用 panic! 或者 expect。
下面是代碼:
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
// 通過&ast.ident 獲取類型的名字
let name=&ast.ident;
// 使用 quote 宏,可以使用 rust 語法來定義要實(shí)現(xiàn)的 trait(Hugo 注:JS 要有這個(gè)就好了)
let gen=quote! {
// #name 是 quote! 的模版語法,會(huì)自動(dòng)替換為這個(gè)變量里的值
impl HelloMacro for #name {
fn hello_macro() {
// 這里把對(duì)應(yīng)的 struct 名字轉(zhuǎn)換好了,stringfy!把值轉(zhuǎn)換為字符串
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
// 轉(zhuǎn)換為最終的 TokenStream
gen.into()
}
通過上面的代碼,cargo build 就可以正常工作了。如果要使用這個(gè)代碼,需要把兩個(gè)依賴都加上。
hello_macro={ path="../hello_macro" }
hello_macro_derive={ path="../hello_macro/hello_macro_derive" }
現(xiàn)在執(zhí)行下面的代碼,就可以看到 Hello, Macro! My name is Pancakes!
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
下一步,我們來探索其他類型的過程宏。
屬性宏和 derive 宏類似,但是可以創(chuàng)造除了 derive 意外的屬性。derive 只能作用于 structs 和 enums,屬性宏可以作用于其他的東西,比如函數(shù)。下面是一個(gè)屬性宏的例子:例如你制作了一個(gè)名為 route 的屬性宏來在一個(gè)web 框架中標(biāo)注函數(shù)。
#[route(GET, "/")]
fn index() {
#[route] 是框架定義的過程宏。定義這個(gè)宏的函數(shù)類似:
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
這里,有兩個(gè)參數(shù),類型都是 TokenStream。第一個(gè)是屬性的內(nèi)容,GET, "/" 部分,第二個(gè)是標(biāo)注屬性宏傳入的語法部分,在這個(gè)例子里,就是剩下的 fn index() {}。
工作原理和 derive 宏是一樣的。
函數(shù)宏的使用比較像調(diào)用一個(gè) rust 函數(shù)。函數(shù)宏有點(diǎn)像 macro_rules! ,能提供比函數(shù)更高的靈活性。例如,可以接受未知個(gè)數(shù)的參數(shù)。但是,macro_rules! 只能使用在上述章節(jié)的匹配型的語法。而函數(shù)宏接受 TokenStream 參數(shù)作為入?yún)?,和其他過程宏一樣,可以做任何變換,然后返回 TokenStream。下面是一個(gè)函數(shù)宏 sql!
let sql=sql!(SELECT * FROM posts WHERE id=1);
這個(gè)宏接受 SQL 語句,可以檢查這個(gè) SQL 的語法是否正確,這種功能比 macro_rules! 提供的要復(fù)雜的多。這個(gè) sql! 的宏可以定義為:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
這個(gè)定義和自定義 derive 宏類似:接受括號(hào)內(nèi)的 tokens,返回生成的代碼。
好了,現(xiàn)在你有了一些可能不常用的 Rust 新工具,但是你要知道的是,在需要的場(chǎng)合,他們的運(yùn)行原理是什么。我們介紹了一些復(fù)雜的話題,當(dāng)你在錯(cuò)誤處理或者別人的代碼里看到這些宏時(shí),可以認(rèn)出這些概念和語法??梢允褂眠@一章的內(nèi)容作為解決這些問題的索引。
概述
在工程規(guī)模較小,不是很復(fù)雜,與硬件結(jié)合緊密,要求移植性的時(shí)候,可采用宏定義簡(jiǎn)化編程,增強(qiáng)程序可讀性。
當(dāng)宏作為常量使用時(shí),C程序員習(xí)慣在名字中只使用大寫字母。但是并沒有如何將用于其他目的的宏大寫的統(tǒng)一做法。由于宏(特別是帶參數(shù)的宏)可能是程序中錯(cuò)誤的來源,所以一些程序員更喜歡使用大寫字母來引起注意。
無參宏的宏名后不帶參數(shù),其定義的一般形式為:
#define 標(biāo)識(shí)符 字符串
// 不帶參數(shù)的宏定義
#define MAX 10
注意:不要在宏定義中放置任何額外的符號(hào),比如"="或者尾部加";"
使用#define來為常量命名一些優(yōu)點(diǎn):
帶參數(shù)的仍要遵循上述規(guī)則,區(qū)別只是宏名后面緊跟的圓括號(hào)中放置了參數(shù),就像真正的函數(shù)那樣。
#define <宏名>(<參數(shù)列表>) <宏體>
注意參數(shù)列表中的參數(shù)必須是有效的c標(biāo)識(shí)符,同時(shí)以,分隔
算符優(yōu)先級(jí)問題:
#define COUNT(M) M*M
int x=5;
print(COUNT(x+1));
print(COUNT(++X));
//結(jié)果輸出:11 和42 而不是函數(shù)的輸出36
注意:
解決辦法:
分號(hào)吞噬問題:
#define foo(x) bar(x); baz(x)
假設(shè)這樣調(diào)用:
if (!feral)
foo(wolf);
將被宏擴(kuò)展為:
if (!feral)
bar(wolf);
baz(wolf);
==baz(wolf);==,不在判斷條件中,顯而易見,這是錯(cuò)誤。如果用大括號(hào)將其包起來依然會(huì)有問題,例如
#define foo(x) { bar(x); baz(x); }
if (!feral)
foo(wolf);
else
bin(wolf);
判斷語言被擴(kuò)展成:
if (!feral) {
bar(wolf);
baz(wolf);
}>>++;++<<
else
bin(wolf);
==else==將不會(huì)被執(zhí)行
解決方法:通過==do{…}while(0)
#define foo(x) do{ bar(x); baz(x); }while(0)
if (!feral)
foo(wolf);
else
bin(wolf);
被擴(kuò)展成:
#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)構(gòu)造后的宏定義不會(huì)受到大括號(hào)、分號(hào)等的影響,總是會(huì)按你期望的方式調(diào)用運(yùn)行。
#的作用就是將#后邊的宏參數(shù)進(jìn)行字符串的操作,也就是將#后邊的參數(shù)兩邊加上一對(duì)雙引號(hào)使其成為字符串。例如a是一個(gè)宏的形參,則替換文本中的#a被系統(tǒng)轉(zhuǎn)化為"a",這個(gè)轉(zhuǎn)換過程即為字符串化。
#define TEST(param) #param
char *pStr=TEST(123);
printf("pSrt=%s\n",pStr);
//輸出結(jié)果為字符 ”123“
##運(yùn)算符也可以用在替換文本中,它的作用起到粘合的作用,即將兩個(gè)宏參數(shù)連接為一個(gè)數(shù)
#define TEST(param1,param2) (param1##param2)
int num=TEST(13,59);
printf("num=%d\n",num);
//輸出結(jié)果為:num=1359
作用主要是為了方便管理軟件中的打印信息。在寫代碼或DEBUG時(shí)通常需要將一些重要參數(shù)打印出來,但在軟件發(fā)行的時(shí)候不希望有這些打印,這時(shí)就用到可變參數(shù)宏了。
# define PR(...) printf(_VA_ARGS_)
2 PR("hello world\n");
3
4 輸出結(jié)果:hello world
#ifndef COMDEF_H
#define COMDEF_H
//頭文件內(nèi)容
#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))
#define FPOS(type,field) ((dword)&((type *)0)->field)
#define FSIZ(type,field) sizeof(((type *)0)->field)
#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))
#define DECCHK(c) ((c)>='0' && (c)<='9')
#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])))
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。