如何在Rust中将字符串分成块以插入空格

18
我正在尝试学习 Rust。我最近遇到的一个问题是:给定一个长度恰好是 n 的字符串 String,我想将其分成大小为 n 的块,并在这些块之间插入一个空格,然后再收集成单个字符串。
我遇到的问题是,chars() 方法返回的是 Chars 结构体,但由于某些原因它没有实现 SliceConcatExt 特性,所以无法对其调用 chunks()
此外,一旦我成功创建了一个 Chunks 结构体(通过调用 .bytes()),我不知道如何调用 .join(' '),因为元素现在是字节片的 Chunks。
必须有一种我忽略的优雅的方法来解决这个问题。
例如,下面是一个输入和输出,说明了这种情况:
given: whatupmyname, 4
output: what upmy name

这是我的拙劣尝试:

let n = 4;
let text = "whatupmyname".into_string();
text.chars()
    // compiler error on chunks() call
    .chunks(n)
    .collect::<Vec<String>>()
    .join(' ')

我不知道您所指的“字符串大小”是什么意思。一个 é(一个码点,两个字节)可以分成多少块?(两个码点,三个字节)呢?``(两个码点,八个字节)呢? - trent
可能是从字符串创建字符切片的滑动窗口迭代器的重复问题。 - hellow
@trentcl 那很公平,我想我应该澄清一下,这种情况下我只担心 char - 可以由那128个字节表示的字符。虽然这更受限制,但对于我的目的来说足够简单。 - Zeke
@大家好,这与在字符串上创建滑动窗口非常接近,但我认为此处情况有所不同,因为我试图创建块,然后将这些块收集到字符串中。我遇到的问题有两个方面:一旦将字符串转换为Vec<char>,块划分就是可能的,但是收集仍然很棘手。 - Zeke
4个回答

26
问题在于 chars() 和 bytes() 返回的是 Iterator 而不是 slice。你可以使用 as_bytes(),它将给你一个 &[u8]。但是,你不能直接从 &str 中获取 &[char],因为只有字节本身存在,并且必须通过查看每个字符由多少个字节组成来创建 char。你需要做类似这样的事情:
text.chars()
    .collect::<Vec<char>>()
    .chunks(n)
    .map(|c| c.iter().collect::<String>())
    .collect::<Vec<String>>()
    .join(" ");

然而,我不建议这样做,因为它必须为 VecString 分配大量的临时存储空间。相反,你可以像这样做,只需要分配来创建最终的 String

text.chars()
    .enumerate()
    .flat_map(|(i, c)| {
        if i != 0 && i % n == 0 {
            Some(' ')
        } else {
            None
        }
        .into_iter()
        .chain(std::iter::once(c))
    })
    .collect::<String>()

在最后一次收集之前,这仍然是迭代器,通过与一个迭代器进行平铺映射,该迭代器可以是字符本身或空格和字符的组合。


在发布之前,我已经尝试了第一个建议(虽然我对制作不必要的Vec感到不安),但是在collect :: <Vec <String>>()调用时遇到了编译器错误。有些东西说明无法从Iterator <&[char]>构建Vec <String>,这让我感到有点奇怪。编辑:现在运行它,它指出Vec <String>未实现特征FromIterator <&[char]>,所以我想也许我可以实现那个特征? - Zeke
修复了。不过我不建议使用那段代码,因为它需要分配太多的内存。顺便说一下,你无法实现该特性,因为涉及到的特性和类型都不是“你的”。 - JayDepp
哦,我明白了。我以为有一种隐式将字符块转换为字符串的方法,但是使用映射可以实现这一点。此外,flat_map 的概念对我来说有些陌生,但我将尝试解构它:
  • flat_map 通常会展平嵌套结构,但在这种情况下,它用于返回一个迭代器。
  • 如果您在第 n 个字符上,请插入一个包装空格的迭代器,并将其链接到当前迭代器中,以使其位于其前面。否则,None 将被转换为一个迭代器,它将不会产生任何内容。
  • 将迭代器收集到一个字符串中。
- Zeke

4

如果您想通过字符列表创建字符串,可以使用fold

像这样:

text.chars
    .enumerate()
    .fold(String::new(), |acc, (i, c)| {
        if i != 0 && i == n {
            format!("{} {}", acc, c)
        } else {
            format!("{}{}", acc, c)
        }
    })

1
哦,有趣,我也喜欢这个解决方案,并感谢你花时间回复。这是一个非常清晰易懂的解决方案,我本应该想到的。谢谢!我很好奇与JayDepp发布的flat_map解决方案相比,是否存在字符串分配开销。 - Zeke
1
很不幸,确实如此。format!正在创建一个String并返回它。因此,对于每个字符,都会创建一个新的String,其中包含先前的字符串与当前字符和必要时的可选空格。最终,您将得到相同的String,但由于多个中间String,存在相当大的开销。上述方法更好,因为您将拥有一个Iterator<Iterator<Char>>,而flat_map将创建一个准备在最后一刻进行collectIterator<Char>。因此,您只会创建一个最终的String - sterfield

3

这样一个简单的任务可以用单个循环来解决:

fn main() {
    let n = 4;
    let text = "whatupmyname";
    let mut result = String::new();

    for (i, c) in text.chars().enumerate() {
        result.push(c);
        if (i + 1) % n == 0 {
            result.push(' ');
        }
    }
    println!("{:?}", result); // "what upmy name "
}

为什么不用 for 循环? - Chayim Friedman
@Friedman 纯粹是个人口味问题。我喜欢使用 while 循环的代码更加清晰和紧凑。 - Kaplan
我认为不是这样的;语言本身存在for循环就证明使用它更符合Rust的风格,因为任何for循环都可以写成while let Some(v) = iter的形式。 - Chayim Friedman
@Friedman 如果我坚持使用 enumerate()(我更喜欢它而不是 range),那么我将不得不写 v.0 代替 i,并写 v.1 代替 c... - Kaplan
for 循环也接受模式:for (i, c) in text.chars().enumerate() - Chayim Friedman

2
如果要分割的数据大小是固定的,则:
use std::str;

fn main() {
    let subs = "&#8204;&#8203;&#8204;&#8203;&#8204;&#8203;&#8203;&#8204;&#8203;&#8204;".as_bytes()
        .chunks(7)
        .map(str::from_utf8)
        .collect::<Result<Vec<&str>, _>>()
        .unwrap();
        
    println!("{:?}", subs);
}

// >> ["&#8204;", "&#8203;", "&#8204;", "&#8203;", "&#8204;", "&#8203;", "&#8203;", "&#8204;", "&#8203;", "&#8204;"]

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