2个不稳定版本
0.2.0 | 2022年2月26日 |
---|---|
0.1.0 | 2022年1月9日 |
#13 在 #lower
5,981 每月下载量
用于 fortify
4KB
一种简单便捷地将拥有数据与借用类型捆绑在一起的方法。
示例
use fortify::*;
// Define a borrowing type. The `Lower` trait specifies that it is covariant in its first
// lifetime parameter.
#[derive(Lower)]
struct Example<'a> {
a: &'a i32,
b: &'a mut i32,
}
// Construct a fortified value that makes an "external" reference to `a` and an "internal"
// reference to `b`, which is owned by the Fortify.
let a = 1;
let mut example: Fortify<Example> = fortify! {
let mut b = 1;
b += 1;
yield Example {
a: &a,
b: &mut b
};
};
// Use `with_mut` for general mutable access to the wrapped value. Note that the reference
// to `b` is still valid even though `b` is not in scope in this block.
example.with_mut(|example| {
assert_eq!(*example.a, 1);
assert_eq!(*example.b, 2);
*example.b += 1;
assert_eq!(*example.b, 3);
});
问题
Rust的主要卖点之一是其借用检查器,它允许你定义类型,这些类型可以引用外部数据,同时确保数据在类型使用期间始终有效。从理论上讲,这是一种理想的内存管理方案:取消引用引用无需任何成本,我们不会推迟任何工作以进行延迟垃圾回收,并且我们可以完全避免使用后释放错误。
然而,许多Rust程序员对此技术感到不安,经常求助于更简单的方法,如引用计数。一个关键原因是生命周期的传染性:将引用存储在类型中需要你指定一个生命周期,而除了极少数的'static
数据之外,这需要你在类型上有一个生命周期参数。然后,为了在另一个类型中使用该类型,你需要为该类型指定一个生命周期参数,依此类推。只有当你最终将值作为局部变量引入时,这个链才会结束,从而为它创建新的唯一生命周期。
但这并不总是可能的。有时你需要一个值在比你可以访问的更高栈级别上进行分配。在事先很难确定这些情况,而当你确定时,整个设计就会崩溃。难怪Rust程序员在使用引用时犹豫不决。
有几个现有的解决方案可以减轻这个问题,但它们都有问题
- 使用如bumpalo或typed-arena之类的区域分配器,你可以从栈的较低级别分配具有特定生命周期的数据。然而,只有在分配器被丢弃之前才能释放内存,因此分配器可以保持活跃的时间有实际限制。
- 《owning_ref》库允许您通过将引用与它引用的数据捆绑在一起来避免指定引用的生存期。然而,它存在许多安全性问题,并且不再维护。
- 有人提出了允许自引用结构的提案。在没有语言支持的情况下,rental 和 ouroboros 库实现了这种有限的功能。然而,自引用结构的实现并不像人们预期的那样简单或直观。在结构体中存储的内容有限,并且必须限制对结构体字段的访问,以遵守 Rust 的别名规则。
此库引入了另一种解决方案,目标是足够灵活和方便,以实现引用和借用类型的无恐惧使用。
解决方案
Fortify<T>
包装器允许包装的值引用由包装器本身拥有的隐藏的辅助数据。例如,一个 Fortify<&'static str>
可以是一个普通的 &'static str
,或者它可以是一个指向 Fortify
内部存储的字符串的引用。类型 T
不仅限于引用,它可以是任何具有生存期参数的类型,具有类似的效果。这里的含义是,您可以通过将生存期设置为 'static
并将其放入 Fortify
包装器中,将任何借用类型(即具有生存期参数的类型)转换为拥有类型。
这是怎么回事?难道一个 &'static str
总是要在 'static
生存期内引用某个东西吗?
关键洞察是您永远无法从 Fortify<&'static str>
获取 &'static str
。相反,您可以获取一个 &'a str
,其中 'a
与 Fortify
的生存期相关联。包装器与其包装类型之间存在复杂的关系,这种关系通常无法在 Rust 中表达(因此需要这个库)。其实施需要能够操作封装类型的生存期参数。
那么,如果我要使用具有多个生存期参数的类型,包装器如何知道它在哪个生存期上“工作”?
所有包装类型都必须实现 Lower<'a>
特性,该特性指定了如何在类型中替换协变生存期参数。这个特性可以自动推导,在这种情况下,它将只操作参数列表中的第一个生存期参数。
如何创建 Fortify<T>
?
创建包装器实例的方法有很多,其中最简单的是直接从 T
转换。然而,首选且最通用方法是使用 fortify!
宏。
let example: Fortify<&'static str> = fortify! {
let mut str = String::new();
str.push_str("Foo");
str.push_str("Bar");
yield str.as_str();
};
这会捕获代码块中的所有局部变量,并将它们存储在 Fortify
包装器内部。最后的 yield
语句提供了对外暴露的包装值。请注意,它可能引用局部变量。
如何使用它?
您可以使用 borrow
获取对包装值的不可变引用,并缩短适当的生命周期。可变访问稍微复杂一些,需要使用 with_mut
。
assert_eq!(example.borrow(), &"FooBar");
// or
example.with_mut(|s| assert_eq!(s, &"FooBar"));
我可以用具有非 'static
生命周期的 Fortify<T>
吗?
当然可以!Fortify
包装器只是为引用(指向包装器内部拥有的数据)引入了一个额外的选项。总是可以放弃这个选项,并直接从 T
构建一个 Fortify<T>
。您甚至可以有一个混合值,它引用外部数据和拥有的数据,就像第一个例子中那样。
依赖关系
~1.5MB
~35K SLoC