【问题标题】:In Rust, how can I make this code less repetitive?在 Rust 中,我怎样才能减少这段代码的重复性?
【发布时间】:2018-02-02 05:53:21
【问题描述】:

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

我终于设法让它运行,但它相当令人沮丧。应该很难的部分非常简单:从 Markdown 到 HTML 的实际转换实际上只有一行。看似简单的部分是我花费时间最长的部分。使用路径向量并将所有文件放入其中是我用glob crate 替换的东西。不是因为我无法让它工作,而是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() 返回一个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&lt;&amp;str&gt; 更改为Vec&lt;String&gt;,它可以工作:

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::option::Option&lt;std::string::String&gt; 类型元素的迭代器上构建std::vec::Vec&lt;std::string::String&gt; 类型的集合

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

这不可能!

【问题讨论】:

  • ok() 返回Option,然后将其包装成Some。你最终得到Option&lt;Option&lt;...&gt;&gt;。从Some(entry.file_name().into_string().ok()) 中删除Some(...)。这不是一个完整的答案,但至少它可以让你继续前进。
  • 如果您的代码有效(我发现很难从阅读您的问题中分辨出来),那么寻求更好的编写方法是better suited for Code Review
  • 谢谢@Shepmaster,但我的问题不是关于那段代码,我只是作为一个例子写的。相反,我想看看我的一般问题是什么导致这段代码如此疯狂地嵌套。

标签: rust ownership borrow-checker


【解决方案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)
}

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

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)
}

【讨论】:

  • 非常感谢@Boiethios!我之前看到过那个运算符,但每次我想使用它时,它都不适合我。
  • 可能是因为它以前只有夜间版本。
  • @koehr 更新了!
【解决方案2】:

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

【讨论】:

  • 谢谢! Rust 编译器在优化这些东西方面有多好?因为我不想一遍又一遍地迭代。
  • 这就是迭代器的美妙之处,这只是一次迭代,因为只有一次调用 collect
  • 这个答案很棒!但是,OP 的代码有所不同,因为在您的代码中,您不会在第一次出现错误时返回。你只是默默地丢弃它们。
猜你喜欢
  • 2017-08-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多