rust

Are there other options other than `Option` in rust patterns like this?


On seldom occasions we have to invoke some data members' methods that take their ownership but we have only reference access like behind a &mut self. One pattern to deal with this is to wrap them in an Option type and take() them out to proceed.

Below is an example from the book:


struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: Option<mpsc::Sender<Job>>,
}

impl Drop for ThreadPool {
    fn drop(&mut self) {

        drop(self.sender.take());

        for worker in &mut self.workers {
            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

Notice thread and sender are wrapped in Option only to call join and drop in the destructor just before program exits. However almost their whole lifetime (except only before halt) they won't be None. The Option type used here feels more like a work around rather than utilizing its semantic that there could be a value there or not under norm.

Is this pattern prevalent in rust projects or are there some other options for this? My concern is that I may not be able to tell the 'real optional cases' from this seemingly dedicate usage quickly in code bases and there might be lots of as_ref() or unwrap() calls enforced.


Solution

  • If you find Option unwieldy, an alternative is to avoid Drop altogether. This would allow you to "partially move" from self:

    struct Worker {
        id: usize,
        thread: JoinHandle<()>,
    }
    
    pub struct ThreadPool {
        workers: Vec<Worker>,
        sender: Sender<Job>,
    }
    
    impl ThreadPool {
        pub fn wait(self) {
            drop(self.sender);
            for worker in self.workers {
                worker.thread.join().unwrap();
            }
        }
    }
    

    A slightly more involved alternative is to retain Drop and use std::mem::replace() to obtain an owned value out of &mut self. Since Drop doesn't allow partial moves, it requires splitting the type to an outer one that implements Drop and the inner one that doesn't. (But this split is zero-cost at run-time.)

    // definition of Worker unchanged from above
    
    pub struct ThreadPool {
        inner: ThreadPoolInner,
    }
    
    struct ThreadPoolInner {
        workers: Vec<Worker>,
        sender: Sender<Job>,
    }
    
    impl ThreadPoolInner {
        fn wait(self) {
            drop(self.sender);
            for worker in self.workers {
                worker.thread.join().unwrap();
            }
        }
    }
    
    impl Drop for ThreadPool {
        fn drop(&mut self) {
            let empty = ThreadPoolInner {
                workers: vec![],
                sender: mpsc::channel::<Job>().0,
            };
            let owned_inner = std::mem::replace(&mut self.inner, empty);
            owned_inner.wait(); // or inline wait() if you don't want it as separate method
        }
    }
    

    Playground

    This looks a bit strange at first, but it has the benefit of retaining the convenient Drop-based interface without the overhead of Option, all in safe code.