2 个版本
使用旧的 Rust 2015
0.1.1 | 2016年6月29日 |
---|---|
0.1.0 | 2016年6月27日 |
#7 in #self-reference
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