Tokubara
Tokubara

Reputation: 470

rust how to make a variable has different types in different conditions

fn main() -> std::io::Result<()> {
  let path = std::env::args().nth(1).expect("usage: minidecaf <input path>");
  let input = std::fs::read_to_string(path)?;
  let output_path = std::env::args().nth(2);
  let writer;
  if output_path.is_none() {
      writer = std::io::stdout();
  } else {
      writer = std::fs::File::open(output_path.unwrap()).unwrap();
  }
  minidecaf::run(&input, &mut writer)
}

writer should be stdout() when output_path command line argument is not provided and be File when output_path command line argument is provided. This code does not work, because the type of writer cannot be determined in compile time.

I change it to the following. Since both File and Stdout implements std::io::Write, and what I need is a reference to std::io::Write. As the signature of function run is:

pub fn run(input: &str, output: &mut impl std::io::Write) -> std::io::Result<()>
fn main() -> std::io::Result<()> {
  let path = std::env::args().nth(1).expect("usage: minidecaf <input path>");
  let input = std::fs::read_to_string(path)?;
  let output_path = std::env::args().nth(2);
  let mut writer : &mut std::io::Write;
  if output_path.is_none() {
      writer = &mut std::io::stdout();
  } else {
      writer = &mut std::fs::File::open(output_path.unwrap()).unwrap();
  }
  minidecaf::run(&input, &mut writer)
}

But it does not work, too. It has lifetime problems.

Then I change it to:

fn main() -> std::io::Result<()> {
  let path = std::env::args().nth(1).expect("usage: minidecaf <input path>");
  let input = std::fs::read_to_string(path)?;
  let output_path = std::env::args().nth(2);
  minidecaf::run(&input, &mut (if output_path.is_none() {std::io::stdout()} else {std::fs::File::open(output_path.unwrap()).unwrap()}) )
}

It has the same problem with the first piece of code.

error[E0308]: `if` and `else` have incompatible types
  --> src/main.rs:11:83
   |
11 |   minidecaf::run(&input, &mut (if output_path.is_none() {std::io::stdout()} else {std::fs::File::open(output_path.unwrap()).unwrap()}) )
   |                                                          -----------------        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `Stdout`, found struct `File`
   |                                                          |
   |                                                          expected because of this

So how can I make writer be stdout() when output_path command line argument is not provided and be File when output_path command line argument is provided?

Upvotes: 0

Views: 332

Answers (3)

Jmb
Jmb

Reputation: 23443

As you noted, you need writer to be some kind of reference to std::io::Write, but you also need someone to own the corresponding value. The obvious solution here is to use a Box:

fn run(input: &str, output: &mut impl std::io::Write) -> std::io::Result<()> {
    unimplemented!();
}

fn main() -> std::io::Result<()> {
    let path = std::env::args()
        .nth(1)
        .expect("usage: minidecaf <input path>");
    let input = std::fs::read_to_string(path)?;
    let output_path = std::env::args().nth(2);
    let mut writer: Box<dyn std::io::Write>;
    if output_path.is_none() {
        writer = Box::new(std::io::stdout());
    } else {
        writer = Box::new(std::fs::File::open(output_path.unwrap()).unwrap());
    }
    run(&input, &mut writer)
}

Playground

Upvotes: 2

L. F.
L. F.

Reputation: 20639

To avoid the overhead of dynamic dispatch, it is possible to use the Either type from the either crate to wrap the value, which automatically implements all (most?) traits from the standard library if possible:

use {
    either::Either,
    std::{fs::File, io},
};

let mut writer = match output_path {
    Some(path) => Either::Left(File::open(path)?),
    None => Either::Right(io::stdout()),
};

(playground)

Here, using a match expression is conceptually clearer in my opinion.


Without using external crates, it is also possible to mimic the behavior of an Either enum using variables:

use std::{env, fs::File, io};

let (mut stdout, mut file);

let writer: &mut dyn io::Write = match output_path {
    Some(path) => {
        file = File::open(path)?;
        &mut file
    }
    None => {
        stdout = io::stdout();
        &mut stdout
    }
};

(playground)

Upvotes: 1

Campbell He
Campbell He

Reputation: 535

I think you may want to read more about how to use trait.

Here is a sample code may do what you want:

#[derive(Default)]
struct A {
    a: usize,
}

#[derive(Default)]
struct B {
    b: usize,
}

trait C {
    fn test(&self);
}

impl C for A {
    fn test(&self) {
        println!("A");
    }
}

impl C for B {
    fn test(&self) {
        println!("B");
    }
}


fn main() {
    let t: Box<dyn C> = if true {
        Box::new(A::default())
    } else {
        Box::new(B::default())
    };
    t.test();
}

You can try it Here

Upvotes: 1

Related Questions