Rust 结构体与属性式和自定义派生宏

文章详细介绍了 Rust 语言中的 attribute-like 和 custom derive 宏的使用方法,通过具体代码示例展示了如何通过宏在编译时修改结构体,并解释了宏的工作原理和实现方式。

Rust attribute and custom-derive macros

Rust 中的类似属性和自定义派生宏用于在编译时以某种方式修改一段 Rust 代码,通常是为了增加功能。

要理解 Rust 中的类似属性和自定义派生宏,我们首先需要简要介绍 Rust 中的实现结构。

结构的实现:impl

以下结构应该是相当容易理解的。当我们创建对特定结构进行操作的函数时,事情就变得有趣了。我们这样做的方法是使用 impl

struct Person {
    name: String,
    age: u8,
}

关联函数和方法是在 impl 块中为结构实现的。

关联函数可以与 Solidity 中为与结构交互创建库的场景进行比较。当我们定义 using lib for MyStruct 时,它允许我们使用语法 myStruct.associatedFunction()。这使得该函数可以通过 Self 关键字访问 myStruct

我们建议使用 Rust Playground,但对于更复杂的示例,你可能需要设置你的 IDE。

让我们看一个下面的示例:

struct Person {
    age: u8,
    name: String,
}

// 为 `Person` 结构实现方法 `new()`,允许初始化一个 `Person` 实例
impl Person {
    // 使用提供的 `name` 和 `age` 创建一个新的 `Person`
    fn new(name: String, age: u8) -> Self {
        Person { name, age }
    }

    fn can_drink(&self) -> bool {
        if self.age >= 21 as u8 {
            return true;
        }
        return false;
    }

    fn age_in_one_year(&self) -> u8 {
        return &self.age + 1;
    }
} 

fn main() {
    // 用法:创建一个带有名字和年龄的新 `Person` 实例
    let person = Person::new(String::from("Jesserc"), 19);

    // 使用一些实现函数
    println!("{:?}", person.can_drink()); // false
    println!("{:?}", person.age_in_one_year()); // 20
    println!("{:?}", person.name);
}

用法:

// 用法:创建一个带有名字和年龄的新 `Person` 实例
let person = Person::new(String::from("Jesserc"), 19);

// 使用一些实现函数
person.can_drink(); // false
person.age_in_one_year(); // 20

Rust Traits

Rust traits 是在不同的 impl 之间实现共享行为的一种方式。可以将它们视为 Solidity 中的接口或抽象合约——任何使用该接口的合约必须实现某些函数。

例如,假设我们有一个需要定义汽车和船的结构的场景。我们想附加一个方法,允许我们以每小时公里数检索它们的速度。在 Rust 中,我们可以通过使用单个 trait 并在两个结构之间共享该方法来实现这个目标。

如下所示:

// traits 用 `trait` 关键字定义,后跟其名称
trait Speed {
    fn get_speed_kph(&self) -> f64;
}

// 汽车结构
struct Car {
    speed_mph: f64,
}

// 船结构
struct Boat {
    speed_knots: f64,
}

// 使用 `impl` 关键字为类型实现 traits,如下所示
impl Speed for Car {
    fn get_speed_kph(&self) -> f64 {
        // 将英里每小时转换为公里每小时
        self.speed_mph * 1.60934
    }
}

// 我们也为 `Boat` 实现 `Speed` trait
impl Speed for Boat {
    fn get_speed_kph(&self) -> f64 {
        // 将节转换为公里每小时
        self.speed_knots * 1.852
    }
}

fn main() {
    // 初始化一个 `Car` 和 `Boat` 类型
    let car = Car { speed_mph: 60.0 };
    let boat = Boat { speed_knots: 30.0 };

    // 获取并打印以公里每小时为单位的速度
    let car_speed_kph = car.get_speed_kph();
    let boat_speed_kph = boat.get_speed_kph();

    println!("Car Speed: {} km/h", car_speed_kph); // 96.5604 km/h
    println!("Boat Speed: {} km/h", boat_speed_kph); // 55.56 km/h
}

