首页 > 解决方案 > 跟踪工厂及其产品之间所有权的惯用方式是什么?

问题描述

我有一个创建另一个类的实例的类。有时,它需要对其产品做出反应或以其他方式使用它。但是,如果它被传递给它不拥有的产品,它可能会造成麻烦。我有以下解决方案:

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 是否有更好的方法来处理这种情况?

编辑:

这只是一个最小的例子。完整的代码是一个链表库。目前我正在创建一个允许从列表中删除节点的函数。这是通过调用 on 来完成的List.remove(Node)。但是,我需要确保该节点确实属于提供的列表。因为,如果您删除一个头、一个尾或最后一个元素,则必须更新 List。如果您提供的 List 和 Node 不匹配,最终结果将不正确。

编辑2:

我已经确认重用内存地址肯定是个问题。此外,虽然时间戳很有帮助,但如果没有随机化,我担心它仍然不够。

标签: rustfactory

解决方案


如果您愿意采用“全局递增 ID”的方法,那么您想要的是Id具有这样一个接口的类型,您可以将其存储在您的ParentChild类型中:

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 { ... } }

有几种方法可以实现这种类型,具体取决于您的需要。如果您从不需要在线程之间移动或共享一个ParentChild,您可以使用一个简单的线程本地计数器:

#[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不实现Sendor Sync,因此对 an 的任何引用Id仍然局限于创建它的线程。你可以使用#![feature(negative_impls)]更清晰的解决方案impl !Send/!Sync for Id { },但这是不稳定的,所以我们只是添加一个不是Send或的虚拟字段Sync(因为原始指针不是发送/同步并PhantomData共享其参数的Send/Sync状态。)

另请参阅:PhantomData在 Rust 书卷中,Send以及Sync在 Rust 书卷中

如果您确实需要访问多个线程ParentChild从多个线程,事情会变得有点复杂。我们需要将下一个 ID 存储在全局原子整数变量中,但问题是没有“原子检查添加”之类的东西,因此我们无法像在单线程情况下那样简单地检测 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 中是可恢复的(也可以运行析构函数),并且上面的代码只在增加 NEXT_PARENT检测溢出,在这种情况下,有可能Id在恐慌期间或之后创建两个相等的 s。如果这可能违反内存安全,您有两个选择:将 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文档并考虑AtomicUsize改用(尽管这可能会显着增加在 32 位平台上遇到溢出恐慌的机会)。


推荐阅读