String

std::string::String 是 UTF-8 编码、可增长的动态字符串. 它也是我们日常开发中最常用的字符串类型,同时对于它所拥有的内容拥有所有权。

基本操作

  1. 🌟🌟
2. // 填空并修复错误
3. // 1. 不要使用 `to_string()`
4. // 2. 不要添加/删除任何代码行
5. fn main() {
6. let mut s: String = "hello, ";
7. s.push_str("world".to_string());
8. s.push(__);

10. move_ownership(s);

12. assert_eq!(s, "hello, world!");

14. println!("Success!")
15. }

17. fn move_ownership(s: String) {
18. println!("ownership of \"{}\" is moved here!", s)
19. }

String and &str

虽然 String 的底层是 Vec<u8> 也就是字节数组的形式存储的,但是它是基于 UTF-8 编码的字符序列。String 分配在堆上、可增长且不是以 null 结尾。

而 &str 是切片引用类型( &[u8] ),指向一个合法的 UTF-8 字符序列,总之,&str 和 String 的关系类似于 &[T] 和 Vec<T> 。

如果大家想了解更多,可以看看易混淆概念解析 - &str 和 String。

  1. 🌟🌟
1. // 填空
2. fn main() {
3. let mut s = String::from("hello, world");

5. let slice1: &str = __; // 使用两种方法
6. assert_eq!(slice1, "hello, world");

8. let slice2 = __;
9. assert_eq!(slice2, "hello");

11. let slice3: __ = __;
12. slice3.push('!');
13. assert_eq!(slice3, "hello, world!");

15. println!("Success!")
16. }
  1. 🌟🌟
2. // 问题:  我们的代码中发生了多少次堆内存分配?
3. // 你的回答:
4. fn main() {
5. // 基于 `&str` 类型创建一个 String,
6. // 字符串字面量的类型是 `&str`
7. let s: String = String::from("hello, world!");

9. // 创建一个切片引用指向 String `s`
10. let slice: &str = &s;

12. // 基于刚创建的切片来创建一个 String
13. let s: String = slice.to_string();

15. assert_eq!(s, "hello, world!");

17. println!("Success!")
18. }

UTF-8 & 索引

由于 String 都是 UTF-8 编码的,这会带来几个影响:

  • 如果你需要的是非 UTF-8 字符串,可以考虑 OsString
  • 无法通过索引的方式访问一个 String

具体请看字符串索引。

  1. 🌟🌟🌟 我们无法通过索引的方式访问字符串中的某个字符,但是可以通过切片的方式来获取字符串的某一部分 &s1[start..end]
2. // 填空并修复错误
3. fn main() {
4. let s = String::from("hello, 世界");
5. let slice1 = s[0]; //提示: `h` 在 UTF-8 编码中只占用 1 个字节
6. assert_eq!(slice1, "h");

8. let slice2 = &s[3..5];// 提示: `世` 在 UTF-8 编码中占用 3 个字节
9. assert_eq!(slice2, "世");

11. // 迭代 s 中的所有字符
12. for (i, c) in s.__ {
13. if i == 7 {
14. assert_eq!(c, '世')
15. }
16. }

18. println!("Success!")
19. }

utf8_slice

我们可以使用 utf8_slice 来按照字符的自然索引方式对 UTF-8 字符串进行切片访问,与之前的切片方式相比,它索引的是字符,而之前的方式索引的是字节.

示例

1. use utf8_slice;
2. fn main() {
3. let s = "The 🚀 goes to the 🌑!";

5. let rocket = utf8_slice::slice(s, 4, 5);
6. // Will equal "🚀"
7. }
  1. 🌟🌟🌟

提示: 也许你需要使用 from_utf8 方法

2. // 填空
3. fn main() {
4. let mut s = String::new();
5. __;

7. let v = vec![104, 101, 108, 108, 111];

9. // 将字节数组转换成 String
10. let s1 = __;

12. assert_eq!(s, s1);

14. println!("Success!")
15. }

内部表示

事实上 String 是一个智能指针,它作为一个结构体存储在栈上,然后指向存储在堆上的字符串底层数据。

存储在栈上的智能指针结构体由三部分组成:一个指针只指向堆上的字节数组,已使用的长度以及已分配的容量 capacity (已使用的长度小于等于已分配的容量,当容量不够时,会重新分配内存空间)。

  1. 🌟🌟 如果 String 的当前容量足够,那么添加字符将不会导致新的内存分配
2. // 修改下面的代码以打印如下内容:
3. // 25
4. // 25
5. // 25
6. // 循环中不会发生任何内存分配
7. fn main() {
8. let mut s = String::new();

10. println!("{}", s.capacity());

12. for _ in 0..2 {
13. s.push_str("hello");
14. println!("{}", s.capacity());
15. }

17. println!("Success!")
18. }
  1. 🌟🌟🌟
2. // 填空
3. use std::mem;

5. fn main() {
6. let story = String::from("Rust By Practice");

8. // 阻止 String 的数据被自动 drop
9. let mut story = mem::ManuallyDrop::new(story);

11. let ptr = story.__();
12. let len = story.__();
13. let capacity = story.__();

15. assert_eq!(16, len);

17. // 我们可以基于 ptr 指针、长度和容量来重新构建 String.
18. // 这种操作必须标记为 unsafe,因为我们需要自己来确保这里的操作是安全的
19. let s = unsafe { String::from_raw_parts(ptr, len, capacity) };

21. assert_eq!(*story, s);

23. println!("Success!")
24. }

常用方法(TODO)

关于 String 的常用方法练习,可以查看这里.

你可以在这里找到答案(在 solutions 路径下)