如何将一个不可变引用升级为可变引用?

5
我有一个程序,围绕着一个共享数据结构展开,提出对数据的更改并在稍后应用这些更改。这些提议的更改将引用核心对象。
在C++或另一种语言中,我会简单地使引用变为非常量,然后在需要时进行突变。但是 Rust 对这种方法不太友好。(我今天早些时候在 IRC 上问过这个问题,但遗憾的是我还是卡住了。)
为了帮助解决问题,我为在剧院预订门票制作了一个精简示例,其中剧院是数据结构,预订是提议的更改,如果我能想出如何让它运作,run方法将应用这些更改!
首先,定义一些数据结构。一个剧院有很多行,每一行有很多座位:
use std::sync::{Arc, RwLock};
use std::thread;

struct Theatre { rows: Vec<Row> }
struct Row { seats: Vec<Seat> }

struct Seat {
    number: i32,
    booked: bool,
}

impl Seat {
    fn new(number: i32) -> Seat {
        Seat { number: number, booked: false }
    }

    fn book(&mut self) {
        self.booked = true;
    }
}

这里,get_booking方法会搜索空座位,并返回一个包含该座位引用的Booking对象。
impl Theatre {
    fn get_booking<'t>(&'t self, number: i32) -> Option<Booking<'t>> {
        for row in self.rows.iter() {
            for seat in row.seats.iter() {
                if seat.number == number && seat.booked == false {
                    return Some(Booking { seats: vec![ seat ] })
                }
            }
        }

        None
    }
}

但这就是我卡住的地方了。 run方法可以对整个剧院进行可变访问(从其参数中),并且它知道要改变哪个座位(self)。但是,由于self不可变,即使包含它的剧院是可变的,也无法改变它。
struct Booking<'t> {
    seats: Vec<&'t Seat>
}

impl<'t> Booking<'t> {
    fn describe(&self) {
        let seats: Vec<_> = self.seats.iter().map(|s| s.number).collect();
        println!("You want to book seats: {:?}", seats);
    }

    fn run(&self, _theatre: &mut Theatre) {
        let mut seat = ??????;
        seat.book();
    }
}

最后,如果它起作用,将使用它的主要方法。
fn main() {
    // Build a theatre (with only one seat... small theatre)
    let theatre = Theatre { rows: vec![ Row { seats: vec![ Seat::new(7) ] } ] };
    let wrapper = Arc::new(RwLock::new(theatre));

    // Try to book a seat in another thread
    let thread = thread::spawn(move || {
        let desired_seat_number = 7;

        let t = wrapper.read().unwrap();
        let booking = t.get_booking(desired_seat_number).expect("No such seat!");

        booking.describe();

        let mut tt = wrapper.write().unwrap();
        booking.run(&mut tt);  // this is never actually reached because we still have the read lock
    });

    thread.join().unwrap();
}

很烦人的是,我确切地知道为什么我的当前代码不起作用——我只是想不出Rust希望我的程序格式是什么样子的。这里有一些我不想做的事情:
  • 最简单的解决方案是让 Booking 持有一个座位的索引,而不是一个引用:在这种情况下,使用 rowseat usize字段。然而,尽管我的剧院使用O(1)向量,我也想引用大树中间的值,在这种情况下,必须迭代查找值,会更加昂贵。这也意味着您不能够在没有传递整个剧院的情况下得到座位号(在describe函数中)。
  • 它也可以通过让Booking持有座位的可变引用来解决,然后我可以像平常一样进行修改。然而,这意味着我一次只能有一个建议更改:例如,我不能有一个预订清单并一次应用所有更改,或者有两个预订并仅应用一个。
我感觉我非常接近拥有Rust将接受的东西,但不知道如何构建我的程序来适应它。那么,有什么建议吗?(双关语)
1个回答

3

首先,这是代码:

use std::sync::{Arc, RwLock};
use std::thread;
use std::sync::atomic::{AtomicBool, Ordering};

struct Theatre { rows: Vec<Row> }
struct Row { seats: Vec<Seat> }

struct Seat {
    number: i32,
    booked: AtomicBool,
}

impl Seat {
    fn new(number: i32) -> Seat {
        Seat { number: number, booked: AtomicBool::new(false) }
    }

    fn book(&self) {
        self.booked.store(true, Ordering::Release);
        println!("Booked seat: {:?}", self.number);
    }
}

impl Theatre {
    fn get_booking<'t>(&'t self, number: i32) -> Option<Booking<'t>> {
        for row in self.rows.iter() {
            for seat in row.seats.iter() {
                if seat.number == number && seat.booked.load(Ordering::Acquire) == false {
                    return Some(Booking { seats: vec![ seat ] })
                }
            }
        }

        None
    }
}

struct Booking<'t> {
    seats: Vec<&'t Seat>
}

impl<'t> Booking<'t> {
    fn describe(&self) {
        let seats: Vec<_> = self.seats.iter().map(|s| s.number).collect();
        println!("You want to book seats: {:?}", seats);
    }

    fn run(&self) {
        for seat in self.seats.iter() {
            seat.book();
        }
    }
}

fn main() {
    // Build a theatre (with only one seat... small theatre)
    let theatre = Theatre { rows: vec![ Row { seats: vec![ Seat::new(7) ] } ] };
    let wrapper = Arc::new(RwLock::new(theatre));

    // Try to book a seat in another thread
    let thread = thread::spawn(move || {
        let desired_seat_number = 7;

        let t = wrapper.read().unwrap();
        let booking = t.get_booking(desired_seat_number).expect("No such seat!");

        booking.describe();

        booking.run();
    });

    thread.join().unwrap();
}

在playpen中查看

有两个重要的更改:

  1. booked字段从bool更改为AtomicBool原子类型提供了一个store方法,可用于不可变引用。 因此,我们可以使Seat :: book()通过不可变引用来获取self。 如果您有一个不被原子类型覆盖的更复杂的类型,则应改为使用MutexRwLock
  2. 我删除了&mut Theatre参数上的Booking :: run()。 如果这不可接受,请留下评论以解释为什么需要该参考。

如您所发现,您无法同时在RwLock上拥有读锁和写锁。 但是,Booking不能比Theatre上的读锁存活更长时间,因为它包含Theatre内部的引用。 一旦您释放了读锁,您无法保证在以后获取另一个锁时获得的引用将保持有效。 如果这是个问题,请考虑使用Arc而不是简单的借用指针(&)。


非常感谢您!这是否意味着在我的数据结构中,我需要将所有想要更改的值都包装在Mutex或RwLock后面? - Ben S
是的。另外,如果您选择使用 Arc,您也需要将它们包装在 Arc 中。 - Francis Gagné
@BenS:如果你使用多线程,那么MutexRwLock是必需的,否则CellRefCell就足够了。 - Matthieu M.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接