Penutupan mungkin lebih lama dari fungsi saat ini

Saya baru mulai belajar Rust. Untuk tujuan ini saya menulis ulang proyek C++ saya di Rust, tetapi masalah terbesar adalah penutupan seumur hidup dan semacamnya.

Saya membuat skenario minimal absolut dari masalah saya yang terlihat di sini dan di bawah:

use std::sync::Arc;
use std::cell::{RefCell, Cell};

struct Context {
    handler: RefCell<Option<Arc<Handler>>>,
}

impl Context {
    pub fn new() -> Arc<Context> {
        let context = Arc::new(Context{
            handler: RefCell::new(None),
        });

        let handler = Handler::new(context.clone());

        (*context.handler.borrow_mut()) = Some(handler);

        context
    }

    pub fn get_handler(&self) -> Arc<Handler> {
        self.handler.borrow().as_ref().unwrap().clone()
    }
}

struct Handler {
    context: Arc<Context>,

    clickables: RefCell<Vec<Arc<Clickable>>>,
}

impl Handler {
    pub fn new(context: Arc<Context>) -> Arc<Handler> {
        Arc::new(Handler{
            context: context,

            clickables: RefCell::new(Vec::new()),
        })
    }

    pub fn add_clickable(&self, clickable: Arc<Clickable>) {
        self.clickables.borrow_mut().push(clickable);
    }

    pub fn remove_clickable(&self, clickable: Arc<Clickable>) {
        // remove stuff ...
    }
}

struct Clickable {
    context: Arc<Context>,

    callback: RefCell<Option<Box<Fn()>>>,
}

impl Clickable {
    pub fn new(context: Arc<Context>) -> Arc<Clickable> {
        let clickable = Arc::new(Clickable{
            context: context.clone(),

            callback: RefCell::new(None),
        });

        context.get_handler().add_clickable(clickable.clone());

        clickable
    }

    pub fn remove(clickable: Arc<Clickable>) {
        clickable.context.get_handler().remove_clickable(clickable);
    }

    pub fn set_callback(&self, callback: Option<Box<Fn()>>) {
        (*self.callback.borrow_mut()) = callback;
    }

    pub fn click(&self) {
        match *self.callback.borrow() {
            Some(ref callback) => (callback)(),
            None => (),
        }
    }
}

struct Button {
    context: Arc<Context>,

    clickable: Arc<Clickable>,
}

impl Button {
    pub fn new(context: Arc<Context>) -> Arc<Button> {
        let clickable = Clickable::new(context.clone());

        let button = Arc::new(Button{
            context: context,

            clickable: clickable.clone(),
        });

        let tmp_callback = Box::new(|| {
            button.do_stuff();
        });
        clickable.set_callback(Some(tmp_callback));

        button
    }

    pub fn do_stuff(&self) {
        // doing crazy stuff
        let mut i = 0;

        for j in 0..100 {
            i = j*i;
        }
    }

    pub fn click(&self) {
        self.clickable.click();
    }
}

impl Drop for Button {
    fn drop(&mut self) {
        Clickable::remove(self.clickable.clone());
    }
}

fn main() {
    let context = Context::new();

    let button = Button::new(context.clone());

    button.click();
}

Saya hanya tidak tahu cara menyampaikan referensi di penutupan.

Hal buruk lainnya adalah Handler dan Context saya saling membutuhkan. Apakah ada cara yang lebih baik untuk menciptakan ketergantungan ini?


person hodasemi    schedule 17.08.2017    source sumber


Jawaban (1)


Mematikan kode awal Anda

pub fn new(context: Arc<Context>) -> Arc<Button> {
    let clickable = Clickable::new(context.clone());

    let button = Arc::new(Button{
        context: context,

        clickable: clickable.clone(),
    });

    let tmp_callback = Box::new(|| {
        button.do_stuff();
    });
    clickable.set_callback(Some(tmp_callback));

    button
}

Pertama, perhatikan kesalahan yang Anda dapatkan

    error[E0373]: closure may outlive the current function, but it borrows `button`, which is owned by the current function
   --> src/main.rs:101:37
    |
