Rust中字符串String集合的具有使用
作者:Y小夜
Rust开发者经常被字符串困扰的原因
- 倾向于确保暴露出可能的错误。
- 字符串是比很多程序员所想象的要更为复杂的数据结构。
- UTF-8。
字符串是什么
Rust 的核心语言中只有一种字符串类型:字符串 slice str
,它通常以被借用的形式出现,&str
。
字符串(String
)类型由 Rust 标准库提供,而不是编入核心语言,它是一种可增长、可变、可拥有、UTF-8 编码的字符串类型。当 Rustaceans 提及 Rust 中的 "字符串 "时,他们可能指的是 String
或 string slice &str
类型,而不仅仅是其中一种类型。
创建字符串
很多 Vec
可用的操作在 String
中同样可用,事实上 String
被实现为一个带有一些额外保证、限制和功能的字节 vector 的封装。其中一个同样作用于 Vec<T>
和 String
函数的例子是用来新建一个实例的 new
函数
let mut s = String::new();
这新建了一个叫做 s
的空的字符串,接着我们可以向其中装载数据。通常字符串会有初始数据,因为我们希望一开始就有这个字符串。为此,可以使用 to_string
方法,它能用于任何实现了 Display
trait 的类型,比如字符串字面值。
let data = "initial contents"; let s = data.to_string(); // 该方法也可直接用于字符串字面值: let s = "initial contents".to_string();
因为字符串应用广泛,这里有很多不同的用于字符串的通用 API 可供选择。其中一些可能看起来多余,不过都有其用武之地!在这个例子中,String::from
和 .to_string
最终做了完全相同的工作,所以如何选择就是代码风格与可读性的问题了。
let s = String::from("initial contents");
记住字符串是 UTF-8 编码的,所以可以包含任何可以正确编码的数据
let hello = String::from("السلام عليكم"); let hello = String::from("Dobrý den"); let hello = String::from("Hello"); let hello = String::from("שָׁלוֹם"); let hello = String::from("नमस्ते"); let hello = String::from("こんにちは"); let hello = String::from("안녕하세요"); let hello = String::from("你好"); let hello = String::from("Olá"); let hello = String::from("Здравствуйте"); let hello = String::from("Hola");
更新String
String
的大小可以增加,其内容也可以改变,就像可以放入更多数据来改变 Vec
的内容一样。另外,可以方便的使用 +
运算符或 format!
宏来拼接 String
值。
使用push_str和push附加字符串
可以通过 push_str
方法来附加字符串 slice,从而使 String
变长:
let mut s = String::from("foo"); s.push_str("bar");
执行这两行代码之后,s
将会包含 foobar
。push_str
方法采用字符串 slice,因为我们并不需要获取参数的所有权。
let mut s1 = String::from("foo"); let s2 = "bar"; s1.push_str(s2); println!("s2 is {s2}");
push
方法被定义为获取一个单独的字符作为参数,并附加到 String
中。
let mut s = String::from("lo"); s.push('l');
如何拼接字符串
let s1 = String::from("Hello, "); let s2 = String::from("world!"); let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用
执行完这些代码之后,字符串 s3
将会包含 Hello, world!
。s1
在相加后不再有效的原因,和使用 s2
的引用的原因,与使用 +
运算符时调用的函数签名有关。+
运算符使用了 add
函数,这个函数签名看起来像这样:
fn add(self, s: &str) -> String {
解释:s2
使用了 &
,意味着我们使用第二个字符串的 引用 与第一个字符串相加。这是因为 add
函数的 s
参数:只能将 &str
和 String
相加,不能将两个 String
值相加。不过等一下 —— &s2
的类型是 &String
, 而不是 add
第二个参数所指定的 &str
。
如果想要级联多个字符串,+
的行为就显得笨重了:
let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = s1 + "-" + &s2 + "-" + &s3;
这时 s
的内容会是 “tic-tac-toe”。在有这么多 +
和 "
字符的情况下,很难理解具体发生了什么。对于更为复杂的字符串链接,可以使用 format!
宏:
let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = format!("{s1}-{s2}-{s3}");
索引字符串
在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果你尝试使用索引语法访问 String
的一部分,会出现一个错误。
let s1 = String::from("hello"); let h = s1[0];
错误和提示说明了全部问题:Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?为了回答这个问题,我们必须先聊一聊 Rust 是如何在内存中储存字符串的。
内部表示
String
是一个 Vec<u8>
的封装。
let hello = String::from("Hola");
在这里,len
的值是 4,这意味着储存字符串 “Hola” 的 Vec
的长度是四个字节:这里每一个字母的 UTF-8 编码都占用一个字节。那下面这个例子又如何呢?(注意这个字符串中的首字母是西里尔字母的 Ze 而不是数字 3。)
let hello = String::from("Здравствуйте");
我们已经知道 answer
不是第一个字符 3
。当使用 UTF-8 编码时,(西里尔字母的 Ze)З
的第一个字节是 208
,第二个是 151
,所以 answer
实际上应该是 208
,不过 208
自身并不是一个有效的字母。返回 208
可不是一个请求字符串第一个字母的人所希望看到的,不过它是 Rust 在字节索引 0 位置所能提供的唯一数据。用户通常不会想要一个字节值被返回。即使这个字符串只有拉丁字母,如果 &"hello"[0]
是返回字节值的有效代码,它也会返回 104
而不是 h
。
字节、标量值、字形簇
比如这个用梵文书写的印度语单词 “नमस्ते”,最终它储存在 vector 中的 u8
值看起来像这样:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135]
这里有 18 个字节,也就是计算机最终会储存的数据。如果从 Unicode 标量值的角度理解它们,也就像 Rust 的 char
类型那样,这些字节看起来像这样:
['न', 'म', 'स', '्', 'त', 'े']
这里有六个 char
,不过第四个和第六个都不是字母,它们是发音符号本身并没有任何意义。最后,如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母:
["न", "म", "स्", "ते"]
Rust 提供了多种不同的方式来解释计算机储存的原始字符串数据,这样程序就可以选择它需要的表现方式,而无所谓是何种人类语言。
最后一个 Rust 不允许使用索引获取 String
字符的原因是,索引操作预期总是需要常数时间(O(1))。但是对于 String
不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。
字符串切割slice
索引字符串通常是一个坏点子,因为字符串索引应该返回的类型是不明确的:字节值、字符、字形簇或者字符串 slice。因此,如果你真的希望使用索引创建字符串 slice 时,Rust 会要求你更明确一些。为了更明确索引并表明你需要一个字符串 slice,相比使用 []
和单个值的索引,可以使用 []
和一个 range 来创建含特定字节的字符串 slice:
let hello = "Здравствуйте"; let s = &hello[0..4];
这里,s
会是一个 &str
,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着 s
将会是 “Зд”。
如果获取 &hello[0..1]
会发生什么呢?答案是:Rust 在运行时会 panic,就跟访问 vector 中的无效索引时一样。你应该小心谨慎地使用这个操作,因为这么做可能会使你的程序崩溃。
遍历string
操作字符串每一部分的最好的方法是明确表示需要字符还是字节。对于单独的 Unicode 标量值使用 chars
方法。对 “Зд” 调用 chars
方法会将其分开并返回两个 char
类型的值,接着就可以遍历其结果来访问每一个元素了:
for c in "Зд".chars() { println!("{c}"); }
另外 bytes
方法返回每一个原始字节,这可能会适合你的使用场景:
for b in "Зд".bytes() { println!("{b}"); }
不过请记住有效的 Unicode 标量值可能会由不止一个字节组成。
字符串不简单
总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理 String
数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何预先处理 UTF-8 数据。这种权衡取舍相比其他语言更多的暴露出了字符串的复杂性,不过也使你在开发周期后期免于处理涉及非 ASCII 字符的错误。
到此这篇关于Rust中字符串String集合的具有使用的文章就介绍到这了,更多相关Rust 字符串String内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!