互联网站,上海浦东发布官网,用股票代码做网站的,怎么做抽奖网站宏
在 rust 中#xff0c;我们一开始就在使用宏#xff0c;例如 println!, vec!, assert_eq! 等。看起来宏和函数在使用时只是多了一个 !。实际上这些宏都是声明式宏#xff08;也叫示例宏或macro_rules!#xff09;#xff0c;rust 还支持过程宏#xff0c;过程宏为我们…宏
在 rust 中我们一开始就在使用宏例如 println!, vec!, assert_eq! 等。看起来宏和函数在使用时只是多了一个 !。实际上这些宏都是声明式宏也叫示例宏或macro_rules!rust 还支持过程宏过程宏为我们提供了强大的元编程工具。
声明式宏
声明式宏类似于 match 匹配。它可以将表达式的结果与多个模式进行匹配。一旦匹配成功那么该模式相关联的代码将被展开。和 match 不同的是宏里的值是一段 rust 源代码。所有这些都发生在编译期并没有运行期的性能损耗。下面是一个例子
// 声明一个add宏
macro_rules! add {($a: expr, $b: expr) {$a $b};
}fn main() {let a 10;let b 22;let _res add!(a, b);let _res add!(a1, b);let _res add!(a*2, b3);
}我们需要一个类似于 GCC -E 的方式来查看一下预处理阶段之后的代码。cargo-expand 正好提供了相应的功能。使用 cargo 安装 cargo-expand 即可。
cargo install cargo-expand安装 cargo-expand 之后可以使用 cargo expand 命令来查看声明式宏是如何被展开的。上面的代码在执行cargo expand之后输出如下所示
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
fn main() {let a 10;let b 22;let _res a b;let _res a 1 b;let _res a * 2 (b 3);
}可以看到每一个 _res 的右边都被展开了并且如果传入的参数是一个表达式则会将整个表达式作为一个整体传递给宏。这就是某些地方提到的“Hygienic Macros”有些地方也翻译为卫生宏翻译的很抽象。最后一行代码中传入的b3被当做了一个整体。如果是在C/C中不会自动将表达式作为整体而是直接进行字符串替换。而 Rust 编译器会自动处理变量名和作用域确保宏展开后的代码不会引入未预料的变量冲突。下面是一个C/C中使用宏的例子。
#includestdio.h
#define ADD(a, b) a b;int main() {int a 10;int b 22;int _res ADD(a, b)_res ADD(a1, b)_res ADD(a*2, b3)
} 同样我们使用 gcc -E main.c 来获取预处理之后的代码。由于展开之后的代码非常得多我们只放上 main 函数中展开的部分。
int main() {int a 10;int b 22;int _res a b;_res a1 b;_res a*2 b3;
}可以看到调用的代码展开之后并没有将 b3 作为一个整体来处理而是简单的进行替换。因此我们在 C/C 中编写宏要特别注意宏参数在使用的时候必须加上括号。现在我们来修复上面 C/C 代码中的宏。
#includestdio.h
#define ADD(a, b) (a) (b);int main() {int a 10;int b 22;int _res ADD(a, b)_res ADD(a1, b)_res ADD(a*2, b3)
} 这样我们在使用宏的时候就避免了意外结果的发生。这样展开之后的代码如下所示
int main() {int a 10;int b 22;int _res (a) (b);_res (a1) (b);_res (a*2) (b3);
}我们接着来定义我们自己的 my_vec! 宏, 来对声明式宏的相关语法做一个解释。
macro_rules! my_vec {// 匹配 my_vec![]() {std::vec::Vec::new()};// 匹配 my_vec, *) {// 这段代码需要用{}包裹起来因为宏需要展开这样能保证作用域正常不影响外部。这也是rust的宏是 Hygienic Macros 的体现。 // 而 C/C 的宏不强制要求但是如果遇到代码片段在 C/C 中也应该使用{}包裹起来。{let mut v std::vec::Vec::new();$(v.push($el);)*v}};// 匹配 my_vec {std::vec::from_elem($el, $n)};
}由于宏要在调用的地方展开我们无法预测调用者的环境是否已经做了相关的 use所以我们使用的代码最好带着完整的命名空间。在声明宏中条件捕获的参数使用 $ 开头的标识符来声明。每个参数都需要提供类型这里 expr 代表表达式所以 $el:expr 是说把匹配到的表达式命名为 $el。$(...),* 告诉编译器可以匹配任意多个以逗号分隔的表达式然后捕获到的每一个表达式可以用 $el 来访问。由于匹配的时候匹配到一个 $(...)* 我们可以不管分隔符在执行的代码块中我们也要相应地使用 $(...)* 展开。所以这句 $(v.push($el);)* 相当于匹配出多少个 $el 就展开多少句 push 语句。如果传入用冒号分隔的两个表达式那么会用 from_element 构建 Vec。
我们来使用一下自定义的 my_vec! 宏
let mut v my_vec!();
v.push(1);
println!({:?}, v);
let v my_vec![1, 2, 3, 4, 5];
println!({:?}, v);
let v my_vec!{1; 3};
println!({:?}, v);我们在使用宏的时候可以使用(), [], {}都是可以的。但是一般都是按照约定成俗的方式来使用。例如vec![1,2,3]而不是使用 vec!{1,2,3}。
这段宏调用展开以后如下所示
let mut v std::vec::Vec::new();
v.push(1);
{::std::io::_print(format_args!({0:?}\n, v));
};
let v {let mut v std::vec::Vec::new();v.push(1);v.push(2);v.push(3);v.push(4);v.push(5);v
};
{::std::io::_print(format_args!({0:?}\n, v));
};
let v std::vec::from_elem(1, 3);
{::std::io::_print(format_args!({0:?}\n, v));
};可以看到let v my_vec![1, 2, 3, 4, 5]; 被展开为
let v {let mut v std::vec::Vec::new();v.push(1);v.push(2);v.push(3);v.push(4);v.push(5);v
};它带上了我们在宏定义中的{}另外我们注意到println! 宏也被展开了, 但是并没有完全展开其中还包含了一个format_args! 宏我们来看一下是否和println宏的定义一样。
// println宏的定义
macro_rules! println {() {$crate::print!(\n)};($($arg:tt)*) {{$crate::io::_print($crate::format_args_nl!($($arg)*));}};
}可以看到println带有参数将会使用 format_args_nl! 宏但是expand确是 format_args 宏。大概可能是因为文档中说format_args_nl宏是nightly模式下的吧并没有完全展开是因为该宏是内置宏(rustc_builtin_macro)。
在使用声明宏时我们需要为参数明确类型刚才的例子都是使用的expr其实还可以使用下面这些
item比如一个函数、结构体、模块等。block代码块。比如一系列由花括号包裹的表达式和语句。stmt语句。比如一个赋值语句。pat模式。expr表达式。刚才的例子使用过了。ty类型。比如 Vec。ident标识符。比如一个变量名。path路径。比如foo、::std::mem::replace、transmute::_, int。 meta元数据。一般是在 #[...] 和 #![…] 属性内部的数据。tt单个的 token 树。vis可能为空的一个 Visibility 修饰符。比如 pub、pub(crate)
声明式宏还算比较简单。它可以帮助我们解决一些问题。
代码重复声明式宏可以帮助消除代码中的冗余通过将重复的代码逻辑抽象成宏从而减少代码量并提高代码的可读性和维护性。代码模板化宏可以用于定义代码模板允许在编译时根据不同的参数生成特定的代码片段从而实现代码的泛化和重用。实现函数重载宏可以匹配多种模式的参数来实现函数重载。
宏的缺点
宏目前的编写无法得到IDE很好的支持另外一点就是如无必要就不要编写宏。如果要编写那么尽量编写声明式宏而不是过程宏。
宏编写复杂过程宏的编写可能相对复杂特别是对于复杂的语法分析和代码生成任务编写和调试过程宏可能需要更多的时间和精力。可读性下降宏可能会导致代码的可读性下降特别是在宏的展开代码复杂或嵌套层级较多时代码可读性可能变差。不利于错误检查宏展开发生在编译期间因此错误信息可能不够明确和直观难以定位宏展开后的具体错误位置。难以调试宏展开过程对于开发者不是透明的因此在调试过程中可能会遇到难以解决的问题。
参考资料
https://github.com/rust-lang/rust/issues/93904https://blog.logrocket.com/macros-in-rust-a-tutorial-with-examples/#:~:textDeclarative%20macros%20enable%20you%20to,Rust%20code%20it%20is%20given.rust编程第一课-陈天The Little Book of Rust Macros