1

I have some code that needs to create and use a quick_xml::Writer with either a File or Cursor depending on user input. What is the proper Rust way (non-inheritance/no downcasting) to create a Writer with different structs but same trait?

All the answers on stack overflow seem to be old and recommend allocating on the heap with Box, but this no longer works, and personally it feels wrong anyway.

Related but outdated now: How do I overcome match arms with incompatible types for structs implementing same trait?

Code:

use std::fs::File;
use std::io::Cursor;
use quick_xml::Writer;

fn main() {
    let some_option = Some("some_file.txt");

    let writer = match &some_option {
        Some(file_name) => {
            Writer::new(File::create(file_name).unwrap())
        },
        _ => {
            Writer::new(Cursor::new(Vec::new()))
        },
    };
}

Error:

error[E0308]: `match` arms have incompatible types
  --> src\main.rs:13:13
   |
8  |       let writer = match &some_option {
   |  __________________-
9  | |         Some(file_name) => {
10 | |             Writer::new(File::create(file_name).unwrap())
   | |             --------------------------------------------- this is found to be of type `Writer<File>`
11 | |         },
12 | |         _ => {
13 | |             Writer::new(Cursor::new(Vec::new()))
   | |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `File`, found struct `std::io::Cursor`
14 | |         },
15 | |     };
   | |_____- `match` arms have incompatible types
   |
   = note: expected type `Writer<File>`
            found struct `Writer<std::io::Cursor<Vec<u8>>>`

For more information about this error, try `rustc --explain E0308`.
Ken White
  • 123,280
  • 14
  • 225
  • 444
David
  • 692
  • 8
  • 21
  • Why do you think it's outdated, except using bare `Trait` (not `dyn Trait`)? The way to do that is still `dyn Trait` or enums. – Chayim Friedman Jul 14 '22 at 02:01
  • https://stackoverflow.com/questions/70257469/using-map-as-an-iterator-interchangeably-with-vector-iterator/70257918#70257918 – Ry- Jul 14 '22 at 02:03
  • @ChayimFriedman Using a Box in the related link is what I was referring to. I guess this used to work with older versions. – David Jul 14 '22 at 02:06
  • @David A box should still work. Alternatively, you could write an enum with a variant for each type, and implement the trait on the enum. This is basically a form of dynamic dispatch, e.g. it does what `dyn` does for you, but does so without requiring a heap allocation. – cdhowie Jul 14 '22 at 02:11
  • Hmm, a box likely won't work here because there isn't a trait that `Writer` implements to use with `dyn`. But the enum approach can still work. – cdhowie Jul 14 '22 at 02:13
  • @Ry This isn't working either. Only difference between that link answer and my code is I'm not declaring the type of writer. – David Jul 14 '22 at 02:15
  • @cdhowie I've tried the enum way, Box way, and the new way Ry linked and it all results in the same error. – David Jul 14 '22 at 02:16

2 Answers2

1

Both File and Cursor implement std::io::Write, so you can solve this by boxing those inner values, giving yourself a Writer<Box<Write>>:

let writer: Writer<Box<dyn std::io::Write>> = Writer::new(match &some_option {
    Some(file_name) => {
        Box::new(File::create(file_name).unwrap())
    },
    _ => {
        Box::new(Cursor::new(Vec::new()))
   },
});

Note that this requires a heap allocation for the File/Cursor value. Alternatively, you can use the either crate and its primary type Either instead:

let writer = Writer::new(match &some_option {
    Some(file_name) => {
        Either::Left(File::create(file_name).unwrap())
    },
    _ => {
        Either::Right(Cursor::new(Vec::new()))
    },
});

This approach doesn't require any additional heap allocation or indirection. This works because Either implements Write when both the Left and Right variants do.

cdhowie
  • 158,093
  • 24
  • 286
  • 300
  • 1
    can you expand on the enum impl and dynamic dispatch? – David Jul 14 '22 at 02:17
  • 2
    Yes, I am working on an example now. – cdhowie Jul 14 '22 at 02:18
  • 1
    @David Added an example. You basically just have to fill in `impl WriterType` with the methods you need from `Writer`, and `writer_type_impl!` handles the dynamic dispatch for you so you don't have to write out the `match` each time. – cdhowie Jul 14 '22 at 02:26
  • 2
    @David You might also be interested in the [`either`](https://docs.rs/either/latest/either/enum.Either.html) crate. It has [its own macro](https://docs.rs/either/latest/either/macro.for_both.html) for dynamic dispatch. – cdhowie Jul 14 '22 at 02:30
  • Thanks. This seems to make sense but it still feels dirty coming from C++ and or Python. Am I going about this the wrong way completely or do I need to just get used to this? – David Jul 14 '22 at 02:32
  • @David You'd run into the same thing in C++, and in Python everything is effectively boxed and dynamically typed so that's not exactly a fair comparison. – cdhowie Jul 14 '22 at 03:25
  • @David There's a simpler solution than the above, I'm rewriting my answer. – cdhowie Jul 14 '22 at 03:30
  • This is interesting putting the match inside the `new` call. I'll look at this further when I get home tonight. – David Jul 14 '22 at 15:33
1

The dyn thing should be the type parameter to Writer. For example, for a boxed trait object,

let destination: Box<dyn Write> = match &some_option {
    Some(file_name) => {
        Box::new(File::create(file_name).unwrap())
    }
    None => {
        Box::new(Cursor::new(Vec::new()))
    }
};

let writer = Writer::new(destination);

Write has the necessary blanket implementations to make the variations on this work.

Ry-
  • 218,210
  • 55
  • 464
  • 476