101 |         let tmp_callback = Box::new(|| {
    |                                     ^^ may outlive borrowed value `button`
102 |             button.do_stuff();
    |             ------ `button` is borrowed here
    |
help: to force the closure to take ownership of `button` (and any other referenced variables), use the `move` keyword, as shown:
    |         let tmp_callback = Box::new(move || {

Memperhatikan blok help di bagian bawah, Anda perlu menggunakan penutupan move, karena ketika fungsi new berakhir, variabel button pada tumpukan akan keluar dari cakupan. Satu-satunya cara untuk menghindarinya adalah dengan memindahkan kepemilikannya ke callback itu sendiri. Dengan demikian Anda akan berubah

let tmp_callback = Box::new(|| {

to

let tmp_callback = Box::new(move || {

Sekarang, Anda akan mendapatkan kesalahan kedua:

    error[E0382]: use of moved value: `button`
   --> src/main.rs:107:9
    |
102 |         let tmp_callback = Box::new(move || {
    |                                     ------- value moved (into closure) here
...
107 |         button
    |         ^^^^^^ value used here after move
    |
    = note: move occurs because `button` has type `std::sync::Arc<Button>`, which does not implement the `Copy` trait

Dan kesalahannya di sini mungkin sedikit lebih jelas. Anda mencoba memindahkan kepemilikan nilai button ke dalam penutupan panggilan balik, tetapi Anda juga menggunakannya di dalam isi fungsi new saat mengembalikannya, dan Anda tidak dapat mencoba dua hal berbeda untuk memiliki nilainya.

Solusi untuk itu semoga sesuai dengan dugaan Anda. Anda harus membuat salinan yang dapat Anda miliki. Anda pasti ingin berubah

let tmp_callback = Box::new(move || {
    button.do_stuff();

to

let button_clone = button.clone();
let tmp_callback = Box::new(move || {
    button_clone.do_stuff();

Sekarang Anda telah membuat objek Button baru, dan mengembalikan Arc untuk objek itu sendiri, sekaligus memberikan kepemilikan Arc kedua pada callback itu sendiri.

Memperbarui

Mengingat komentar Anda, memang ada masalah ketergantungan siklik di sini, karena objek Clickable Anda memegang kepemilikan referensi ke Button, sedangkan Button memegang kepemilikan referensi ke Clickable. Cara termudah untuk memperbaikinya di sini adalah dengan memperbarui kode tersebut untuk ketiga kalinya, dari

let button_clone = button.clone();
let tmp_callback = Box::new(move || {
    button_clone.do_stuff();

to

let button_weak = Arc::downgrade(&button);
let tmp_callback = Box::new(move || {
    if let Some(button) = button_weak.upgrade() {
        button.do_stuff();
    }
});

jadi Clickable hanya akan menyimpan referensi lemah ke Button, dan jika Button tidak lagi direferensikan, panggilan baliknya tidak akan dilakukan.

Anda juga mungkin ingin mempertimbangkan untuk membuat clickables daftar referensi Weak daripada referensi yang kuat, sehingga Anda dapat menghapus item dari daftar tersebut ketika item yang direferensikannya dihapus.

person loganfsmyth    schedule 17.08.2017
comment
Ya, itu solusi yang juga saya temukan. Apakah ada cara lain seperti memastikan bahwa tombol dalam yang dapat diklik dibersihkan ketika tombol akan dijatuhkan, dengan masa pakai? Bagaimanapun, terima kasih atas usaha Anda! - person hodasemi; 18.08.2017
comment
Memperbarui jawabannya. - person loganfsmyth; 18.08.2017
comment
Terima kasih banyak. Selain itu panggilan balik saya sekarang menjadi monster, karena saya memiliki hingga 4 Arc di mana saya harus melakukan hal yang sama seperti dengan tombol. - person hodasemi; 18.08.2017
comment
Anda juga dapat mempertimbangkan untuk menjadikan Clickable suatu sifat, lalu menerapkan sifat tersebut untuk Button dan semacamnya, lalu Anda akan mendaftarkannya. - person loganfsmyth; 18.08.2017