#struct #reference #self #self-reference #struct-fields #self-referential

refstruct

为可以引用自身字段的 structs 生成代码。目前处于 alpha/experimental 阶段。

2 个版本

使用旧的 Rust 2015

0.1.1 2016年6月29日
0.1.0 2016年6月27日

#7 in #self-reference

Apache-2.0/MIT

12KB
224

可以引用自身字段的 Rust structs。

当前状态:Alpha / experimental,但应该是安全的和可用的 - 至少还没有人证明它是不可安全的,我在寻找有关改进可用性的反馈。

此文件是目前唯一的文档,但应该包含足够的信息来帮助您入门。

背景

比如说,你有一个 String,一些该字符串的切片,然后你想要在同一个 struct 中存储这个 String 和切片

pub struct Foo {
    data: String,
    found_items: Vec<&str>,
}

上面的代码无法编译,因为你不能为 &str 指定有效的生命周期。你有几个选择,但都有其缺点

  • 存储索引和长度而不是 &str - 这适用于非常简单的例子,如上面的例子,但不具有可扩展性(例如,如果有许多不同的所有者 String,事情会变得越来越复杂)。这也不适用于许多其他场景,其中所有者和借用指针之间的关系不太明显。
  • 使用 unsafe - 这很糟糕,因为它是不可安全的。
  • self-borrowing for life - 使得 struct 永远由自己借用,因此你永远不会再次修改它。
  • 使用 Rc/Arc 与 owning_ref 一起 - 可能会导致一些开销,以及通过 Rc 循环发生内存泄漏的风险。

有关此主题的更详细阅读,请参阅 Tomaka 的文章

想法

复杂的自引用 struct 很可能是一次性构建然后经常使用,但不会更改。它以特定的字段顺序创建,并以相反的字段顺序销毁。字段可以包含对较早创建的字段的引用,但不能对较晚创建的字段进行引用。此外,struct 总是在堆上,以确保它永远不会在内存中移动。

因此,此 crate 生成一些代码,它是对该想法的安全抽象。由于 Rust 宏存在 限制,并且编译器插件在稳定 Rust 上不运行,因此代码通过 build.rs 脚本生成。

入门

构建脚本

首先,你必须设置一个 构建脚本

你的 build.rs 会是这个样子

extern crate refstruct;

fn main() {
   refstruct::Scanner::process_src().unwrap();
}

以下是需要添加到 Cargo.toml 中的内容,以便执行构建脚本

[package]
build = "build.rs"

[dependencies]
refstruct = "0.1"

[build-dependencies]
refstruct = "0.1"

其次,在您的代码中,创建 refstruct! 宏来创建一个可以引用其自身字段的 struct。因为我的解析器非常简单(目前是这样的),请按照以下方式编写

#[macro_use]
extern crate refstruct;

include!(refstruct!(r#"
// Content of macro, see below
#"));

宏的内容需要以 TOML 格式编写。如果您想使您的 struct 看起来像这样

pub struct Foo {
    data: String,
    found_items: Vec<&str>,
}

对应的宏如下所示

include!(refstruct!(r#"
name = "Foo"
fields = [
    ["data", "String"],
    ["found_items", "Vec<& '_ str>"]
]
"#));

注意未命名的生命周期 '_ - 这代表了早期字段的生存期。

使用生成的代码

// Start building the struct, this takes the first field (data) as parameter.
let f1 = Foo::new("ABCDEFG".into());

// Calculate the second field from the first.
// Notice how me must use the parameter sent into the closure - we cannot use f1 here. 
let f2 = f1.found_items(|f| vec![&f.data()[0..2], &f.data()[4..6]]);

// Finish building the struct.
let foo: Foo = f2.build();

// foo has accessors for all struct fields.
assert_eq!(foo.data(), &String::from("ABCDEFG"));
assert_eq!(foo.found_items(), &vec!["AB", "EF"]);

更多详情

命名空间和模块

大部分生成的代码都放置在模块内部。这意味着您可能需要将名称导入到这个模块中。您可以使用“use”键来完成此操作,如下所示

name = "MyFooHandler"
use = "super::MyFoo"
fields = [
    ["all_foos", "Vec<MyFoo>"],
    ["best_foo", "& '_ MyFoo"]
]

在这个模块内部,会生成一些内部 struct 的代码。为了确保您导入到模块中的名称与内部 struct 之间没有名称冲突,您可以添加一个“namespace”键。这个键将用于内部 struct 的前缀,为模块本身命名,用作生命周期标识符等。如果您想要更精细的控制,还有“module”和“lifetime”键,分别用于命名模块本身和生命周期标识符。

关于“include”呢?

您需要编写 include!(refstruct!( /* ... /* )) 而不是仅仅 refstruct!( /* ... /* )。这是因为 refstruct 宏会计算出一个文件名。这个文件是由构建脚本创建的。

解析器限制

代码扫描器目前非常原始,它只是扫描文本字符串 refstruct!,然后捕获文本直到它找到一个带有 "# 的行。也就是说,即使您的宏被注释掉,或者您在字符串字面量中编写了包含“refstruct!”的文本,它仍然会尝试生成代码。

依赖关系

~370KB