🎃个人专栏:
🐬 算法设计与分析:算法设计与分析_IT闫的博客-CSDN博客
🐳Java基础:Java基础_IT闫的博客-CSDN博客
🐋c语言:c语言_IT闫的博客-CSDN博客
🐟MySQL:数据结构_IT闫的博客-CSDN博客
🐠数据结构:数据结构_IT闫的博客-CSDN博客
💎C++:C++_IT闫的博客-CSDN博客
🥽C51单片机:C51单片机(STC89C516)_IT闫的博客-CSDN博客
💻基于HTML5的网页设计及应用:基于HTML5的网页设计及应用_IT闫的博客-CSDN博客
🥏python:python_IT闫的博客-CSDN博客
🐠离散数学:离散数学_IT闫的博客-CSDN博客
🥽Linux:Linux_Y小夜的博客-CSDN博客
🚝Rust:Rust_Y小夜的博客-CSDN博客
欢迎收看,希望对大家有用!
🎯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 字符的错误。
版权归原作者 Y小夜 所有, 如有侵权,请联系我们删除。