Rust 学习笔记(16)-String

参考章节《Rust 程序设计语言》第8.2章 使用字符串储存 UTF-8 编码的文本

在集合章节中讨论字符串的原因是,字符串就是作为字节的集合外加一些方法实现的

在之前的章节我门曾了解过字符串 slice:它们是一些储存在别处的 UTF-8 编码字符串数据的引用。

Rust 的核心语言中只有一种字符串类型:str,它通常以被借用的形式出现,&str。

String是由标准库提供的,而没有写进核心语言部分,它是可增长的可变的有所有权的UTF-8 编码的字符串类型。
你可以把它理解为Java中的字符串包装类,它提供了一系列方便的功能,下面我门就来看看它的大致用法

  • 字符串创建的三种方式
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fn main() {
    // 第一种方式,通过关联函数 new() 创建一个空字符串
    let s1 = String::new();
    println!("{}", s1);

    // 第二种方式,通过 to_string() 方法创建一个字符串,它能用于任何实现了 Display trait 的类型,字符串字面值也实现了它。
    let data = "s2 Helllo World";
    let s2 = data.to_string();
    println!("{}", s2);
    let s3 = "s3 Hello World".to_string(); // 字符串字面量 to_string()
    println!("{}", s3);

    // 第三种方式,可以使用 String::from 函数来从字符串字面值创建 String,它等同于使用 to_string。
    // String::from 和 .to_string 最终做了完全相同的工作,所以如何选择就是风格问题了。
    let s4 = String::from("s4 Hello World");
    println!("{}", s4);
}
  • 字符串的更新
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn main() {
    // 可以通过 push_str 方法来附加字符串 slice
    let mut s5 = String::from("foo");
    let s6 = "bar";
    s5.push_str(s6);
    println!("s6 is {}", s6); // 我们注意到s6依然可用,原因是 push_str 方法采用字符串 slice,因为我们并不需要获取参数的所有权。

    // push 方法被定义为获取一个单独的字符作为参数,并附加到 String 中。
    s5.push('l');
    println!("{}", s5);
}
  • 拼接字符串
1
2
3
4
5
6
7
fn main() {
    // 使用 + 号运算符来拼接字符串
    let s7 = String::from("Hello, ");
    let s8 = String::from("world!");
    let s9 = s7 + &s8; 
    println!("{}", s7); // 注意 s7 被移动了,不能继续使用
}

这里要说明一下,s7 在相加后不再有效的原因,使用 + 运算符时调用的函数签名有关。+ 运算符使用了 add 函数,这个函数签名看起来像这样:

1
fn add(self, s: &str) -> String {}

可以发现签名中 add 获取了 self 的所有权,因为 self 没有 使用 &。这意味着上面示例中的 s7 的所有权将被移动到 add 调用中,之后就不再有效。
具体原理可以参考书中使用 + 运算符或 format! 宏拼接字符串这一章节

对于更为复杂的字符串拼接,可以使用 format!

1
2
3
4
5
6
7
8
fn main() {
    // 更好的办法是使用 format! 宏:
    let s10 = String::from("tic");
    let s11 = String::from("tac");
    let s12 = String::from("toe");
    let s13 = format!("{}-{}-{}", s10, s11, s12);
    println!("{}", s13);
}

注意 format! 生成的代码使用索引并且不会获取任何参数的所有权。

  • 字符串索引

在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。
然而在 Rust 中,如果你尝试使用索引语法访问 String 的一部分,会出现一个错误。下面的示例会报错
Rust 不支持这么做,至于为什么不支持,可以参考内部表现这一章节

1
2
3
4
fn main() {
    let s14 = String::from("hello");
    let h = s14[0];
}

那么应该怎么做呢?

如果你真的希望使用索引创建字符串 slice 时,Rust 会要求你更明确一些。
为了更明确索引并表明你需要一个字符串 slice,相比使用 [] 和单个值的索引,可以使用 [] 和一个 range 来创建含特定字节的字符串 slice

1
2
3
4
5
6
7
8
9
fn main() { 
    let hello = "Здравствуйте"; // 这些字母都是两个字节长度
    let s = &hello[0..4]; // 如果获取 &hello[0..1] 会发生什么呢?答案是:Rust 在运行时会 panic,就跟访问 vector 中的无效索引时一样
    println!("{}", s); // Зд 

    let hello2 = "Hello World";
    let s2 = &hello2[0..1]; // 正常的英文字母占1个字节的,因此这里获取 &hello[0..1] 不会有问题
    println!("{}", s2); // H
}

你应该小心谨慎的使用这个操作,因为这么做可能会使你的程序崩溃。

  • 遍历字符串

操作字符串每一部分的最好的方法是明确表示需要字符还是字节。对于单独的 Unicode 标量值使用 chars 方法。

1
2
3
4
5
fn main() {
    for c in "नमस्ते".chars() {
        println!("{}", c);
    }
}

这将返回 Unicode 标量值

1
2
3
4
5
6

另外 bytes 方法返回每一个原始字节,这可能会适合你的使用场景

1
2
3
for b in "नमस्ते".bytes() {
    println!("{}", b);
}

这些代码会打印出组成这个 String18 个字节

1
2
3
4
5
224
164
// --snip--
165
135
  • 什么是字节、标量值和字形簇?
1
2
3
  : [224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135]  
标量值: ['न', 'म', 'स', '्', 'त', 'े']  
字型簇: ["न", "म", "स्", "ते"]  

从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能。crates.io 上有些提供这样功能的 crate

updatedupdated2024-10-012024-10-01