跟踪工厂及其产品之间所有权的惯用方式是什么?
What would be the idiomatic way of tracking ownership between a factory and its products?
我有一个 class 可以创建另一个 class 的实例。有时它需要对其产品做出反应或以其他方式使用它的产品。但是,如果要通过它不拥有的产品,它可能会造成麻烦。我有以下解决方案:
struct Parent {
id: Option<*const Parent>,
name: String,
}
impl Parent {
fn new(name: String) -> Parent {
Parent {
id : None,
name : name,
}
}
fn spawn(&mut self, name: String) -> Child {
if let None = self.id {
self.id = Some(self as *const Parent);
}
Child {
parent: self.id.unwrap(),
name,
}
}
fn is_parent(&self, child: &Child) -> bool {
if self.id.unwrap() == child.parent {
true
} else {
false
}
}
}
struct Child {
parent: *const Parent,
name: String,
}
fn main() {
let mut parent_one = Parent::new(String::from("Bob"));
let mut parent_two = Parent::new(String::from("Ben"));
let child_one = parent_one.spawn(String::from("Barry"));
let child_two = parent_two.spawn(String::from("Bishop"));
if parent_one.is_parent(&child_one) {
println!("{} is the parent of {}.",parent_one.name,child_one.name);
}
if parent_one.is_parent(&child_two) {
println!("{} is the parent of {}.",parent_one.name,child_two.name);
}
if parent_two.is_parent(&child_one) {
println!("{} is the parent of {}.",parent_two.name,child_one.name);
}
if parent_two.is_parent(&child_two) {
println!("{} is the parent of {}.",parent_two.name,child_two.name);
}
}
我担心的第一件事是父级被破坏并且地址被重用。也许需要时间戳以进一步确保所有权?
更进一步,我想知道Rust是否有更好的方法来处理这种情况?
编辑:
这只是一个最小的例子。完整代码是链表库。目前我正在创建一个允许从列表中删除节点的函数。这是通过调用 List.remove(Node)
来完成的。但是,我需要确保该节点确实属于提供的列表。因为,如果您删除了头部、尾部或最后一个元素,则必须更新 List。如果您提供的列表和节点不匹配,最终结果将不正确。
编辑 2:
我已经确认重用内存地址肯定是个问题。此外,虽然时间戳很有用,但没有随机化,我担心它仍然不够。
如果您愿意采用 'globally incremented ID' 方法,您想要的是一个 Id
类型,具有这样的接口,您可以将其存储在 Parent
和 Child
类型:
pub struct Id(...);
impl Id {
pub fn new() -> Id { ... }
}
impl PartialEq<Id> for Id {
fn eq(&self, other: &Id) -> bool { ... }
}
impl Eq for Id { }
impl Clone for Id { fn clone(&self) -> Id { ... } }
有几种方法可以实现这种类型,具体取决于您的需要。如果您永远不需要在线程之间移动或共享 Parent
或 Child
,您可以使用简单的线程本地计数器:
#[derive(Copy, Clone, Eq, PartialEq)]
pub struct Id(u64, PhantomData<*mut ()>);
impl Id {
pub fn new() -> Id {
thread_local! (static NEXT_ID: Cell<u64> = Cell::new(0));
let id = NEXT_ID.with(|cell| {
let id = cell.get();
cell.set(id.checked_add(1).expect("Ran out of IDs!"));
id
});
Id(id, PhantomData)
}
}
此解决方案的唯一微妙之处在于 PhantomData<*mut ()>
字段。它的存在是为了强制 Id
不实现 Send
或 Sync
,因此对 Id
的任何引用仍然限于创建它的线程。使用 #![feature(negative_impls)]
你可以使用更清晰的 impl !Send/!Sync for Id { }
解决方案,但这是不稳定的,所以我们只是添加一个不是 Send
或 Sync
的虚拟字段(因为原始指针不是 Send/Sync 并且 PhantomData
共享其参数的 Send
/Sync
状态。)
另请参阅:PhantomData
in the Rust nomicon, Send
and Sync
in the Rust nomicon
如果您确实需要从多个线程访问 Parent
或 Child
,事情就会变得有点复杂。我们需要将下一个 ID 存储在全局原子整数变量中,但问题是没有 'atomic checked add,' 这样的东西,所以我们无法像在单线程情况下那样简单地检测 ID 换行。代码看起来像这样:
#[derive(Copy, Clone, Eq, PartialEq)]
pub struct Id(u64);
impl Id {
pub fn new() -> Id {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
let id = todo!(); // what goes here?
Id(id)
}
}
您没有说明您是依赖 is_child
检查来确保内存安全还是仅依赖逻辑正确性。如果只是后者,您可以保留 u64::MAX
来表示“我们没有 ID”并以这种方式实现 Id::new
:
pub fn new() -> Id {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
if id == u64::MAX { panic!("Ran out of IDs!"); }
Id(id)
}
由于 Rust 中的 panic 是可恢复的(以及 运行 析构函数),并且上面仅检测溢出 after NEXT_PARENT
已递增,在此在恐慌期间或之后可能会创建两个相等的 Id
的情况。如果这可能违反内存安全,您有两个选择:要么将 panic 更改为 std::process::abort
,要么使用比较和交换循环来检查溢出并避免实际递增计数器,如下所示:
pub fn new() -> Id {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
let id = loop {
let id = NEXT_ID.load(Ordering::Relaxed);
if id == u64::MAX { panic!("Ran out of IDs!");
if let Ok(_) = NEXT_ID.compare_exchange(id, id + 1, Ordering::Relaxed, Ordering::Relaxed) {
break id
}
// another thread changed NEXT_ID after we checked for overflow, try again
};
Id(id)
}
显然,此循环可能会带来小的性能损失。最后,请记住 AtomicU64
被广泛使用,但并非普遍可用。如果考虑到可移植性,请参考 std::sync::atomic
docs 并考虑改用 AtomicUsize
(尽管这可能会显着增加在 32 位平台上发生溢出错误的几率)。
我有一个 class 可以创建另一个 class 的实例。有时它需要对其产品做出反应或以其他方式使用它的产品。但是,如果要通过它不拥有的产品,它可能会造成麻烦。我有以下解决方案:
struct Parent {
id: Option<*const Parent>,
name: String,
}
impl Parent {
fn new(name: String) -> Parent {
Parent {
id : None,
name : name,
}
}
fn spawn(&mut self, name: String) -> Child {
if let None = self.id {
self.id = Some(self as *const Parent);
}
Child {
parent: self.id.unwrap(),
name,
}
}
fn is_parent(&self, child: &Child) -> bool {
if self.id.unwrap() == child.parent {
true
} else {
false
}
}
}
struct Child {
parent: *const Parent,
name: String,
}
fn main() {
let mut parent_one = Parent::new(String::from("Bob"));
let mut parent_two = Parent::new(String::from("Ben"));
let child_one = parent_one.spawn(String::from("Barry"));
let child_two = parent_two.spawn(String::from("Bishop"));
if parent_one.is_parent(&child_one) {
println!("{} is the parent of {}.",parent_one.name,child_one.name);
}
if parent_one.is_parent(&child_two) {
println!("{} is the parent of {}.",parent_one.name,child_two.name);
}
if parent_two.is_parent(&child_one) {
println!("{} is the parent of {}.",parent_two.name,child_one.name);
}
if parent_two.is_parent(&child_two) {
println!("{} is the parent of {}.",parent_two.name,child_two.name);
}
}
我担心的第一件事是父级被破坏并且地址被重用。也许需要时间戳以进一步确保所有权?
更进一步,我想知道Rust是否有更好的方法来处理这种情况?
编辑:
这只是一个最小的例子。完整代码是链表库。目前我正在创建一个允许从列表中删除节点的函数。这是通过调用 List.remove(Node)
来完成的。但是,我需要确保该节点确实属于提供的列表。因为,如果您删除了头部、尾部或最后一个元素,则必须更新 List。如果您提供的列表和节点不匹配,最终结果将不正确。
编辑 2:
我已经确认重用内存地址肯定是个问题。此外,虽然时间戳很有用,但没有随机化,我担心它仍然不够。
如果您愿意采用 'globally incremented ID' 方法,您想要的是一个 Id
类型,具有这样的接口,您可以将其存储在 Parent
和 Child
类型:
pub struct Id(...);
impl Id {
pub fn new() -> Id { ... }
}
impl PartialEq<Id> for Id {
fn eq(&self, other: &Id) -> bool { ... }
}
impl Eq for Id { }
impl Clone for Id { fn clone(&self) -> Id { ... } }
有几种方法可以实现这种类型,具体取决于您的需要。如果您永远不需要在线程之间移动或共享 Parent
或 Child
,您可以使用简单的线程本地计数器:
#[derive(Copy, Clone, Eq, PartialEq)]
pub struct Id(u64, PhantomData<*mut ()>);
impl Id {
pub fn new() -> Id {
thread_local! (static NEXT_ID: Cell<u64> = Cell::new(0));
let id = NEXT_ID.with(|cell| {
let id = cell.get();
cell.set(id.checked_add(1).expect("Ran out of IDs!"));
id
});
Id(id, PhantomData)
}
}
此解决方案的唯一微妙之处在于 PhantomData<*mut ()>
字段。它的存在是为了强制 Id
不实现 Send
或 Sync
,因此对 Id
的任何引用仍然限于创建它的线程。使用 #![feature(negative_impls)]
你可以使用更清晰的 impl !Send/!Sync for Id { }
解决方案,但这是不稳定的,所以我们只是添加一个不是 Send
或 Sync
的虚拟字段(因为原始指针不是 Send/Sync 并且 PhantomData
共享其参数的 Send
/Sync
状态。)
另请参阅:PhantomData
in the Rust nomicon, Send
and Sync
in the Rust nomicon
如果您确实需要从多个线程访问 Parent
或 Child
,事情就会变得有点复杂。我们需要将下一个 ID 存储在全局原子整数变量中,但问题是没有 'atomic checked add,' 这样的东西,所以我们无法像在单线程情况下那样简单地检测 ID 换行。代码看起来像这样:
#[derive(Copy, Clone, Eq, PartialEq)]
pub struct Id(u64);
impl Id {
pub fn new() -> Id {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
let id = todo!(); // what goes here?
Id(id)
}
}
您没有说明您是依赖 is_child
检查来确保内存安全还是仅依赖逻辑正确性。如果只是后者,您可以保留 u64::MAX
来表示“我们没有 ID”并以这种方式实现 Id::new
:
pub fn new() -> Id {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
if id == u64::MAX { panic!("Ran out of IDs!"); }
Id(id)
}
由于 Rust 中的 panic 是可恢复的(以及 运行 析构函数),并且上面仅检测溢出 after NEXT_PARENT
已递增,在此在恐慌期间或之后可能会创建两个相等的 Id
的情况。如果这可能违反内存安全,您有两个选择:要么将 panic 更改为 std::process::abort
,要么使用比较和交换循环来检查溢出并避免实际递增计数器,如下所示:
pub fn new() -> Id {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
let id = loop {
let id = NEXT_ID.load(Ordering::Relaxed);
if id == u64::MAX { panic!("Ran out of IDs!");
if let Ok(_) = NEXT_ID.compare_exchange(id, id + 1, Ordering::Relaxed, Ordering::Relaxed) {
break id
}
// another thread changed NEXT_ID after we checked for overflow, try again
};
Id(id)
}
显然,此循环可能会带来小的性能损失。最后,请记住 AtomicU64
被广泛使用,但并非普遍可用。如果考虑到可移植性,请参考 std::sync::atomic
docs 并考虑改用 AtomicUsize
(尽管这可能会显着增加在 32 位平台上发生溢出错误的几率)。