#lifetime #reference #ownership

fortify

一种简单方便地将拥有数据与借用类型捆绑在一起的方法

6 个版本 (3 个破坏性更新)

0.4.1 2022年4月22日
0.4.0 2022年3月26日
0.3.1 2022年2月26日
0.2.1 2022年2月13日
0.1.0 2022年1月9日

#726 in 内存管理

Download history 1744/week @ 2024-03-13 931/week @ 2024-03-20 288/week @ 2024-03-27 928/week @ 2024-04-03 659/week @ 2024-04-10 1068/week @ 2024-04-17 1022/week @ 2024-04-24 697/week @ 2024-05-01 885/week @ 2024-05-08 1658/week @ 2024-05-15 1707/week @ 2024-05-22 500/week @ 2024-05-29 1482/week @ 2024-06-05 731/week @ 2024-06-12 73/week @ 2024-06-19 567/week @ 2024-06-26

每月下载量:3,026

MIT/Apache

38KB
638

一种简单方便地将拥有数据与借用类型捆绑在一起的方法。

示例

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 程序员在使用引用时犹豫不决。

有一些现有的解决方案可以缓解这个问题,但它们都有其问题

  • 使用如 bumpalotyped-arena 这样的区域分配器,你可以从堆栈的较低级别分配具有特定生命周期的数据。然而,内存只能在分配器被丢弃时才能释放,因此分配器可以保持活跃的时间有实际限制。
  • owning_ref 包允许你通过将其与所引用的数据捆绑在一起来避免指定引用的生命周期。但是,它存在 许多 安全性 问题,并且不再维护。
  • 已经有人提出了允许自引用结构体的建议。由于缺乏语言支持,rentalouroboros 库实现了这种有限的功能。然而,自引用结构体的实现并不像人们预期的那样简单或直观。在结构体中可以存储的内容存在限制,并且为了遵守 Rust 的别名规则,对结构体字段的访问必须受到限制。

该库引入了另一种解决方案,旨在足够灵活和方便,以便能够放心地使用引用和借用类型。

解决方案

Fortify<T> 包装器允许包装的值引用包装器本身拥有的隐藏的辅助数据。例如,一个 Fortify<&'static str> 可以是一个常规的 &'static str,或者它可以是引用到 Fortify 内部存储的字符串。 T 不限于引用,它可以是一个具有生命周期参数的任何类型,效果类似。这意味着您可以通过将生命周期设置为 'static 并将其放入 Fortify 包装器中,将任何借用类型(即具有生命周期参数的类型)转换为拥有类型。

这怎么行?一个 &'static str 不总是必须引用 'static 生命周期中的某个内容吗?

关键洞察是,您永远无法从 Fortify<&'static str> 获取 &'static str。相反,您可以获取一个 &'a str,其中 'aFortify 的生命周期相关联。包装器与其包装类型之间存在复杂的关联关系,这通常在 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"));

我可以用 Fortify<T> 与非 'static 生命周期的类型一起使用吗?

当然可以!Fortify 包装器只为引用(指向包装器内部的所有权数据)引入了一个额外的选项。始终可以选择放弃此选项,并直接从 T 构建一个 Fortify<T>。您甚至可以有一个混合值,它对某些外部数据和一些所有权数据进行引用,就像第一个示例中那样。

依赖关系

~1.5MB
~35K SLoC