整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

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