从字符串中 trim 额外空格的理想方法是什么?

What's the ideal way to trim extra spaces from a string?

我正在处理需要用单个 space 替换多个 spaces 的字符串。看起来其中大部分只是人为错误,但我很好奇处理这个问题的理想方式——最好是从 &strString.

的最少分配

到目前为止,这是我的以下方法:

const SPACE: &str = " ";
const TWO_SPACES: &str = "  ";

/// Replace multiple spaces with a single space
pub fn trim_whitespace(s: &str) -> String {
    let mut new_str: String = s.trim().to_owned();
    while new_str.contains(TWO_SPACES) {
        new_str = new_str.replace(TWO_SPACES, SPACE);
    }
    new_str
}

let result = trim_whitespace("Hello     world! ");
assert_eq!(result, "Hello world!");

您可以使用 split(' '),过滤掉空条目,然后 re-join 通过 space:

s.trim()
    .split(' ')
    .filter(|s| !s.is_empty())
    .collect::<Vec<_>>()
    .join(" ")

// Or, using itertools:
use itertools::Itertools;
s.trim().split(' ').filter(|s| !s.is_empty()).join(" ")

另一种可能性是使用 String::retain() 并删除连续的 space。它也应该更快,因为它只为修剪后的字符串分配一次:

pub fn trim_whitespace(s: &str) -> String {
    let mut new_str = s.trim().to_owned();
    let mut prev = ' '; // The initial value doesn't really matter
    new_str.retain(|ch| {
        let result = ch != ' ' || prev != ' ';
        prev = ch;
        result
    });
    new_str
}

编辑: 我很好奇,所以我用字符串 " a bb cc ddd " 对此处建议的所有版本进行了基准测试(当然,不同的字符串将具有不同的性能特征)。基准代码是 here(需要 criterionitertools)。

结果:

benches/trim_whitespace_replace
                        time:   [846.02 ns 872.71 ns 901.90 ns]
benches/trim_whitespace_retain
                        time:   [146.79 ns 153.07 ns 159.91 ns]
benches/trim_whitespace_split_space
                        time:   [268.61 ns 277.44 ns 287.55 ns]
benches/trim_whitespace_split_space_itertools
                        time:   [392.82 ns 406.92 ns 423.88 ns]
benches/trim_whitespace_split_whitespace
                        time:   [236.38 ns 244.51 ns 254.00 ns]
benches/trim_whitespace_split_whitespace_itertools
                        time:   [395.82 ns 413.59 ns 433.26 ns]
benches/trim_whitespace_split_whitespace_only_one_string
                        time:   [146.25 ns 152.73 ns 159.94 ns]

不出所料,您使用 replace() 的版本是最慢的。我使用 retain() 和 @prog-fh 的更快版本是最快的(我希望他的版本更快,因为它需要复制更少,但显然差异非常小,现代 CPU 复制小块内存非常快。也许在更大的字符串中这会出现)。有点令人惊讶的是,使用 itertools 的 join() 的版本比仅使用标准库 collect() 然后 join() 的版本慢,尽管不需要先收集到向量中。不过我可以解释一下——这个版本在 Display 上使用动态调度,编译器可能无法消除(不过我不确定,需要检查程序集来验证),更糟糕的是,他们可能实际上需要分配更多,因为他们不知道提前需要多少space,他们还需要插入分隔符。

split_whitespace()这个用法很方便

在非常简单的第一个解决方案中分配了一个向量和一个字符串。

第二个解决方案只分配一个字符串,但有点不雅(每次迭代时 if)。

pub fn trim_whitespace_v1(s: &str) -> String {
    // first attempt: allocates a vector and a string
    let words: Vec<_> = s.split_whitespace().collect();
    words.join(" ")
}

pub fn trim_whitespace_v2(s: &str) -> String {
    // second attempt: only allocate a string
    let mut result = String::with_capacity(s.len());
    s.split_whitespace().for_each(|w| {
        if !result.is_empty() {
            result.push(' ');
        }
        result.push_str(w);
    });
    result
}

fn main() {
    let source = "  a   bb cc   ddd    ";
    println!("{:?}", trim_whitespace_v1(source)); // "a bb cc ddd"
    println!("{:?}", trim_whitespace_v2(source)); // "a bb cc ddd"
}