文章目录

  • 字符串
  • String 和 &str 转换
  • 字符串索引
  • 字符串内部
  • 字符串的不同表现形式
  • 字符串切片
  • 操作字符串
  • 追加
  • 插入
  • 替换
  • 删除
  • 连接
  • 使用 + 或者 += 连接字符串**
  • 使用 format! 连接字符串
  • 字符串转义
  • 操作 UTF-8 字符串
  • 字符串剖析
  • 总结


字符串

字符串是由字符组成的连续集合,Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4),这样有助于大幅降低字符串所占用的内存空间。

Rust 在语言级别,只有一种字符串类型: str,它通常是以引用类型出现 &str,也就是上文提到的字符串切片。虽然语言级别只有上述的 str 类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是 String 类型。

let s ="hello"

s 是被硬编码进程序里的字符串值(类型为 &str )。字符串字面值是很方便的,但是它并不适用于所有场景。

  • 字符串字面值是不可变的,因为被硬编码到程序代码中
  • 并非所有字符串的值都能在编写代码时得知

例如,字符串是需要程序运行时,通过用户动态输入然后存储在内存中的,这种情况,字符串字面值就完全无用武之地。 为此,Rust 为我们提供动态字符串类型: String, 该类型被分配到堆上,因此可以动态伸缩,也就能存储在编译时大小未知的文本。

可以使用下面的方法基于字符串字面量来创建 String 类型:

let s = String::from("hello");

调用 String 中的 from 方法,因为 String 存储在堆上是动态的,你可以这样修改它:

let mut s = String::from("hello");
s.push_str(", world!"); // push_str() 在字符串后追加字面值
println!("{}", s); // 将打印 `hello, world!`

String 和 &str 转换

将 &str 转换成 String

  • String::from(“hello,world”)
  • “hello,world”.to_string()

将 String 转为 &str
let s = String::from(“hello world”)

  • &s
  • &s[…]
  • s.as_str()

字符串索引

与其他语言不同, 使用索引的方式访问字符串的某个字符或子串是会报错的 String cannot be indexed by {integer}

字符串内部

像开头所讲,字符串的底层数据存储格式是 [u8],字节数组。对于 hello 来说,长度是 5 个字节,因为 hello 每个字母都在 UTF-8 中仅占用一个字节。
但是对于其他 如汉字 中国人 就是 9个字节,因为汉字在 UTF-8 占用三个字节。
这种情况用索引可能会返回奇怪的值。

字符串的不同表现形式

Rust 提供了不同的字符串展现方式,这样程序可以挑选自己想要的方式去使用,而无需去管字符串从人类语言角度看长什么样。

还有一个原因导致了 Rust 不允许去索引字符串:因为索引操作,我们总是期望它的性能表现是 O(1),然而对于 String 类型来说,无法保证这一点,因为 Rust 可能需要从 0 开始去遍历字符串来定位合法的字符。

字符串切片

上节已介绍,需要注意,要保证切片的索引落在字符的边界上。

操作字符串

由于 String 是可变字符串,下面介绍 Rust 字符串的修改,添加,删除等常用方法:

追加

在字符串尾部可以使用 push() 方法追加字符 char,也可以使用 push_str() 方法追加字符串字面量。这两个方法都是在原有的字符串上追加并不会返回新的字符串。由于字符串追加操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut 关键字修饰
示例

fn main() {
    let mut s = String::from("Hello ");
    s.push('r');
    println!("追加字符 push() -> {}", s);

    s.push_str("ust!");
    println!("追加字符串 push_str() -> {}", s);
}

插入

可以使用 insert() 方法插入单个字符 char,也可以使用 insert_str() 方法插入字符串字面量,与 push() 方法不同,这俩方法需要传入两个参数,第一个参数是字符(串)插入位置的索引,第二个参数是要插入的字符(串),索引从 0 开始计数,如果越界则会发生错误。由于字符串插入操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut 关键字修饰

示例

fn main() {
    let mut s = String::from("Hello rust!");
    s.insert(5, ',');
    println!("插入字符 insert() -> {}", s);
    s.insert_str(6, " I like");
    println!("插入字符串 insert_str() -> {}", s);
}

替换

如果想要把字符串中的某个字符串替换成其它的字符串,那可以使用 replace() 方法。与替换有关的方法有三个。

  1. replace
    该方法可适用于 String 和 &str 类型。replace() 方法接收两个参数,第一个参数是要被替换的字符串,第二个参数是新的字符串。该方法会替换所有匹配到的字符串。该方法是返回一个新的字符串,而不是操作原来的字符串
    示例:
fn main() {
    let string_replace = String::from("I like rust. Learning rust is my favorite!");
    let new_string_replace = string_replace.replace("rust", "RUST");
    dbg!(new_string_replace);
}
  1. replacen
    该方法可适用于 String 和 &str 类型。replacen() 方法接收三个参数,前两个参数与 replace() 方法一样,第三个参数则表示替换的个数。该方法是返回一个新的字符串,而不是操作原来的字符串
    示例:
fn main() {
    let string_replace = "I like rust. Learning rust is my favorite!";
    let new_string_replacen = string_replace.replacen("rust", "RUST", 1);
    dbg!(new_string_replacen);
}
  1. replace_range
    该方法仅适用于 String 类型。replace_range 接收两个参数,第一个参数是要替换字符串的范围(Range),第二个参数是新的字符串。该方法是直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut 关键字修饰
    示例:
