I want to move a File
datatype from one struct into another and then drop the original struct.
I have a data structure with a logger of type RefCell<Box<dyn std::io::Write>>
pub struct WriterLogger {
pub logger: RefCell<Box<dyn std::io::Write>>,
}
I also have a data structure with a logger that contains the concrete File type.
pub struct FileLogger {
pub logger: RefCell<File>,
}
As a convenience, I want to allow users to convert a WriterLogger
into a FileLogger
if possible. I do that by casting to Box<dyn Any>
and checking the type.
impl WriterLogger {
fn to_file_logger(self) -> Option<FileLogger> {
let logger: Box<dyn Any> = Box::new(self.logger);
// Attempt to downcast to concrete type
if let Some(file_logger) = logger.downcast_ref::<RefCell<Box<File>>>() {
// file_logger type is &RefCell<Box<File>>
// I want to drop file_logger, and take ownership
// of Box<File>
let file = file_logger.into_inner();
Some(FileLogger {
logger: RefCell::new(*file),
})
} else {
None
}
}
}
However, I can't figure out how move the concreate File
from the WriterLogger to the new FileLogger structure. I get this error on the line where I call file_logger.into_inner()...
cannot move out of *file_logger which is behind a shared reference move occurs because *file_logger has type std::cell::RefCell<std::boxed::Box<std::fs::File>>, which does not implement the Copy trait
Which makes sense since I do have shared references. But what I want to do is just take the value and then drop everything. If I was working with an Option<File>
I could call take()
but I really don't want to change the data structure to have an Option in it. Is there any other way to do this?
Simply, you can't do this with the types you have. Not because of ownership problems but because this isn't how Any
works.
You could only do this if you had a RefCell<Box<dyn Any>>
. All you're doing here is wrapping your RefCell
in a Box<dyn Any>
. The only type you can extract this as is RefCell<Box<dyn std::io::Write>>
.
If you do this with a RefCell<Box<dyn Any>>
then you lose direct access to the Write
trait.
What if we make our own trait that combines Any
with Write
?
trait AnyWrite: Any + std::io::Write {}
impl<T: Any + std::io::Write> AnyWrite for T {}
Now you could use RefCell<Box<dyn AnyWrite>>
instead.
Oh, but the downcast
methods of Any
require exactly a Box<dyn Any>
! So this won't work either. We need a way to convert our type into a Box<dyn Any>
temporarily so we can downcast it.
We can do this by adding a suitable method to our AnyWrite
trait:
trait AnyWrite: std::io::Write {
fn into_boxed_any(self: Box<Self>) -> Box<dyn Any>;
}
impl<T: std::io::Write + Any> AnyWrite for T {
fn into_boxed_any(self: Box<Self>) -> Box<dyn Any> {
Box::new(*self)
}
}
Now, we can perform the downcast like so:
impl WriterLogger {
fn to_file_logger(self) -> Option<FileLogger> {
self.logger
.into_inner()
.into_boxed_any()
.downcast()
.ok()
.map(|file| FileLogger {
logger: RefCell::new(*file),
})
}
}
A few notes about this approach:
pub
here. For POD or "record" types it makes sense but here it leaks a lot of implementation detail that's unnecessary.pub
issue above, you can make AnyWrite
a module-private type, keeping it a secret implementation detail of WriterLogger
.If you wanted a good exercise from here, see if you can figure out how to have to_file_logger
return Result<FileLogger, WriterLogger>
-- that is, if the downcast fails, return back the original WriterLogger
so the caller can do something else with it. Once you do that it would be trivial to impl TryFrom<WriterLogger> for FileLogger
and then you can use the standard .try_into()
to attempt the conversion, too!