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