切片包含Unicode字符的字符串

Sas*_*nov 18 string unicode slice rust

我有一段文字,字符长度不同.

let text = "Hello ??????";
Run Code Online (Sandbox Code Playgroud)

给定开始(包含)和结束(排除)字符索引时,我需要获取一小段字符串.我试过这个

let slice = &text[start..end];
Run Code Online (Sandbox Code Playgroud)

并得到以下错误

thread 'main' panicked at 'byte index 7 is not a char boundary; it is inside '?' (bytes 6..8) of `Hello ??????`'
Run Code Online (Sandbox Code Playgroud)

我想这是因为西里尔字母是多字节的,并且[..]符号使用字节索引来表示字符.如果我想使用字符索引切片,我可以使用什么,就像我在Python中做的那样:

slice = text[start:end] ?

我知道我可以使用chars()迭代器并手动遍历所需的子字符串,但是有更简洁的方法吗?

Luk*_*odt 24

码点切片的可能解决方案

我知道我可以使用chars()迭代器并手动遍历所需的子字符串,但是有更简洁的方法吗?

如果您知道确切的字节索引,则可以对字符串进行切片:

let text = "Hello ??????";
println!("{}", &text[2..10]);
Run Code Online (Sandbox Code Playgroud)

这打印"lloпр".所以问题是找出确切的字节位置.你可以用做很容易char_indices()迭代器(或者你可以使用chars()同char::len_utf8()):

let text = "Hello ??????";
let end = text.char_indices().map(|(i, _)| i).nth(8).unwrap();
println!("{}", &text[2..idx]);
Run Code Online (Sandbox Code Playgroud)

作为另一种选择,您可以先将字符串收集到Vec<char>.然后,索引很简单,但要将其作为字符串打印,您必须再次收集它或编写自己的函数来完成它.

let text = "Hello ??????";
let text_vec = text.chars().collect::<Vec<_>>();
println!("{}", text_vec[2..8].iter().cloned().collect::<String>());
Run Code Online (Sandbox Code Playgroud)

为什么这不容易?

如您所见,这些解决方案都不是那么好.这是故意的,原因有两个:

由于str是简单的UTF8缓冲区,因此通过unicode代码点进行索引是一种O(n)操作.通常,人们期望[]操作员是O(1)操作.Rust使这个运行时复杂性显而易见,并不试图隐藏它.在上面的两个解决方案中,您可以清楚地看到它不是O(1).

但更重要的原因是:

Unicode代码点通常不是一个有用的单元

Python所做的(以及你认为你想要的)并不是那么有用.这一切都归结为语言的复杂性以及unicode的复杂性.Python切片Unicode 代码点.这就是Rust所char代表的.它是32位大(少了一些就足够了,但我们的功能为2).

但你真正想要做的是切割用户感知的角色.但这是一个明确松散定义的术语.不同的文化和语言将不同的东西视为"一个字".最接近的是"字形簇".这样的集群可以包含一个或多个unicode代码点.考虑这个Python 3代码:

>>> s = "Ju?rgen"
>>> s[0:2]
'Ju'
Run Code Online (Sandbox Code Playgroud)

令人惊讶,对吗?这是因为上面的字符串是:

  • 0x004A 拉丁文大写字母J
  • 0x0075 拉丁文小写字母U.
  • 0x0308 结合DIAERESIS
  • ...

这是作为前一个字符的一部分呈现的组合字符的示例.Python切片在这里做了"错误"的事情.

另一个例子:

>>> s = "?re"
>>> s[0:2]
'?r'
Run Code Online (Sandbox Code Playgroud)

也不是你所期望的.这一次,fi实际上是连字?,这是一个代码点.

有更多的例子,Unicode以令人惊讶的方式表现.有关更多信息和示例,请参阅底部的链接.

因此,如果您想使用应该能够在任何地方工作的国际字符串,请不要进行代码点切片!如果您确实需要在语义上将字符串视为一系列字符,请使用字形集群.要做到这一点,箱子unicode-segmentation是非常有用的.


关于这个主题的更多资源:

  • unicode分段板条箱无法解决“ fire”示例的问题-不会将连字分为两个字符。(我不是在说这个,而是在澄清,因为这个答案可能会给人以不同的印象。) (2认同)

Tim*_*ann 7

UTF-8编码的字符串可能包含由多个字节组成的字符.在您的情况下,?从索引6(包括)开始,到第8位(不包括)结束,因此索引7不是字符的开头.这就是您的错误发生的原因.

你可以用它str::char_indices来解决这个问题(记住,在UTF-8中找到一个位置是O(n)):

fn get_utf8_slice(string: &str, start: usize, end: usize) -> Option<&str> {
    assert!(end >= start);
    string.char_indices().nth(start).and_then(|(start_pos, _)| {
        string[start_pos..]
            .char_indices()
            .nth(end - start + 1)
            .map(|(end_pos, _)| &string[start_pos..end_pos])
    })
}
Run Code Online (Sandbox Code Playgroud)

操场

你可以使用,str::chars()如果你没有得到String:

let string: String = text.chars().take(end).skip(start).collect();
Run Code Online (Sandbox Code Playgroud)

  • 该功能没有按预期工作;恐慌示例:`get_utf8_slice("héllo", 2, 3)`。将最后一个范围“[start_pos..end_pos]”替换为“[start_pos..start_pos+end_pos]”可以解决此问题。 (3认同)