为什么在 Rust 中实现 C++ 信号和槽时会出​​现 "does not live long enough" 错误?

Why do I get "does not live long enough" errors when reimplementing C++ signals & slots in Rust?

作为学习练习,我一直在将相当标准的 C++ 信号实现翻译成 Rust:

use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::{Rc, Weak};

trait Notifiable<E> {
    fn notify(&self, e: &E);
}

struct SignalData<'l, E>
where
    E: 'l,
{
    listeners: BTreeMap<usize, &'l Notifiable<E>>,
}

struct Signal<'l, E>
where
    E: 'l,
{
    next_id: usize,
    data: Rc<RefCell<SignalData<'l, E>>>,
}

struct Connection<'l, E>
where
    E: 'l,
{
    id: usize,
    data: Weak<RefCell<SignalData<'l, E>>>,
}

impl<'l, E> Signal<'l, E>
where
    E: 'l,
{
    pub fn new() -> Self {
        Self {
            next_id: 1,
            data: Rc::new(RefCell::new(SignalData {
                listeners: BTreeMap::new(),
            })),
        }
    }

    pub fn connect(&mut self, l: &'l Notifiable<E>) -> Connection<'l, E> {
        let id = self.get_next_id();
        self.data.borrow_mut().listeners.insert(id, l);
        Connection {
            id: id,
            data: Rc::downgrade(&self.data),
        }
    }

    pub fn disconnect(&mut self, connection: &mut Connection<'l, E>) {
        self.data.borrow_mut().listeners.remove(&connection.id);
        connection.data = Weak::new();
    }

    pub fn is_connected_to(&self, connection: &Connection<'l, E>) -> bool {
        match connection.data.upgrade() {
            Some(data) => Rc::ptr_eq(&data, &self.data),
            None => false,
        }
    }

    pub fn clear(&mut self) {
        self.data.borrow_mut().listeners.clear();
    }

    pub fn is_empty(&self) -> bool {
        self.data.borrow().listeners.is_empty()
    }

    pub fn notify(&self, e: &E) {
        for (_, l) in &self.data.borrow().listeners {
            l.notify(e);
        }
    }

    fn get_next_id(&mut self) -> usize {
        let id = self.next_id;
        self.next_id += 1;
        id
    }
}

impl<'l, E> Connection<'l, E>
where
    E: 'l,
{
    pub fn new() -> Self {
        Connection {
            id: 0,
            data: Weak::new(),
        }
    }

    pub fn is_connected(&self) -> bool {
        match self.data.upgrade() {
            Some(_) => true,
            None => false,
        }
    }

    pub fn disconnect(&mut self) {
        match self.data.upgrade() {
            Some(data) => {
                data.borrow_mut().listeners.remove(&self.id);
                self.data = Weak::new();
            }
            None => (),
        }
    }
}

impl<'l, E> Drop for Connection<'l, E>
where
    E: 'l,
{
    fn drop(&mut self) {
        self.disconnect();
    }
}

对于简单的测试代码,此编译和行为符合预期:

struct Event {}
struct Listener {}

impl Notifiable<Event> for Listener {
    fn notify(&self, _e: &Event) {
        println!("1: event");
    }
}

fn main() {
    let l1 = Listener {};
    let l2 = Listener {};
    let mut s = Signal::<Event>::new();
    let c1 = s.connect(&l1);
    let mut c2 = s.connect(&l2);

    println!("c2: {}", c2.is_connected());
    s.disconnect(&mut c2);
    println!("c2: {}", c2.is_connected());

    let e = Event {};
    s.notify(&e);

    println!("done!");
}

但是,如果我尝试与设想的用例更相似的东西,它不会编译:

struct Event {}
struct System<'l> {
    signal: Signal<'l, Event>,
}
struct Listener<'l> {
    connection: Connection<'l, Event>,
}

impl<'l> Notifiable<Event> for Listener<'l> {
    fn notify(&self, _e: &Event) {
        println!("1: event");
    }
}

fn main() {
    let mut listener = Listener {
        connection: Connection::new(),
    };
    let mut system = System {
        signal: Signal::new(),
    };

    listener.connection = system.signal.connect(&listener);

    println!("is_connected(): {}", listener.connection.is_connected());
    system.signal.disconnect(&mut listener.connection);
    println!("is_connected(): {}", listener.connection.is_connected());

    let e = Event {};
    system.signal.notify(&e);

    println!("done!");
}

这给出了以下错误:

error[E0597]: `listener` does not live long enough
   --> src/main.rs:147:50
    |
147 |     listener.connection = system.signal.connect(&listener);
    |                                                  ^^^^^^^^ borrowed value does not live long enough
...
157 | }
    | - `listener` dropped here while still borrowed
    |
    = note: values in a scope are dropped in the opposite order they are created

我的问题似乎源于 SignalData,我将听众集合存储为引用:listeners: BTreeMap<usize, &'l Notifiable<E>>。这种对生命周期的要求似乎一直在向外传播。

Connection class(至少在 C++ 中)的目的是允许从 Listener 端断开连接,并管理连接的生命周期,删除 Listener 超出范围时从信号进入。

Connection 的生命周期必须小于或等于 Signal 和相关 Listener 的生命周期。但是,ListenerSignal 的生命周期应该是完全独立的。

有没有办法改变我的实现来实现这个,或者它从根本上被破坏了?

Signals/Slots 很复杂。 真复杂.

在 C++ 中,您可以使用 Boost.Signals, a library crafted by C++ experts which... ah wait no. Despite their expertise, the authors of Boost.Signals didn't manage to make it thread-safe, you should use Boost.Signals2

很有可能,您自己开发的 C++ 实现需要小心使用,以免调用未定义的行为。

这种库的直接移植在 Rust 中是行不通的。 Rust 的目标是提前处理未定义的行为:您必须清楚地将不安全代码标记为... unsafe.


好吧,Signals/Slots 实现类似于观察者,而观察者传统上需要 循环所有权。这在垃圾收集语言中运行良好,但在手动管理内存时需要更多考虑。

最直接的 error-prone 解决方案是使用一对 Rc/Weak(或 Arc/Weak 用于 multi-threaded代码)。由开发人员策略性地放置 Weak 指针来打破循环(以免它们泄漏)。

在 Rust 中,还有另一个障碍:循环所有权意味着 别名。默认情况下,Rust 要求别名内容是不可变的,这是非常有限的。要重新获得可变性,您将需要 内部可变性,使用 CellRefCell(或 Mutex 用于 multi-threaded 代码)。

好消息:尽管存在所有复杂性,但如果您的代码编译通过,它将是安全的(尽管它仍然可能会泄漏)。


为了避免这种设计固有的所有堆分配,另一种解决方案是转向 message-passing 方案。您可以通过对象的 ID 向对象发送消息,而不是直接调用对象的方法。该方案的难点在于消息是异步的,所以调用的方法只能通过回传消息来传达结果。

Citybound is a game developed in Rust using fine-grained actors communicating with such a scheme. There is also the actix actor framework, which has been quite finely tuned performance wise as can be seen on the TechEmpower benchmarks (placed 7 in Round 15).