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 CommandCommand结构体添加了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 })
    }
}

其中,executableargcurrent_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