序
本篇是 我必须立刻押注 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 用于调试输出,尤其是当结构体等复杂类型打印时。