宏如何修改结构

在我们关于类似函数的宏的教程中,我们看到了宏如何扩展代码,比如 println!(...)msg!(...) 在大型 Rust 代码中的用法。在 Solana 的上下文中,我们关心的另一种宏是 attribute-like 宏和 derive 宏。我们可以在 anchor 创建的启动程序中看到这三种宏(函数式、类似属性和派生):

Rust attribute and custom-derive macros

为了直观了解类似属性的宏做了什么,我们将创建两个宏:一个将字段添加到结构中,另一个将其删除。

示例 1:类似属性的宏,插入字段

为了更好地理解 Rust 的属性和宏如何工作,我们将创建一个 attribute-like macro,其功能为:

  1. 处理一个不包含 foobar 字段的结构,字段类型为 i32
  2. 将这些字段插入到结构中
  3. 创建一个包含称为 double_foo 的函数的 impl,该函数返回 foo 字段所持的整数值的两倍。

设置

首先我们创建一个新的 Rust 项目:

cargo new macro-demo --lib 
cd macro-demo
touch src/main.rs

在 Cargo.toml 文件中添加以下内容:

[lib]
proc-macro = true

[dependencies]
syn = {version="1.0.57",features=["full","fold"]}
quote = "1.0.8"

创建主程序

将以下代码粘贴到 src/main.rs 文件中。请确保阅读注释:

// src/main.rs
// 导入 macro_demo crate 并用 `*` 通配符引入所有项
// (实际上是此 crate 中的所有内容,包括我们在 `src/lib.rs` 中的宏)
use macro_demo::*;

// 将我们在 `src/lib.rs` 中创建的 `foo_bar_attribute` 过程属性宏应用于 `struct MyStruct`
// 该过程宏将生成一个带有指定字段和方法的新结构定义
#[foo_bar_attribute]
struct MyStruct {
    baz: i32,
}

fn main() {
    // 使用 `default()` 方法创建 `MyStruct` 的新实例
    // 此方法由宏生成的 `Default` trait 实现提供
    let demo = MyStruct::default();

    // 将 `demo` 的内容打印到控制台
    // 宏生成的 `Debug` trait 实现允许使用 `println!` 进行格式化输出
    println!("struct is {:?}", demo);

    // 在 `demo` 上调用 `double_foo()` 方法
    // 此方法由宏生成,返回 `foo` 字段值的两倍
    let double_foo = demo.double_foo();

    // 将调用 `double_foo` 的结果打印到控制台
    println!("double foo: {}", double_foo);
}

一些观察:

  • 结构 MyStruct 中没有字段 foo
  • double_foo 函数没有在上面的代码中定义,假设它存在。

现在让我们创建将修改 MyStruct 的类似属性的宏。

将 src/lib.rs 中的代码替换为以下代码(请确保阅读注释):

// src/lib.rs
// 导入必要的外部库
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemStruct};

// 声明一个使用 `proc_macro_attribute` 指令的过程属性宏
// 这使得宏可以作为属性使用
#[proc_macro_attribute]
// `foo_bar_attribute` 函数接受两个参数:
// _metadata:提供给宏的参数(如果有)
// _input:宏所应用的 TokenStream
pub fn foo_bar_attribute(_metadata: TokenStream, _input: TokenStream) -> TokenStream {
    // 将输入 TokenStream 解析为表示结构的 AST 节点
    let input = parse_macro_input!(_input as ItemStruct);
    let struct_name = &input.ident; // 获取结构的名称

    // 使用 quote! 宏构建输出 TokenStream
    // quote! 宏允许将 Rust 代码写成字符串的方式,但可以插入值
    TokenStream::from(quote! {
        // 为 #struct_name 派生 Debug trait,以启用使用 `println()` 的格式化输出
        #[derive(Debug)]
        // 定义具有两个字段:foo 和 bar 的新结构 #struct_name
        struct #struct_name {
            foo: i32,
            bar: i32,
        }

        // 为 #struct_name 实现 Default trait
        // 这提供了一个 default() 方法,用于创建 #struct_name 的新实例
        impl Default for #struct_name {
            // 默认方法返回一个新的 #struct_name 实例,其中 foo 设置为 10,bar 设置为 20
            fn default() -> Self {
                #struct_name { foo: 10, bar: 20}
            }
        }

        impl #struct_name {
            // 为 #struct_name 定义一个方法 double_foo
            // 此方法返回 foo 的双倍值
            fn double_foo(&self) -> i32 {
                self.foo * 2
            }
        }
    })
}

