如何使我的Rust函数更加通用和高效?

8

我有一个能够工作的函数,但它比我想象中更加专业化,并且存在一些低效率的问题,我希望进行改进。

这是一个能够工作但存在缺陷的函数:

fn iter_to_min<T>(i:T) -> i64 where T:Iterator<Item=String>{
    i.collect::<Vec<String>>()
        .iter()
        .flat_map(|s|s.split_whitespace())
        .map(str::trim)
        .map(str::parse::<i64>)
        .map(Result::unwrap)
        .min()
        .expect("No min found.")
}

我不喜欢这个实现的原因如下:
  • i64 被硬编码,我希望可以重复使用这个函数并适用于 u64 和其他可能的返回类型
  • 它只是为了立即迭代而收集输入,效率不高(无原因堆分配)
  • 传递给 flat_map 的闭包在所有情况下可能都不能被LLVM优化掉
而我能得到的最接近理想函数的代码如下:
use std::str::FromStr;

fn iter_to_min<T,U>(i:T) -> U where T:Iterator<Item=String>,U: Ord+FromStr{
    i.flat_map(str::split_whitespace)
        .map(str::trim)
        .map(str::parse::<U>)
        .map(Result::unwrap)
        .min()
        .expect("No min found.")
}

我看到的问题如下:
  • 传递给str::split_whitespace的参数是String类型,不能强制转换为str
  • 传递给str::split_whitespace的参数未知是否存在足够长的生命周期
  • Result::unwrap 报错,因为类型<U as core::str::FromStr>::Err没有实现特质core::fmt::Debug

我认为,通过巧妙的生命周期标注和特质要求,至少可以解决其中两个问题,也许还有一种方法可以三全其美。

以下是使用一些建议修复的示例代码:

use std::io::BufRead;
use std::str::FromStr;
use std::fmt::Debug;

fn iter_to_min<T,U>(i:T) -> U where T:Iterator<Item=String>,U: Ord+FromStr, U::Err: Debug{
    i.collect::<Vec<String>>()
        .iter()
        .flat_map(|s|s.split_whitespace())
        .map(str::trim)
        .map(str::parse::<U>)
        .map(Result::unwrap)
        .min()
        .expect("No min found.")
}

fn main() {
    let a: Vec<_> = std::env::args().skip(1).collect();
    let m:i64 = if a.is_empty() {
        let s = std::io::stdin();
        let m = iter_to_min(s.lock().lines().map(Result::unwrap));
        m
    }else{
        iter_to_min(a.into_iter())
    };
    println!("{}", m);
}

1
如果您能够发布一个完整的、最小化的示例,我就可以将其粘贴到本地文件中并运行起来,那就太好了 :) - Magnus Hoff
2个回答

8
很遗憾,在保持通用性和不分配内存的情况下,无法完成你想要的功能。原因是你需要有人拥有你的字符串数据,但如果在拥有的字符串迭代器上执行flat_map(str::split_whitespace),那么就没有人再保留这些拥有的字符串了,因为str::split_whitespace只借用它所调用的字符串。但是,如果将所有权推向调用链,你将无法完全通用并通过值接受拥有的字符串。
因此,有两种解决方案:将整个迭代器预先收集到Vec<String>中(或者单独将split_whitespace()产生的项目转换为占有的字符串,并再次将它们收集到Vec中),或者接受引用的迭代器。
以下是我能想到的第一种解决方案的最通用版本:
use std::str::FromStr;
use std::fmt::Debug;

fn iter_to_min<S, T, U>(i: T) -> U
    where S: Into<String>,
          T: IntoIterator<Item=S>,
          U: Ord + FromStr,
          U::Err: Debug
{
    i.into_iter()
        .map(Into::into)
        .collect::<Vec<_>>()
        .iter()
        .flat_map(|s| s.split_whitespace())
        .map(str::parse::<U>)
        .map(Result::unwrap)
        .min()
        .expect("No min found")
}

(在此处尝试

它基本上与您的第一个相同,但更通用。还请注意,您不需要修剪split_whitespace()之后的字符串部分-后者将确保字符串部分没有两侧的空格。 Into<String>绑定允许传递&strString迭代器,在后一种情况下,不会进行额外的副本。

或者,您可以单独将每行拆分为拥有的字符串:

fn iter_to_min<S, T, U>(i: T) -> U
    where S: AsRef<str>,
          T: IntoIterator<Item=S>,
          U: Ord + FromStr,
          U::Err: Debug
{
    i.into_iter()
        .flat_map(|s| s.as_ref().split_whitespace().map(String::from).collect::<Vec<_>>())
        .map(|s| s.parse::<U>())
        .map(Result::unwrap)
        .min()
        .expect("No min found")
}

在这里,我们只需要从迭代器项中获得一个 &str,而不是 String,所以我使用了 AsRef<str>。然而,每一行不仅必须被转换为一组 String;由于正如上面描述的那样,这个序列必须被收集到一个向量中,否则就没有人来保留类型为 S 的原始值,以免被销毁。
但是,如果你愿意失去一些通用性,是可以避免使用 .map(String::from).collect::<Vec<_>>() 的。这是我上面提到的第二种解决方案。我们可以接受对引用的迭代器。
fn iter_to_min<'a, S: ?Sized, T, U>(i: T) -> U
    where S: AsRef<str> + 'a,
          T: IntoIterator<Item=&'a S>,
          U: Ord + FromStr,
          U::Err: Debug
{
    i.into_iter()
        .map(AsRef::as_ref)
        .flat_map(str::split_whitespace)
        .map(|s| s.parse::<U>())
        .map(Result::unwrap)
        .min()
        .expect("No min found")
}

您可以在此处尝试:链接

简单来说,现在有一些值S被其他人所拥有,它们的生命周期比iter_to_min()的作用域更长,所以您不需要将每个部分转换为String,也不需要将整个拆分结果收集到一个Vec<String>中。然而,您将无法将Vec<String>传递给此函数;但您仍可以传递vec.iter()

let v: Vec<String> = vec!["0".into(), "1".into()];
iter_to_min(v.iter())

在所有这些示例中,我已将Iterator更改为IntoIterator - 这几乎总是您想要使用的而不仅仅是Iterator。例如,它允许您直接将集合传递给此类函数。其次,我添加了U::Err: Debug条件,这对于使Result::unwrap工作是必要的。最后,为了解决“String not coercing to &str”的问题,您始终可以使用显式闭包和方法语法,这会为您执行此强制转换。


3
没有额外分配空间的解决方案
use std::str::FromStr;

fn iter_to_min<T, U>(i: T) -> Option<U>
    where T: Iterator<Item = String>,
          U: Ord + FromStr
{
    i.filter_map(|string| {
            string.split_whitespace()
                .map(str::trim)
                .map(str::parse::<U>)
                .filter_map(Result::ok)
                .min()
        })
        .min()
}

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