4 个版本 (2 个重大更改)
| 0.3.0 | 2023 年 7 月 22 日 |
|---|---|
| 0.2.1 | 2023 年 7 月 21 日 |
| 0.2.0 | 2023 年 7 月 20 日 |
| 0.1.0 | 2023 年 7 月 7 日 |
#625 in Rust 模式
69KB
980 行
genrc
此软件包提供对 std::sync::Arc 和 std::rc::Rc 的替代方案,它们是(几乎)直接替换,但允许子对象的引用计数指针,类似于 C++ 的 shared_ptr。
主要功能,它增加了令人惊讶的灵活性:如果您有一个 Rc<T>,并且 T 包含一些类型为 U 的子对象,则可以通过调用 Rc::project() 来构造一个与原始对象共享所有权的 Rc<U>。
use genrc::rc::{Rc, Weak};
let a: Rc<[i32; 3]> = Rc::new([1, 2, 3]);
// convert the sized array into a slice
let b: Rc<[i32]> = Rc::project(a, |x| &x[..]);
// get a reference to one element of the array
let c: Rc<i32> = Rc::project(b, |x| &x[1]);
还有类型 RcBox<T>(和 ArcBox<T>)从 new_unique() 返回,它们利用新创建的引用计数指针仍然是唯一的这一事实,因此可以用于可变操作。
用途
更简单、更安全的初始化
您可以使用 RcBox<Option<T>> 来表示尚未初始化的类型,而不是使用 std::rc 中的各种不安全的 MaybeInit 相关 API。在对象初始化后,您可以使用 project 将其转换为普通的 Rc<T>。
# use genrc::rc::{Rc, RcBox, Weak};
// construct the object initially uninitialized
let mut obj : RcBox<Option<i32>> = Rc::new_unique(None);
// ... later ...
// initialize the object
obj.replace(5);
// project to the inner value that we just created
let obj : Rc<i32> = RcBox::project(obj, |x| x.as_ref().unwrap());
assert_eq!(*obj, 5);
您还可以创建循环数据结构,而不需要使用 RefCell 或 new_cyclic
use genrc::rc::{Rc, RcBox, Weak};
struct Node {
edges: Vec<Weak<Node>>,
}
// Make a graph
let mut graph: Vec<RcBox<Node>> = (0..5).map(|_| {
Rc::new_unique(Node { edges: vec![] })
}).collect();
// Make some random edges in the graph
for i in 0..5 {
for d in 1..3 {
let j = (i + d) % 5;
let link = RcBox::downgrade(&graph[j]);
graph[i].edges.push(link);
}
}
// we still have unique handles on the nodes, so attempting to upgrade
// weak pointers will fail.
let p = graph[1].edges[0].clone();
assert!(p.upgrade().is_none());
// convert `RcBox` to a normal `Rc` with `into()`.
let graph: Vec<Rc<Node>> = graph.into_iter().map(Into::into).collect();
// now the weak pointers are valid - we've made a graph with (weak)
// cycles, no unsafe or internal mutation required.
assert!(Rc::ptr_eq(&graph[0].edges[1].upgrade().unwrap(), &graph[2]));
静态数据
与 std 不同,引用可以指向静态数据而不需要复制,这同样是通过使用 project() 实现的。
# use genrc::rc::Rc;
static BIGBUF: [u8; 1024] = [1; 1024];
let p: Rc<()> = Rc::new(());
let p: Rc<[u8]> = Rc::project(p, |_| &BIGBUF[..]);
assert!(std::ptr::eq(&BIGBUF[..], &*p));
因此,您可以使用 Rc 来跟踪可能拥有的、可能静态的数据,类似于 Cow。
其他内容
夜间Rust allocator_api 支持
allocator_api 功能启用了不稳定分配器API,允许 Rc 和 Arc 使用自定义分配器。
Rc::new_in 返回一个类型为 Rc<T, A> 的 Rc,其中分配器是类型的一部分。如果需要,您可以使用 Rc::erase_allocator() 来隐藏分配器。
生命周期
有些令人惊讶的是,std::rc::Rc 允许您创建一个指向局部变量的 Rc。例如,这是合法的
use std::{cell::Cell, rc::Rc};
let x = Cell::new(1);
let y : Rc<&Cell<i32>> = Rc::new(&x);
x.set(2);
assert_eq!(y.get(), 2);
这种 Rc 的类型是 Rc<&'a T>,其中 'a 是被引用者的生命周期,因此 Rc 不能比被引用者存活更长时间。
genrc::Rc 也可以这样做。但是,如果您使用 project() 将 Rc<&'a T> 转换为指向同一对象的 Rc<T>,后者类型没有地方放置生命周期 'a,因此如果允许这样,这将导致引用存活时间过长,成为一个鲁棒性错误。
为了避免这种情况,类型 Rcl<'a, T> 将生命周期参数添加到 Rc 中。
(实际上,Rc<T> 只是 Rcl<'static, T> 的别名,而 Arc<T> 是 Arcl<'static, T> 的别名。它们都是 genrc::Genrc 的别名,它是生命周期、引用类型、原子性、分配器和唯一性的泛型。)
要在一个短生命周期引用上使用 project(),您必须使用 Rcl::project(),它返回一个具有非静态生命周期的 Rcl。
use genrc::rc::Rcl;
// Imagine we have some JSON data that we loaded from a file
// (or data allocated in an arena, etc)
let bigdata : Vec<u8> = b"Not really json, use your imagination".to_vec();
// buf points directly into `bigdata`, not a copy
let buf : Rcl<[u8]> = Rcl::from_ref(&bigdata[..]);
assert!(std::ptr::eq(&*buf, &bigdata[..]));
let word : Rcl<[u8]> = Rcl::project(buf, |x| &x[4..10]);
assert!(std::ptr::eq(&*word, &bigdata[4..10]));
由于生命周期通常是可以推断的,在大多数情况下 Rcl 与 Rc 的工作方式完全相同。主要的例外是在数据类型中,你可能需要显式指定生命周期。例如,如果你想要一个字段,它是一个 Rc<str> 类型,并且字符串可能存在生命周期较短的情况,你可以这样编写:
use genrc::rc::Rcl;
// Token in a parser where the buffer is an `Rc<str>`, and `text` can point
// directly into the buffer. (Or `text` can point to owned data, e.g. for
// unescaped strings, and callers generally don't have to care.)
struct Token<'a> {
some_data: u32,
text: Rcl<'a, str>
}
当类型擦除具有生命周期的自定义分配器时,也需要生命周期参数,因为 Rc<T> 也会隐藏分配器。
与 std::sync::Arc 和 std::rc::Rc
Rc::from_box 不会从原始盒子中复制对象。相反,它直接以当前的形式获取盒子的所有权,计数在单独的分配中。
如果你泄露了如此多的 Rc 对象,以至于引用计数溢出,std 指针将终止。但是 genrc 不会这样做,因为在 no_std 中没有 abort() 函数。
从 Rc<T> 到 Rc<dyn Trait> 的隐式转换不受支持,因为这需要一些不稳定的特征。但是你可以使用 Rc::project 显式进行转换。[待办:在夜间要求的功能后支持此功能.]
std 指针有各种与 MaybeUninit 相关的方法,用于分配后初始化对象。该 API 在 Genrc 中未提供,因为你可以使用 Option 和 project 在安全代码中完成相同的事情。
# use genrc::rc::{Rc, RcBox, Weak};
// construct the object uninitialized
let mut obj : RcBox<Option<i32>> = Rc::new_unique(None);
// ... later ...
// initialize the object
obj.replace(5);
// project to the inner value that we just created
let obj : Rc<i32> = RcBox::project(obj, |x| x.as_ref().unwrap());
assert_eq!(*obj, 5);
与 std 不同,Rc 和 Arc(以及 RcBox 和 ArcBox)共享单个泛型实现。Rc<T> 是别名 Genrc<'static, T, Nonatomic>,而 Arc<T> 是别名 Genrc<'static, T, Atomic>。这确实使文档看起来有些难看,因为它都在 Genrc 结构中,而不是你通常关心的实际类型。
std::rc::Rc::ptr_eq(a,b) 如果 a 和 b 共享相同的分配,则返回 true,这等同于询问它们是否是相等的指针。但在 genrc 中,这是两个不同的问题:你可以从同一个分配中获得指向两个不同子对象的指针,或者来自两个不同分配的指针指向同一个对象!(例如,它们可能被投影到一个静态对象)。因此,这里我们有 Rc::ptr_eq,它相当于 std::ptr::eq(&*a, &*b),以及 Rc::root_ptr_eq,后者检查计数是否共享。
from_raw 和 into_raw 不可用,因为返回的指针可能与原始分配没有关系。
与 shared-rc 的差异
shared-rc 与此非常相似的 crate;如果我知道 shared-rc 已经存在,我就不会写这个。话虽如此,还有一些差异
-
shared-rc在底层使用Arc和Rc的 std 版本,因此它不支持零分配使用。 -
shared-rc包含一个Owner类型参数,具有显式的erase_owner方法来隐藏它。genrc::arc::Arc总是类型擦除所有者。这节省了指针中一个字节的开销,当类型擦除的shared-rc指向一个无大小类型时。(例如,shared_rc::rc::[u8]是 32 字节,但genrc::rc::[u8]是 24。) -
genrc在原子与共享之间是泛型的。shared-rc使用宏来实现这一点,这使得 rustdocs 更难阅读,但“转到定义”更容易阅读。
与 rc-box 的差异
rc-box crate 在 std Arc/Rc 周围添加了很好的 API:创建后立即知道你有了对其的唯一指针,所以将其放入一个实现 DerefMut 的包装类型中。这个 crate 复制了那个 API。
-
由于
rc-box是基于 std 类型构建的,因此允许弱指针指向其RcBox类型是不安全的,因此它不能像上面图例中的new_cyclic一样替换。 -
在
genrc中的实现是通用的,关于指针是否唯一(GenRc 的 UNIQ 参数)。这允许编写关于指针唯一性的通用代码,这在初始化时可能很有用(例如上面的图创建示例,其中图是一个Vec<RcBox<Node>>,在初始化时,然后被转换为Vec<Rc<Node>>。)
相关 Crates
shared-rc:与这个 crate 类似,但它包装了 std 版本的Arc和Rc而不是重新实现它们。rc-box:已知为唯一版本的 Rc 和 Arc。erasable:擦除其具体类型的指针。rc-borrow:Rc和Arc的借用形式。
待办事项
在一个功能后面实现各种 Unsize 特性。(尽管它们自 1.0 以来没有变化,但它们仍然是实现智能指针所必需的,尽管它们需要在夜间运行。)
如果计数溢出,使行为匹配 std
更丰富的自定义分配器 API。目前只提供了 new_in 和 from_box;应该有 try_* 并支持 no_global_oom_handling。
更多的文档示例。
许可证
genrc 根据 MIT 或 Apache 2.0 许可证进行许可,您选择哪一个。