我必须立刻押注 Rust 之八🎲:深入理解字符串

134 阅读4分钟

本篇是 我必须立刻押注 Rust 的第八章。

通过前面的篇章,我们对数据结构有了多多少少的接触。

实战中,字符串是我们最常用数据类型之一。

因此,我们有必要深入了解一下 Rust 中的字符串特性与操作。

字符串类型

Rust 中的字符串类型主要有两种:

String:动态可增长的字符串,通常在堆上分配,用于存储和操作可变文本数据。

字符串切片 &str:不可变的字符串视图,通常指向已有的 String 或字符串字面量,通常在栈上或静态区域分配。

&str 与 &String

&str 是一个不可变的字符串切片,通常指向已有的 String 或字符串字面量,通常在栈上或静态区域分配。

let s = String::from("Hello, world!");
let s1 = &s[0..5]; // &str
let s2 = "Hello, world!"; // &str

&String 是一个指向 String 的不可变引用,它可以自动解引用为 &str 类型。

let s = String::from("Hello, world!");
let s1 = &s; // &String 类型
let s2: &str = s1; // 自动转化为 &str 类型

s1 是 &String 类型。Rust 看到你想把它赋值给 s2(类型是 &str),它会通过 Deref trait 进行解引用。

Rust 会自动调用 s1.deref(),即访问 s1 内部的 &str 数据。

let s2:&str = &s; 
// 等价于
let s2 = s.deref();

&String 类型在大多数情况下是不常见的,因为 Rust 自动进行解引用,使得 &String 在需要 &str 的地方能够直接转换为 &str。

因此直接使用 &String 作为类型通常没必要,通常 &str 更为常见。

自动解引用(Dereferencing)机制实现

#[stable(feature = "rust1", since = "1.0.0")]
impl ops::Deref for String {
    type Target = str;

    #[inline]
    fn deref(&self) -> &str {
        unsafe { str::from_utf8_unchecked(&self.vec) }
    }
}

String 操作

创建String::new() 创建空字符串,或者使用 String::from("文本")

添加内容push_str 可以添加字符串切片,push 可以添加字符。

连接+ 运算符或 format! 宏。

索引和长度len() 获取字节长度(注意,非字符长度),可以用 chars() 方法遍历每个字符。

let mut s = String::from("Hello");

s.push_str(", world");  // 添加字符串切片
s.push('!');  // 添加字符
println!("{}", s);  // 输出: Hello, world!
println!("len: {}", s.len());  // 输出: len: 13

+ 运算符

运算符用于将一个字符串与另一个字符串连接。

要求左操作数必须是 String 类型,右操作数是 &str,也可以是可自动解引用为 &str 的类型,例如 &String

+ 运算符会将左侧 String 的所有权转移给右操作数。因此,原来的变量将无法再使用。可参考 所有权规则

let s1 = String::from("Hello, ");

let num = 42;
let num_string: &String = &num.to_string();

let s3 = s1 + num_string;
 // print!("s1:{}", s1);  borrow of moved value: `s1`
println!("{}", s3);  // 输出: Hello, 42

format!

format! 宏用于通过格式化字符串创建新的 String,它可以接受多个参数,并且不会获取参数的所有权。

let mut s = String::from("Hello");

s.push_str(", world");  // 添加字符串切片
s.push('!');  // 添加字符
println!("{}", s);  // 输出: Hello, world!

let f1 = format!("s: {}", s);// => "s: Hello, world!"
let f2 = format!("test"); // => "test"
let f3 = format!("x = {}, y = {val}", 10, val = 30);  // => "x = 10, y = 30"
let (x, y) = (1, 2);
let f4 =  format!("{x} + {y} = 3");  // => "1 + 2 = 3"
println!("f1: {}; f2: {}; f3: {}; f4: {};",  f1, f2, f3, f4);

格式化系统

类似 println!format!, 可以接受各种类型的参数, 通过 {}{:?} 等占位符来格式化输出。

他们背后调用了 std::fmt::write 函数来格式化参数。它根据传入的参数类型来决定如何进行格式化。

  • 如果类型实现了 Display 特征,则使用 {} 格式进行格式化, 如 String、i32 等

  • 如果类型实现了 Debug 特征,则使用 {:?} 格式进行格式化, 如 Vec、HashMap 等

实现 Display 特征

use std::fmt;

struct Point {
    x: i32,
    y: i32,
}

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // 使用 `write!` 宏将格式化的内容写入 `f`(Formatter)
        write!(f, "({}, {})", self.x, self.y) 
    }
}

fn main() {
    let p = Point { x: 10, y: 20 };
    println!("p: {}", p);  // 输出: (10, 20)
}

实现 Debug 特征

use std::fmt;

struct Point {
    x: i32,
    y: i32,
}

impl fmt::Debug for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // {{ 和 }} 是转义字符,用来表示字面量的大括号 { 和 },因为 write! 或 format! 中大括号有特殊意义,所以我们需要通过双大括号 {{ 和 }} 来表示字面量的大括号。
        write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y)
    }
}

fn main() {
    let p = Point { x: 10, y: 20 };
    println!("p: {:?}", p);  // 输出: Point { x: 10, y: 20 }
}

  • String 是动态可增长的字符串,通常在堆上分配,用于存储和操作可变文本数据。

  • &str&String 更常见,它是不可变的字符串切片,通常指向已有的 String 或字符串字面量,通常在栈上或静态区域分配。

  • + 运算符用于将一个字符串与另一个字符串连接,语法是 String + &str。但在复杂拼接中,format! 宏更适用。

  • 格式化输出中 {} 用于 Display 特征,{:?} 用于 Debug 特征。实现 Display 特征通常用来进行标准的格式化输出,而 Debug 用于调试输出,尤其是当结构体等复杂类型打印时。