?博主现有专栏:
C51单片机(STC89C516),c语言,c++,离散数学,算法设计与分析,数据结构,Python,Java基础,MySQL,linux,基于HTML5的网页设计及应用,Rust(官方文档重点总结),jQuery,前端vue.js,Javaweb开发,Python机器学习等
?主页链接:
Y小夜-CSDN博客
目录
?函数指针
?返回闭包
?宏
?宏和函数的区别
?使用macro_rules!的声明宏用于通用元编程
?用于从属性生成代码的过程宏
?如何编写自定义derive宏
?类属性宏
?类函数宏
?函数指针
我们讨论过了如何向函数传递闭包;也可以向函数传递常规函数!这个技术在我们希望传递已经定义的函数而不是重新定义闭包作为参数时很有用。函数满足类型 fn
(小写的 f),不要与闭包 trait 的 Fn
相混淆。fn
被称为 函数指针(function pointer)。通过函数指针允许我们使用函数作为另一个函数的参数
这里定义了一个 add_one
函数将其参数加一。do_twice
函数获取两个参数:一个指向任何获取一个 i32
参数并返回一个 i32
的函数指针,和一个 i32
值。do_twice
函数传递 arg
参数调用 f
函数两次,接着将两次函数调用的结果相加。main
函数使用 add_one
和 5
作为参数调用 do_twice
。
fn add_one(x: i32) -> i32 { x + 1}fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 { f(arg) + f(arg)}fn main() { let answer = do_twice(add_one, 5); println!("The answer is: {}", answer);}
这会打印出 The answer is: 12
。do_twice
中的 f
被指定为一个接受一个 i32
参数并返回 i32
的 fn
。接着就可以在 do_twice
函数体中调用 f
。在 main
中,可以将函数名 add_one
作为第一个参数传递给 do_twice
。
不同于闭包,fn
是一个类型而不是一个 trait,所以直接指定 fn
作为参数而不是声明一个带有 Fn
作为 trait bound 的泛型参数。
函数指针实现了所有三个闭包 trait(Fn
、FnMut
和 FnOnce
),所以总是可以在调用期望闭包的函数时传递函数指针作为参数。倾向于编写使用泛型和闭包 trait 的函数,这样它就能接受函数或闭包作为参数。
一个只期望接受 fn
而不接受闭包的情况的例子是与不存在闭包的外部代码交互时:C 语言的函数可以接受函数作为参数,但 C 语言没有闭包。
作为一个既可以使用内联定义的闭包又可以使用命名函数的例子,让我们看看一个 map
的应用。使用 map
函数将一个数字 vector 转换为一个字符串 vector,就可以使用闭包,比如这样:
let list_of_numbers = vec![1, 2, 3]; let list_of_strings: Vec<String> = list_of_numbers.iter().map(|i| i.to_string()).collect();
或者可以将函数作为 map
的参数来代替闭包,像是这样:
let list_of_numbers = vec![1, 2, 3]; let list_of_strings: Vec<String> = list_of_numbers.iter().map(ToString::to_string).collect();
因为存在多个叫做 to_string
的函数;这里使用了定义于 ToString
trait 的 to_string
函数,标准库为所有实现了 Display
的类型实现了这个 trait。
?返回闭包
闭包表现为 trait,这意味着不能直接返回闭包。对于大部分需要返回 trait 的情况,可以使用实现了期望返回的 trait 的具体类型来替代函数的返回值。但是这不能用于闭包,因为它们没有一个可返回的具体类型;例如不允许使用函数指针 fn
作为返回值类型。
fn returns_closure() -> dyn Fn(i32) -> i32 { |x| x + 1}
编译器给出的错误是:
$ cargo build Compiling functions-example v0.1.0 (file:///projects/functions-example)error[E0746]: return type cannot have an unboxed trait object --> src/lib.rs:1:25 |1 | fn returns_closure() -> dyn Fn(i32) -> i32 { | ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time | = note: for information on `impl Trait`, see <https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits>help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:8]`, which implements `Fn(i32) -> i32` |1 | fn returns_closure() -> impl Fn(i32) -> i32 { | ~~~~~~~~~~~~~~~~~~~For more information about this error, try `rustc --explain E0746`.error: could not compile `functions-example` due to previous error
错误又一次指向了 Sized
trait!Rust 并不知道需要多少空间来储存闭包。不过我们在上一部分见过这种情况的解决办法:可以使用 trait 对象:
fn returns_closure() -> Box<dyn Fn(i32) -> i32> { Box::new(|x| x + 1)}
?宏
我们已经在本书中使用过像 println!
这样的宏了,不过还没完全探索什么是宏以及它是如何工作的。宏(Macro)指的是 Rust 中一系列的功能:使用 macro_rules!
的 声明(Declarative)宏,和三种 过程(Procedural)宏:
#[derive]
宏在结构体和枚举上指定通过 derive
属性添加的代码类属性(Attribute-like)宏定义可用于任意项的自定义属性类函数宏看起来像函数不过作用于作为参数传递的 token 我们会依次讨论每一种宏,不过首要的是,为什么已经有了函数还需要宏呢?
?宏和函数的区别
从根本上来说,宏是一种为写其他代码而写代码的方式,即所谓的 元编程(metaprogramming)。在附录 C 中会探讨 derive
属性,其生成各种 trait 的实现。我们也在本书中使用过 println!
宏和 vec!
宏。所有的这些宏以 展开 的方式来生成比你所手写出的更多的代码。
元编程对于减少大量编写和维护的代码是非常有用的,它也扮演了函数扮演的角色。但宏有一些函数所没有的附加能力。
一个函数签名必须声明函数参数个数和类型。相比之下,宏能够接收不同数量的参数:用一个参数调用 println!("hello")
或用两个参数调用 println!("hello {}", name)
。而且,宏可以在编译器翻译代码前展开,例如,宏可以在一个给定类型上实现 trait。而函数则不行,因为函数是在运行时被调用,同时 trait 需要在编译时实现。
实现宏不如实现函数的一面是宏定义要比函数定义更复杂,因为你正在编写生成 Rust 代码的 Rust 代码。由于这样的间接性,宏定义通常要比函数定义更难阅读、理解以及维护。
宏和函数的最后一个重要的区别是:在一个文件里调用宏 之前 必须定义它,或将其引入作用域,而函数则可以在任何地方定义和调用。
?使用macro_rules!的声明宏用于通用元编程
Rust 最常用的宏形式是 声明宏(declarative macros)。它们有时也被称为 “macros by example”、“macro_rules!
宏” 或者就是 “macros”。其核心概念是,声明宏允许我们编写一些类似 Rust match
表达式的代码。正如在第六章讨论的那样,match
表达式是控制结构,其接收一个表达式,与表达式的结果进行模式匹配,然后根据模式匹配执行相关代码。宏也将一个值和包含相关代码的模式进行比较;此种情况下,该值是传递给宏的 Rust 源代码字面值,模式用于和前面提到的源代码字面值进行比较,每个模式的相关代码会替换传递给宏的代码。所有这一切都发生于编译时。
可以使用 macro_rules!
来定义宏。让我们通过查看 vec!
宏定义来探索如何使用 macro_rules!
结构。第八章讲述了如何使用 vec!
宏来生成一个给定值的 vector。例如,下面的宏用三个整数创建一个 vector:
let v: Vec<u32> = vec![1, 2, 3];
也可以使用 vec!
宏来构造两个整数的 vector 或五个字符串 slice 的 vector。但却无法使用函数做相同的事情,因为我们无法预先知道参数值的数量和类型。
#[macro_export]macro_rules! vec { ( $( $x:expr ),* ) => { { let mut temp_vec = Vec::new(); $( temp_vec.push($x); )* temp_vec } };}
#[macro_export]
注解表明只要导入了定义这个宏的 crate,该宏就应该是可用的。如果没有该注解,这个宏不能被引入作用域。
接着使用 macro_rules!
和宏名称开始宏定义,且所定义的宏并 不带 感叹号。名字后跟大括号表示宏定义体,在该例中宏名称是 vec
。
vec!
宏的结构和 match
表达式的结构类似。此处有一个分支模式 ( $( $x:expr ),* )
,后跟 =>
以及和模式相关的代码块。如果模式匹配,该相关代码块将被执行。这里这个宏只有一个模式,那就只有一个有效匹配方向,其他任何模式方向(译者注:不匹配这个模式)都会导致错误。更复杂的宏会有多个分支模式。
?用于从属性生成代码的过程宏
第二种形式的宏被称为 过程宏(procedural macros),因为它们更像函数(一种过程类型)。过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。有三种类型的过程宏(自定义派生(derive),类属性和类函数),不过它们的工作方式都类似。
创建过程宏时,其定义必须驻留在它们自己的具有特殊 crate 类型的 crate 中。这么做出于复杂的技术原因,将来我们希望能够消除这些限制。在示例 19-29 中展示了如何定义过程宏,其中 some_attribute
是一个使用特定宏变体的占位符。
use proc_macro;#[some_attribute]pub fn some_name(input: TokenStream) -> TokenStream {}
定义过程宏的函数接收一个 TokenStream 作为输入并生成 TokenStream 作为输出。TokenStream
是定义于proc_macro
crate 里代表一系列 token 的类型,Rust 默认携带了proc_macro
crate。这就是宏的核心:宏所处理的源代码组成了输入 TokenStream
,宏生成的代码是输出 TokenStream
。函数上还有一个属性;这个属性指明了我们创建的过程宏的类型。在同一 crate 中可以有多种的过程宏。
?如何编写自定义derive宏
让我们创建一个 hello_macro
crate,其包含名为 HelloMacro
的 trait 和关联函数 hello_macro
。不同于让用户为其每一个类型实现 HelloMacro
trait,我们将会提供一个过程式宏以便用户可以使用 #[derive(HelloMacro)]
注解它们的类型来得到 hello_macro
函数的默认实现。该默认实现会打印 Hello, Macro! My name is TypeName!
use hello_macro::HelloMacro;use hello_macro_derive::HelloMacro;#[derive(HelloMacro)]struct Pancakes;fn main() { Pancakes::hello_macro();}
?类属性宏
类属性宏与自定义派生宏相似,不同的是 derive
属性生成代码,它们(类属性宏)能让你创建新的属性。它们也更为灵活;derive
只能用于结构体和枚举;属性还可以用于其它的项,比如函数。作为一个使用类属性宏的例子,可以创建一个名为 route
的属性用于注解 web 应用程序框架(web application framework)的函数:
#[route(GET, "/")]fn index() {
#[route]
属性将由框架本身定义为一个过程宏。其宏定义的函数签名看起来像这样:
#[proc_macro_attribute]pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
这里有两个 TokenStream
类型的参数;第一个用于属性内容本身,也就是 GET, "/"
部分。第二个是属性所标记的项:在本例中,是 fn index() {}
和剩下的函数体。
除此之外,类属性宏与自定义派生宏工作方式一致:创建 proc-macro
crate 类型的 crate 并实现希望生成代码的函数!
?类函数宏
类函数(Function-like)宏的定义看起来像函数调用的宏。类似于 macro_rules!
,它们比函数更灵活;例如,可以接受未知数量的参数。
let sql = sql!(SELECT * FROM posts WHERE id=1);
这个宏会解析其中的 SQL 语句并检查其是否是句法正确的,这是比 macro_rules!
可以做到的更为复杂的处理。sql!
宏应该被定义为如此:
#[proc_macro]pub fn sql(input: TokenStream) -> TokenStream {
这类似于自定义派生宏的签名:获取括号中的 token,并返回希望生成的代码。