我有一段包含不同字节长度字符的文本。
let text = "Hello привет";
我需要在给定开始(包括)和结束(排除)字符索引的情况下截取字符串的一部分。我试过了
let slice = &text[start..end];
出现如下错误
thread 'main' panicked at 'byte index 7 is not a char boundary; it is inside 'п' (bytes 6..8) of `Hello привет`'
我想这是因为西里尔字母是多字节的,[..]
符号使用 byte 索引获取字符。如果我想使用 character 索引进行切片,我可以使用什么,就像我在 Python 中所做的那样:
slice = text[开始:结束]
?
我知道我可以使用 chars()
迭代器并手动遍历所需的子字符串,但有没有更简洁的方法?
最佳答案
代码点切片的可能解决方案
I know I can use the
chars()
iterator and manually walk through the desired substring, but is there a more concise way?
如果你知道确切的字节索引,你可以切片一个字符串:
let text = "Hello привет";
println!("{}", &text[2..10]);
这会打印“llo пр”。所以问题是找出确切的字节位置。您可以使用 char_indices()
轻松做到这一点迭代器(或者你可以使用 chars()
和 char::len_utf8()
):
let text = "Hello привет";
let end = text.char_indices().map(|(i, _)| i).nth(8).unwrap();
println!("{}", &text[2..end]);
作为另一种选择,您可以先将字符串收集到 Vec<char>
中.然后,索引很简单,但是要将其打印为字符串,您必须重新收集它或编写自己的函数来完成。
let text = "Hello привет";
let text_vec = text.chars().collect::<Vec<_>>();
println!("{}", text_vec[2..8].iter().cloned().collect::<String>());
为什么这不简单?
如您所见,这两种解决方案都不是那么好。这是有意为之,原因有二:
作为str
是一个简单的 UTF8 缓冲区,通过 unicode 代码点进行索引是一个 O(n) 操作。通常,人们期望 []
运算符是 O(1) 操作。 Rust 使这种运行时复杂性显式化并且不试图隐藏它。在上面的两个解决方案中,您可以清楚地看到它不是 O(1)。
但更重要的原因:
Unicode 代码点通常不是有用的单位
Python 所做的(以及您认为自己想要的)并不是那么有用。这一切都归结为语言的复杂性以及 unicode 的复杂性。 Python 切片 Unicode 代码点。这就是 Rust char
代表。它有 32 位大(少几位就足够了,但我们四舍五入为 2 的幂)。
但您真正想要做的是切片用户感知的字符。但这是一个明确定义松散的术语。不同的文化和语言将不同的事物视为“一字”。最接近的近似是“字素簇”。这样的集群可以由一个或多个 unicode 代码点组成。考虑这个 Python 3 代码:
>>> s = "Jürgen"
>>> s[0:2]
'Ju'
令人惊讶,对吧?这是因为上面的字符串是:
-
0x004A
拉丁文大写字母 J -
0x0075
拉丁文小写字母 U -
0x0308
结合分娩 - ...
这是作为前一个字符的一部分呈现的组合字符的示例。 Python 切片在这里做了“错误”的事情。
另一个例子:
>>> s = "fire"
>>> s[0:2]
'fir'
也不是您所期望的。这次,fi
实际上是连字 fi
, 这是一个代码点。
Unicode 以令人惊讶的方式表现的例子还有很多。有关详细信息和示例,请参阅底部的链接。
因此,如果您想使用应该能够在任何地方使用的国际字符串,请不要进行代码点切片!如果您确实需要从语义上将字符串视为一系列字符,请使用字素簇。为此, crate unicode-segmentation
非常有用。
有关此主题的更多资源:
关于string - 切片包含 Unicode 字符的字符串,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51982999/