Rust过程宏入门(二)——初探派生宏
写于2021年1月7日。最早发表于知乎。
认识派生宏,手动实现生成代码
我们将从一个简单的案例开始,设计一个Builder模式的派生宏。
#[derive(Builder)]
pub struct Command {
executable: String,
#[builder(each = "arg")]
args: Vec<String>,
current_dir: Option<String>,
}
fn main() {
let command = Command::builder()
.executable("cargo".to_owned())
.arg("build".to_owned())
.arg("--release".to_owned())
.build()
.unwrap();
assert_eq!(command.executable, "cargo");
}
上述代码展示了我们希望呈现给用户的使用方式,即用户只需定义一个结构体(若结构体实现了成员函数或trait,下文中也称之为「类」),则派生宏将为其自动生成Builder类。
Command类实现
首先从Builder类的使用方式入手,若欲手动实现Builder类,那么它大概会像这样:
impl Command {
pub fn builder() -> CommandBuilder {
CommandBuilder {
executable: None,
args: Vec::new(),
current_dir: None
}
}
}
pub struct CommandBuilder {
executable: Option<String>,
args: Vec<String>,
current_dir: Option<String>,
}
其中,impl Command
为Command
结构体添加了builder
成员函数,将返回一个CommandBuilder
结构体, 并设置其成员变量的初始量。注意这里的builder
不带任何参数(在C++、Java等语言中又称为静态函数或静态方法)。
而CommandBuilder
结构体中的成员变量与Command
基本一致,只是executable
字段的类型由String
变成了Option<String>
,这是因为在刚创建CommandBuilder
类时,executable
的值未知,所以只好利用Rust自带的可选类型Option<T>
,其定义很简单:
pub enum Option<T> {
None,
Some(T),
}
虽然在不少语言中(如C/C++、Java等)都有枚举类型的概念,但对不熟悉Rust的朋友或Rust初学者而言,这里有必要指出:Rust的枚举类型与其他语言稍有不同,除了包含枚举标签以外,它还可以额外附带任意值。例如Option<T>
类型中,有「有值(Some
)」与「无值(None
)」两种状态,而处于「有值」的状态时,还附带一个T类型(即泛型参数)的值,这便很好地表示了可选类型。
CommandBuilder类及实现
接下来,为CommandBuilder
结构体实现相应的函数:
impl CommandBuilder {
pub fn executable(mut self, executable: String) -> Self {
self.executable = Some(executable);
self
}
pub fn arg(mut self, arg: String) -> Self {
self.args.push(arg);
self
}
pub fn current_dir(mut self, current_dir: String) -> Self {
self.current_dir = Some(current_dir);
self
}
pub fn build(self) -> Result<Command, String> {
let executable = self.executable.ok_or(
"executable required, but not set".to_owned())?;
let args = self.args;
let current_dir = self.current_dir;
Ok(Command { executable, args, current_dir })
}
}
其中,executable
、arg
、current_dir
函数分别对应三个字段的设置函数。其第一个参数mut self
代表结构体自身(由于rust中的变量默认是不可变的,所以要修改self
,需要加上mut
。第二个参数是相应字段的值,最后返回结构体自身(这里用到了自身类型别名Self
,须注意它与自身变量别名self
是不同的)。
最后的build
函数则是返回所希望构建的Command
类。由于最终构建Command
类时,可能有必填字段未设置(如executable
是必须设置的),此时不能构造一个有效的Command
类,且需将此错误信息告知调用者。
错误处理
说到这里,不得不提及Rust独特的错误处理机制。C语言中常通过返回非零值代表错误;C++、Java、Python语言中通常会中断当前函数执行并抛出异常信息交予调用者处理。而Rust得益于其特有的枚举类型,将错误信息直接返回调用者,且通常调用者无法直接忽略错误。Rust标准库中定义了Result<T, E>
类型如下:
pub enum Result<T, E> {
Ok(T),
Err(E),
}
该类型有「正常(Ok
)」与「错误(Err
)」两种状态,因此调用者要么立即处理错误信息,要么将其向上传播。 不过Result<T, E>
类中也提供了一个unwarp
方法,可以直接取其正常状态的值。但若返回的是错误状态,调用unwarp
会触发panic!
并打印该错误信息。
若忘记设置executable
的值,运行时将输出如下错误信息。
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: "executable required, but not set"'
本期的介绍到此结束,主要以较为常见的Builder模式为例,介绍了如何手动实现其主要功能,接下将逐步介绍如何通过Rust过程宏自动实现上述代码。
实例来源自GitHub上的仓库: dtolnay/proc-macro-workshop。