现在,为了测试我们的宏,我们使用 cargo run src/main.rs 运行代码。

我们会得到以下输出:

struct is MyStruct { foo: 10, bar: 20 }
double foo: 20

示例 2:类似属性的宏,删除字段

关于类似属性的宏,可以认为它们在修改结构时具有无限的能力。让我们重复上面的示例,但这次类似属性的宏将删除结构中的所有字段。

将 src/lib.rs 替换为以下内容:

// src/lib.rs
// 导入必要的外部库
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemStruct};

#[proc_macro_attribute]
pub fn destroy_attribute(_metadata: TokenStream, _input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(_input as ItemStruct);
    let struct_name = &input.ident; // 获取结构的名称

    TokenStream::from(quote! {
        // 返回一个具有相同名称的空结构
        #[derive(Debug)]
        struct #struct_name {
        }
    })
}

将 src/main.rs 替换为以下内容:

use macro_demo::*;

#[destroy_attribute]
struct MyStruct {
    baz: i32,
    qux: i32,
}

fn main() {
    let demo = MyStruct { baz: 3, qux: 4 };

    println!("struct is {:?}", demo);
}

当你尝试使用 cargo run src/main.rs 编译时,会收到以下错误消息:

Error: struct `MyStruct` has no field named `baz`

这可能看起来很奇怪,因为结构显然有这些字段。然而,类似属性的宏删除了它们!

#[derive(…)]

#[derive(…)] 宏的功能远没有类似属性宏强大。对于我们的目的而言,派生宏 增强 了结构,而不是改变它。(这不是一个精确的定义,但现在足够了)。

派生宏可以,除了其他外,向结构附加一个 impl

例如,如果我们尝试做以下操作:

struct Foo {
    bar: i32,
}

pub fn main() {
    let foo = Foo { bar: 3 };
    println!("{:?}", foo);
}

编译时将不会通过,因为结构不可“打印”。

为了使它们可打印,需要有一个 impl,其中有一个函数 fmt,返回结构的字符串表示。

如果我们这样做:

#[derive(Debug)]
struct Foo {
    bar: i32,
}

pub fn main() {
    let foo = Foo { bar: 3 };
    println!("{:?}", foo);
}

我们期望它打印:

Foo { bar: 3 }

派生属性以某种方式“增强”了 Foo,使得 println! 可以为其创建字符串表示。

总结

impl 是一组对结构进行操作的函数。它们通过使用与结构相同的名称“附加”到结构上。trait 强制工具的 impl 实现某些函数。在我们的示例中,我们通过语法 impl Speed for Car 将 trait Speed 附加到 impl Car。

类似属性的宏接收一个结构,可以完全重写它。

派生宏增强了结构,使其具有附加功能。

宏允许 Anchor 隐藏复杂性

让我们再看看 Anchor 在 anchor init 时创建的程序:

Rust attribute and custom-derive macros

属性 #[program] 在幕后修改模块。例如,它实现了一个路由器,自动将传入的区块链指令定向到模块内的适当函数。

结构 Initialize {} 被增强了额外的功能,以便在 Solana 框架中使用。

总结

宏是一个非常大的主题。我们在这里的目的是让你了解当你看到 #[program]#[derive(Accounts)] 时发生了什么。不要因它感到陌生而沮丧。你不需要能够编写宏才能编写 Solana 程序

不过,了解它们的作用希望能让你看到的程序变得不那么神秘。

下一步学习

本教程是我们免费的 Solana 课程 的一部分。

原文出版于 2024 年 2 月 16 日

  • 原文链接: rareskills.io/post/rust-...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/