&str的Join迭代器

45

如何将Iterator<&str>转换为一个以常量字符串(如"\n")分隔的String? 例如,给定:

let xs = vec!["first", "second", "third"];
let it = xs.iter();

通过将字符串collect到一个Vec<&str>中,然后join结果,可以生成一个字符串s:

let s = it
    .map(|&x| x)
    .collect::<Vec<&str>>()
    .join("\n");

然而,这样做会为Vec<&str>分配不必要的内存。
是否有更直接的方法?

1
抱歉 - 我之前的回答中去掉了迭代器,但你的问题是要求如何连接迭代器而不分配额外的向量。 - Simon Whitehead
2
请注意,根据您的迭代器的确切特性,将其收集到一个切片向量中,然后再进行连接,实际上可能比使用Websterix的方法或itertools更快,因为SliceConcatExt :: join可以提前计算出完整字符串所需的大小,因此在累加过程中绝对不需要重新分配;而其他方法可能需要重新分配字符串。您一定要进行基准测试。 - Sebastian Redl
1
@chpio 如果迭代器提供了一个好的大小提示,它必须进行分配,但不是重新分配。 - Sebastian Redl
2
这怎么是重复的?! - Matt Joiner
1
问题已重新开放! - Matt Joiner
显示剩余5条评论
5个回答

32
你可以使用 itertools crate 完成这个任务。在例子中,我使用了intersperse助手,它与迭代器的join类似。 cloned()需要将&&str项目转换为&str项目,不会进行任何分配。当rust@1.36获得稳定版本后,最终可以用copied()替换。
use itertools::Itertools; // 0.8.0

fn main() {
    let words = ["alpha", "beta", "gamma"];
    let merged: String = words.iter().cloned().intersperse(", ").collect();
    assert_eq!(merged, "alpha, beta, gamma");
}

游乐场


23

您可以通过使用迭代器的fold函数轻松实现:

let s = it.fold(String::new(), |a, b| a + b + "\n");

完整代码如下:

fn main() {
    let xs = vec!["first", "second", "third"];
    let it = xs.into_iter();

    // let s = it.collect::<Vec<&str>>().join("\n");

    let s = it.fold(String::new(), |a, b| a + b + "\n");
    let s = s.trim_end();

    println!("{:?}", s);
}

Playground

编辑:在Sebastian Redl的评论后我检查了折叠使用的性能成本并在playground上创建了基准测试

你可以看到对于许多迭代方法,fold 的使用需要更长时间。

虽然没有检查分配的内存使用情况


17
你的代码运行缓慢是因为在每次迭代时使用"+"连接创建两个新的字符串。如果你使用单个字符串(参见playground),它可以比collect和join更好地工作(参见playground)。 - mdonoughe
1
v2 使用 black_box(xs).iter().copied() 在 collect+join 上的执行时间现在是 fold 的两倍(black_box(xs) 不重要,xs 是相同的)。<3 微基准测试。 - chpio
我猜他们会将类似 vec!["hey"; 100_000].into_iter().collect<Vec<_>> 这样的东西优化为直接返回原始的 vec?! - chpio
1
是的,他们确实这样做 - chpio
1
另一种解决方案是 let mut it = xs.into_iter(); let first = it.next().unwrap_or("").to_owned(); let r = it.fold(first, |a, b| a + "\n" + b);,这样你最终得到的是一个 String 而不是 &str - d2weber
显示剩余2条评论

6
使用itertools,你不仅可以使用intersperse()还可以使用join()
use itertools::Itertools;

let s = it.join("\n");

它比 intersperse() 更通用(它接受任何实现了 Display 接口的类型),但因此可能会更慢(尽管我没有进行基准测试)。


0
使用Iterator::reduce
fn main() {
    let it = ["1", "2", "3"].into_iter();
    let res = it.map(String::from).reduce(|acc, s| format!("{acc}, {s}")).unwrap_or_default();
    assert_eq!(&res, "1, 2, 3");
}

你可以使用 Cow 来避免不必要的分配。
use std::borrow::Cow;

fn main() {
    let it = ["1", "2", "3"].into_iter();
    let res = it.map(Cow::from).reduce(|mut acc, s| {
        acc.to_mut().push('\n');
        acc.to_mut().push_str(&s);
        acc
    }).unwrap_or_default();
    assert_eq!(&res, "1\n2\n3");
}


-4

在 Rust 文档中有相关的示例:这里

let words = ["alpha", "beta", "gamma"];

// chars() returns an iterator
let merged: String = words.iter()
                          .flat_map(|s| s.chars())
                          .collect();
assert_eq!(merged, "alphabetagamma");

你也可以使用 Extend trait:

fn f<'a, I: Iterator<Item=&'a str>>(data: I) -> String {
    let mut ret = String::new();
    ret.extend(data);
    ret
}

7
这个答案没有重现原作者的需求。原作者询问的是如何在一些常量字符串(例如“\n”)中间插入其他内容。 - Akiner Alkan
1
同时这也可以在不使用 flat_map 的情况下运作,因为 String 已经实现了 Extend<&str> - chpio

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