在 Rust 中,我怎样才能减少这段代码的重复性?

In Rust, how can I make this code less repetitive?

目标是写一个函数,获取input_diroutput_dir两个路径,将input_dir中的所有markdown文件转换为html中的文件15=].

我终于设法达到了 运行 但这很令人沮丧。应该很难的部分非常简单:从 Markdown 到 HTML 的实际转换实际上只有一行。看似简单的部分是我花费时间最长的部分。使用路径向量并将所有文件放入其中是我用 glob 板条箱替换的东西。不是因为我无法让它工作,而是因为 if letunwrap 乱七八糟。一个遍历元素列表并找出其中哪些实际上是文件而不是目录的简单函数?如果 if let 我需要四个缩进级别,或者我对 matches.

感到害怕

我做错了什么?

但是让我们从一些事情开始吧,我试图在一个过滤为只包含实际文件的目录中获取项目列表:

use std::fs;
use std::vec::Vec;


fn list_files (path: &str) -> Result<Vec<&str>, &str> {
    if let Ok(dir_list) = fs::read_dir(path) {
        Ok(dir_list.filter_map(|e| {
            match e {
                Ok(entry) => match entry.file_type() {
                    Ok(_) => entry.file_name().to_str(),
                    _ => None
                },
                _ => None
            }
        }).collect())
    } else {
        Err("nope")
    }
}


fn main() {
    let files = list_files("testdir");
    println!("{:?}", files.unwrap_or(Vec::new()));
}

因此,此代码无法构建,因为第 10 行中的文件名寿命不够长。我想我可以以某种方式创建一个拥有的 String 但这会引入另一个嵌套级别,因为 OsStr.to_string() returns a Result.

现在我查看了 glob crate 的代码,他们只使用了一个可变向量:

fn list_files (path: &str) -> Result<Vec<&str>, &str> {
    let mut list = Vec::new();

    if let Ok(dir_list) = fs::read_dir(path) {
        for entry in dir_list {
            if let Ok(entry) = entry {
                if let Ok(file_type) = entry.file_type() {
                    if file_type.is_file() {
                        if let Some(name) = entry.file_name().to_str() {
                            list.push(name)
                        }
                    }
                }
            }
        }

        Ok(list)
    } else {
        Err("nope")
    }
}

这不仅增加了疯狂的嵌套,而且还因同样的问题而失败。如果我从 Vec<&str> 更改为 Vec<String>,它会起作用:

fn list_files (path: &str) -> Result<Vec<String>, &str> {
    let mut list = Vec::new();

    if let Ok(dir_list) = fs::read_dir(path) {
        for entry in dir_list {
            if let Ok(entry) = entry {
                if let Ok(file_type) = entry.file_type() {
                    if file_type.is_file() {
                        if let Ok(name) = entry.file_name().into_string() {
                            list.push(name)
                        }
                    }
                }
            }
        }

        Ok(list)
    } else {
        Err("nope")
    }
}

看来我应该把它应用到我的第一次尝试中,对吧?

fn list_files (path: &str) -> Result<Vec<String>, &str> {
    if let Ok(dir_list) = fs::read_dir(path) {
        Ok(dir_list.filter_map(|e| {
            match e {
                Ok(entry) => match entry.file_type() {
                    Ok(_) => Some(entry.file_name().into_string().ok()),
                    _ => None
                },
                _ => None
            }
        }).collect())
    } else {
        Err("nope")
    }
}

至少短了一点……但它无法编译,因为类型std::vec::Vec<std::string::String>的集合不能从std::option::Option<std::string::String>[=58=类型的元素上的迭代器构建].

在这里很难保持耐心。为什么 .filter_map return Options 而不是仅仅使用它们来过滤?现在,我必须将第 15 行从 }).collect()) 更改为 }).map(|e| e.unwrap()).collect()),这会在结果集上再次迭代。

这不对!

你可以大量依赖 ? operator:

use std::fs;
use std::io::{Error, ErrorKind};

fn list_files(path: &str) -> Result<Vec<String>, Error> {
    let mut list = Vec::new();

    for entry in fs::read_dir(path)? {
        let entry = entry?;
        if entry.file_type()?.is_file() {
            list.push(entry.file_name().into_string().map_err(|_| {
                Error::new(ErrorKind::InvalidData, "Cannot convert file name")
            })?)
        }
    }

    Ok(list)
}

不要忘记,您可以将代码拆分为函数或实现自己的 trait 以简化最终代码:

use std::fs;
use std::io::{Error, ErrorKind};

trait CustomGetFileName {
    fn get_file_name(self) -> Result<String, Error>;
}

impl CustomGetFileName for std::fs::DirEntry {
    fn get_file_name(self) -> Result<String, Error> {
        Ok(self.file_name().into_string().map_err(|_|
            Error::new(ErrorKind::InvalidData, "Cannot convert file name")
        )?)
    }
}

fn list_files(path: &str) -> Result<Vec<String>, Error> {
    let mut list = Vec::new();

    for entry in fs::read_dir(path)? {
        let entry = entry?;
        if entry.file_type()?.is_file() {
            list.push(entry.get_file_name()?)
        }
    }

    Ok(list)
}

迭代器的替代答案,playground

use std::fs;
use std::error::Error;
use std::path::PathBuf;

fn list_files(path: &str) -> Result<Vec<PathBuf>, Box<Error>> {
    let x = fs::read_dir(path)?
        .filter_map(|e| e.ok())
        .filter(|e| e.metadata().is_ok())
        .filter(|e| e.metadata().unwrap().is_file())
        .map(|e| e.path())
        .collect();

    Ok(x)
}

fn main() {
    let path = ".";
    for res in list_files(path).unwrap() {
        println!("{:#?}", res);
    }
}