如何解决实现相同trait的结构体之间类型不兼容的问题?

26

我正尝试编写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(),
  |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^

似乎无法使用多态方式匹配特质实现者 (相关)。我该如何将FileStdin用作阅读器?

5个回答

28
问题在于stdin()返回一个类型为Stdio的对象,而File :: open(...).unwrap()返回一个类型为File的对象。 在Rust中,match的所有分支必须返回相同类型的值。
在这种情况下,您可能希望返回常见的Read对象。不幸的是Read是一个trait,因此不能通过值传递它。最简单的替代方法是使用堆分配:
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>,
        };
    }
}

16

这是Lukas答案的一种变体,避免了装箱:

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绑定,同时仍然具有足够长的生命周期,以便将它们用作借用指针的目标。


不错!当然,如果所有这些都在另一个函数中,比如 open_reader 中,那么这个方法就无法奏效了。但确实是个好主意。 - Lukas Kalbertodt

15

被接受的答案在 Rust v1.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>,
        };
    }
}

7

coalesce 这个crate提供了一种不需要boxing的方法,并且比我之前的回答更加简洁。其想法是使用一个简单的枚举类型,可以保存对应每个分支的具体类型和一个宏(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,
        };
    }
}

0
@FrancisGagné的答案的变体:虽然coalesce crate更通用,但在这种情况下,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;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接