在Rust中,如何使这些代码重复性降低?

时间:2017-08-24 11:09:36

标签: rust ownership borrow-checker

目标是编写一个获取两个路径input_diroutput_dir的函数,并将input_dir中的所有降价文件转换为output_dir中的html文件。

我终于设法让它运行,但它相当令人沮丧。应该很难的部分非常简单:从Markdown到HTML的实际转换实际上只有一行。看似简单的部分是我花费最长的时间。使用路径向量并将所有文件放入其中是我用glob箱子替换的东西。不是因为我无法让它工作,而是if letunwrap混乱。一个简单的函数,迭代元素列表并确定哪些是实际文件而不是目录?如果if let或者我对match es感到惊讶,我需要四个缩进级别。

我做错了什么?

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

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()会返回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> 的元素从迭代器构建。

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

那可能是对的!

2 个答案:

答案 0 :(得分:1)

您可以大量依赖? 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)
}

答案 1 :(得分:1)

迭代器的替代答案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);
    }
}