Как мне преодолеть спички с несовместимыми типами для структур, реализующих ту же черту?

Я пытаюсь написать cat команда для изучения Rust, но я не могу преобразовать аргументы командной строки в структуры чтения.

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

fn main() {
    for arg in env::args().skip(1) {
        let reader = match arg.as_str() {
            "-" => io::stdin(),
            path => File::open(&path).unwrap(),
        };
    }
}

Ошибка:

error[E0308]: match arms have incompatible types
 --> src/main.rs:6:22
  |
6 |         let reader = match arg.as_str() {
  |                      ^ expected struct `std::io::Stdin`, found struct `std::fs::File`
  |
  = note: expected type `std::io::Stdin`
  = note:    found type `std::fs::File`
note: match arm with an incompatible type
 --> src/main.rs:8:21
  |
8 |             path => File::open(&path).unwrap(),
  |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^

Не похоже, что можно полиморфно сопоставлять реализации свойств (связанные). Как я могу использовать либо File или же Stdin как читатель?

5 ответов

Решение

Проблема в том, что stdin() возвращает объект типа Stdio а также File::open(...).unwrap() возвращает объект типа File, В Rust все ответвления должны возвращать значения одного типа.

В этом случае вы, вероятно, хотели вернуть общий Read объект. к несчастью Read это черта, поэтому вы не можете передать его по значению. Самая простая альтернатива - прибегнуть к распределению кучи:

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

fn main() {
    for arg in env::args().skip(1) {
        let reader = match arg.as_str() {
            "-" => Box::new(io::stdin()) as Box<Read>,
            path => Box::new(File::open(&path).unwrap()) as Box<Read>,
        };
    }
}

Вот вариант ответа Лукаса, который избегает бокса:

use std::io::{self, Read};
use std::fs::File;
use std::path::Path;

fn main() {
    if let Some(arg) = std::env::args().nth(1).as_ref() {
        let stdin;
        let file;
        let reader = match arg.as_ref() {
            "-"  => {
                stdin = io::stdin();
                &stdin as &Read
            }
            path => {
                file = File::open(&Path::new(path)).unwrap();
                &file as &Read
            }
        };
    }
}

Хитрость здесь заключается в использовании let привязки, которые инициализируются только в некоторых путях кода, но при этом имеют достаточно длительный срок службы, чтобы иметь возможность использовать их в качестве цели заимствованного указателя.

Принятый ответ не работает с Rustv1.0 больше. Тем не менее, главное утверждение остается верным: спичечное оружие должно возвращать те же типы. Размещение объектов в куче решает проблему.

use std::io::{self, Read};
use std::fs::File;
use std::path::Path;

fn main() {
    if let Some(arg) = std::env::args().nth(1).as_ref() {
        let reader = match arg.as_ref() {
            "-"  => Box::new(io::stdin()) as Box<Read>,
            path => Box::new(File::open(&Path::new(path)).unwrap()) as Box<Read>,
        };
    }
}

coalesceклеть обеспечивает способ сделать это без бокса и таким способом, который является менее многословным, чем мой другой ответ. Идея состоит в том, чтобы использовать простое перечисление, которое может содержать конкретный тип, соответствующий каждой руке, и макрос (coalesce!), который расширяется до match где выражение тела одинаково для каждой руки.

#[macro_use]
extern crate coalesce;

use std::io::{self, Read};
use std::fs::File;
use std::path::Path;
use coalesce::Coalesce2;

fn main() {
    if let Some(arg) = std::env::args().nth(1).as_ref() {
        let reader = match arg.as_ref() {
            "-"  => Coalesce2::A(io::stdin()),
            path => Coalesce2::B(File::open(&Path::new(path)).unwrap()),
        };

        let reader = coalesce!(2 => |ref reader| reader as &Read);

        // the previous line is equivalent to:
        let reader = match reader {
            Coalesce2::A(ref reader) => reader as &Read,
            Coalesce2::B(ref reader) => reader as &Read,
        };
    }
}

Вариант ответа @FrancisGagné : в то время какcoalesceящик является более общим, в данном случае eitherтакже будет выполняться и будет немного менее подробным:

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

fn main() {
    for arg in env::args().skip(1) {
        let mut reader = match arg.as_str() {
            "-" => Either::Right(io::stdin()),
            path => Either::Left(File::open(&path).unwrap()),
        };
        // just for show: Either implements Read!
        let reader: &mut dyn io::Read = &mut reader;
    }
}
Другие вопросы по тегам