fn main() {
    let mut string_replace_range = String::from("I like rust!");
    string_replace_range.replace_range(7..8, "R");
    dbg!(string_replace_range);
}

删除

与字符串删除相关的方法有 4 个,他们分别是 pop(),remove(),truncate(),clear()。这四个方法仅适用于 String 类型。

  1. pop —— 删除并返回字符串的最后一个字符
    该方法是直接操作原来的字符串。但是存在返回值,其返回值是一个 Option 类型,如果字符串为空,则返回 None。 示例代码如下:
fn main() {
    let mut string_pop = String::from("rust pop 中文!");
    let p1 = string_pop.pop();
    let p2 = string_pop.pop();
    dbg!(p1);
    dbg!(p2);
    dbg!(string_pop);
}
  1. remove —— 删除并返回字符串中指定位置的字符
    该方法是直接操作原来的字符串。但是存在返回值,其返回值是删除位置的字符串,只接收一个参数,表示该字符起始索引位置。remove() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。
    示例代码如下:
fn main() {
    let mut string_remove = String::from("测试remove方法");
    println!(
        "string_remove 占 {} 个字节",
        std::mem::size_of_val(string_remove.as_str())
    );
    // 删除第一个汉字
    string_remove.remove(0);
    // 下面代码会发生错误
    // string_remove.remove(1);
    // 直接删除第二个汉字
    // string_remove.remove(3);
    dbg!(string_remove);
}
  1. truncate —— 删除字符串中从指定位置开始到结尾的全部字符
    该方法是直接操作原来的字符串。无返回值。该方法 truncate() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。
    示例代码如下:
fn main() {
    let mut string_truncate = String::from("测试truncate");
    string_truncate.truncate(3);
    dbg!(string_truncate);
}

4、clear —— 清空字符串

该方法是直接操作原来的字符串。调用后,删除字符串中的所有字符,相当于 truncate() 方法参数为 0 的时候。
示例代码如下:

fn main() {
    let mut string_clear = String::from("string clear");
    string_clear.clear();
    dbg!(string_clear);
}

连接

使用 + 或者 += 连接字符串**

使用 + 或者 += 连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型。其实当调用 + 的操作符时,相当于调用了 std::string 标准库中的 add() 方法,这里 add() 方法的第二个参数是一个引用的类型。因此我们在使用 +, 必须传递切片引用类型。不能直接传递 String 类型。+ 和 += 都是返回一个新的字符串。所以变量声明可以不需要 mut 关键字修饰。

示例代码如下:

fn main() {
    let string_append = String::from("hello ");
    let string_rust = String::from("rust");
    // &string_rust会自动解引用为&str
    let result = string_append + &string_rust;
    let mut result = result + "!";
    result += "!!!";

    println!("连接字符串 + -> {}", result);
}
使用 format! 连接字符串

format! 这种方式适用于 String 和 &str 。format! 的用法与 print! 的用法类似,详见格式化输出。

示例代码如下:

fn main() {
    let s1 = "hello";
    let s2 = String::from("rust");
    let s = format!("{} {}!", s1, s2);
    println!("{}", s);
}

字符串转义

我们可以通过转义的方式 \ 输出 ASCII 和 Unicode 字符。

fn main() {
    // 通过 \ + 字符的十六进制表示,转义输出一个字符,\\保持字符串原样;\3F -> ?
    let byte_escape = "I'm writing \x52\x75\x73\x74!";
    println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape);

    // \u 可以输出一个 unicode 字符
    let unicode_codepoint = "\u{211D}";
    let character_name = "\"DOUBLE-STRUCK CAPITAL R\"";

    println!(
        "Unicode character {} (U+211D) is called {}",
        unicode_codepoint, character_name
    );

    // 换行了也会保持之前的字符串格式
    let long_string = "String literals
                        can span multiple lines.
                        The linebreak and indentation here ->\
                        <- can be escaped too!";
    println!("{}", long_string);
}

操作 UTF-8 字符串

前文提到了几种使用 UTF-8 字符串的方式,下面来一一说明。
字符
如果你想要以 Unicode 字符的方式遍历字符串,最好的办法是使用 chars 方法,例如:

for c in "中国人".chars() {
    println!("{}", c);
}

字节
这种方式是返回字符串的底层字节数组表现形式:

for b in "中国人".bytes() {
    println!("{}", b);
}

字符串内有变长字符的情况下,想要取出某一个子串,标准库无法做到。utf8_slice

字符串剖析

为啥 String 可变,而字符串字面值 str 却不可以?

就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串字面值的不可变性。

对于 String 类型,为了支持一个可变、可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容,这些都是在程序运行时完成的:

  • 首先向操作系统请求内存来存放 String 对象
  • 在使用完成后,将内存释放,归还给操作系统

其中第一部分由 String::from 完成,它创建了一个全新的 String。

Rust 不使用 GC,变量在离开作用域后,就自动释放其占用的内存。

总结

  1. 字符串切片索引要注意,必须落在字符边界
  2. push,insert,replace_range, pop,remove,truncate,clear 修改原有字符串,即字符串必须是 mut
  3. replace,replacen,catenate, format!返回新的字符串,不用 mut。