Skip to content

\

字符串相关

str

str(发音为 "string slice" 或 "stir")本身是一个动态大小类型 (DST),也称为非固定大小类型。这意味着 str 的大小直到运行时才能确定。因此,我们不能直接在栈上创建 str 类型的变量,也不能直接按值传递或返回 str 类型。

实际上,当我们谈论 str 时,几乎总是指 &str(字符串切片),它是一个对 str 的引用。

由双引号 " 包围的字符串字面量(例如 "hello") 是 Rust 语言中的一个重要概念。 字符串字面量被硬编码进可执行的二进制文件中,存放在程序的只读数据段中,它们是不可修改的,也不能被扩展长度。这种存储方式使得字符串字面量非常高效,因为它们在编译时就已经确定,并且在程序运行期间始终有效。

当我们在 Rust 代码中使用字符串字面量时,编译器会将它们处理为 &'static str 类型。这里的 & 表示它是一个引用,'static 生命周期说明这个引用在整个程序的运行期间都有效,而 str 指明它引用的是一个字符串数据。

这种静态分配的特性与动态分配的 String 类型形成对比。String 类型在运行时于堆上分配内存,因此在创建、修改和销毁时会有一些性能开销。

如果需要通过字符串字面值创建可修改的 String 对象,可以使用 to_string()String::from() 方法:

rust
// &'static str => String
let s_literal: &'static str = "你好,世界";
let s_string1: String = s_literal.to_string();
let s_string2: String = String::from("你好,世界");

String 类型转换回字符串切片 &str 是很常见的,但这通常不会得到一个具有 'static 生命周期的切片,除非该 String 本身就是从一个 'static 源创建且其内容从未改变(这种情况比较特殊且不常见)。通常,从 String 获取的 &str 的生命周期会受到 String 变量本身的生命周期的限制。

rust
// String => &str
let string_obj: String = String::from("hello world");
// str_slice 的生命周期不能超过 string_obj
let str_slice: &str = &string_obj[..];
// 或者更简洁地:
let str_slice_implicit: &str = &string_obj;

在这个例子中,str_slice 借用了 string_obj 的数据。当 string_obj 超出作用域并被销毁时,str_slice 也会变得无效。

&str

&str(字符串切片)是一个指向 UTF-8 编码的字符串数据的只读引用。它是一个“胖指针”,包含两部分信息:

  1. 指向字符串数据第一个字节的指针。
  2. 字符串的长度(以字节为单位)。

&str 不包含容量(capacity)信息,因为它只是一个视图(view)或借用(borrow),并不拥有数据本身。它指向的数据可以存储在不同的地方:

  • 静态内存区(例如字符串字面量)。
  • 堆内存(例如 String 对象内部的数据)。
  • 栈内存(虽然不常见,但技术上可能,例如从栈上的数组创建的切片)。
rust
let s: String = String::from("hello world");

// hello 是 &s[0..5] 的语法糖,表示从索引 0 开始,到索引 5 结束(不包括 5)的切片
let hello: &str = &s[0..5]; // 等同于 &s[..5]
let world: &str = &s[6..11]; // 等同于 &s[6..]

// 也可以直接从字符串字面量创建 &str
let literal_slice: &str = "这是一个字符串字面量";

hello 引用了 s 的一部分。创建切片的语法是 &variable[开始索引..结束索引],这是一个左闭右开的区间。 切片内部存储了指向数据的指针和切片的长度。例如,world 指向 s 中 'w' 的字节,长度为 5

&str&'static str 的区别

主要区别在于生命周期

  • &str:这是一个通用的字符串切片引用,其生命周期由其引用的数据以及 Rust 的借用规则决定。它可以指向任何有效的字符串数据,无论数据存储在哪里。
  • &'static str:这是一个具有 'static 生命周期的字符串切片引用。这意味着它引用的数据在整个程序的执行期间都有效。最常见的例子就是字符串字面量,它们在编译时就嵌入到程序中。

内存位置和使用场景

  • &'static str 引用的数据通常位于程序的只读数据段。这使得它们非常高效,因为不需要在运行时进行分配或管理。
  • &str 引用的数据来源更多样:
    • 如果是从字符串字面量派生,则数据在只读数据段。
    • 如果是从 String 对象切片而来,则数据在堆上。
    • (较少见)如果从栈上数据切片而来,则数据在栈上。

总结: 当你需要一个保证在整个程序运行期间都有效的字符串引用时(例如,程序中定义的常量错误信息),使用 &'static str。 在大多数其他情况下,当你需要一个对字符串数据的只读视图时(例如,作为函数参数接受字符串数据,而不关心其所有权),使用 &str&str 更加灵活,因为它可以引用任何生命周期内的字符串数据。

String

String 是 Rust 标准库提供的一个可增长、可修改、拥有所有权的 UTF-8 编码字符串类型。它的数据存储在上,因此可以在运行时动态地改变其内容和大小。

当你需要一个可以修改的字符串,或者需要在函数之间转移字符串的所有权时,通常会使用 String

rust
fn main() {
    // 创建一个 String
    let mut s_own: String = String::from("你好"); // s_own 是可变的

    // 修改 String
    s_own.push_str(",世界!"); // 追加字符串切片
    s_own.push('!');          // 追加单个字符
    println!("修改后的 String: {}", s_own);

    // 所有权转移
    let s1: String = String::from("Hello");
    let s2: String = s1; // s1 的所有权转移给了 s2
    // println!("{}", s1); // 这行代码会编译错误,因为 s1 不再拥有数据,其值已被移动

    println!("s2 拥有数据: {}", s2);
}

在上面的例子中,let s2 = s1; 演示了 String 的移动语义。当 s1 赋值给 s2 时,s1 所拥有的堆上数据的指针、长度和容量信息被复制给了 s2。之后,s1 不再有效,以防止对同一块内存进行两次释放(double free)。如果需要数据的深拷贝,可以使用 clone() 方法:let s3 = s2.clone();

str vs &str vs String 小结

  • str: 不直接使用的 DST,代表字符串数据本身。
  • &str: 对 str 的不可变引用(借用)。轻量级,用于高效访问字符串数据,但不拥有数据。
  • String: 拥有所有权的、可变的字符串。数据在堆上,可以动态增长。

通常,当 Rust 开发者提到“字符串”时,他们指的是 String&str。这两种类型都保证其内容是有效的 UTF-8 编码。

标准库还提供了其他特定用途的字符串类型,如 OsString/OsStr (用于与操作系统交互,编码可能非 UTF-8) 和 CString/CStr (用于与 C 代码交互,以空字符结尾)。

[T]类型(通用切片)

[T] 类型代表一个元素类型为 T切片 (slice)。它是一个由一系列相同类型的值组成的、长度在运行时确定的序列。切片是对另一块连续内存区域(如数组或 Vec<T> 的一部分)的视图引用

str (可以看作是 [u8] 的一种特殊形式,并带有 UTF-8 保证) 类似,[T] 也是一个 DST。因此,我们通常使用它的引用形式:&[T] (不可变切片) 或 &mut [T] (可变切片)。

&[T]&mut [T] 都是“胖指针”,包含两部分:

  1. 指向切片第一个元素的指针。
  2. 切片的长度(元素数量)。
rust
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let slice_all: &[i32] = &arr[..];      // 引用整个数组
let slice_part: &[i32] = &arr[1..4]; // 引用数组的一部分 [2, 3, 4]

let mut vec_data: Vec<i32> = vec![10, 20, 30, 40];
let slice_from_vec: &[i32] = &vec_data[..];

// 可变切片
let mut mut_slice: &mut [i32] = &mut vec_data[..];
mut_slice[0] = 100; // 修改切片中的元素会影响原始 Vec
// mut_slice.push(50); // 错误!切片的长度是固定的,不能通过切片引用来增加或减少元素数量。
                     // 长度的改变需要通过拥有数据的结构(如 Vec)来进行。

关键特性

  • 不拥有数据:切片只是借用数据。当切片超出作用域时,它引用的数据不会被释放。数据的生命周期由其所有者(如数组或 Vec)管理。
  • 固定长度:一旦创建,一个切片实例的长度就固定了。你不能通过切片引用来改变其指向的数据序列的长度。要改变长度,你需要操作拥有数据的原始集合(例如 Vec::pushVec::pop),然后可能需要创建一个新的切片来反映这些变化。
  • 高效访问:切片提供了对连续内存区域的直接、高效的访问。

[T] 类型在 Rust 中非常普遍,广泛用于函数参数(允许函数处理不同长度的数组或 Vec 的部分数据而无需知道其具体类型)、数据处理等场景。

DST (Dynamically Sized Types - 动态大小类型)

在 Rust 中,DST 指的是那些大小在编译时无法确定的类型。这意味着编译器不能在编译期间为这些类型分配固定大小的内存空间。

常见的 DST 例子包括:

  • str: 字符串本身,其长度可变。
  • [T]: 通用切片,其元素数量可变。
  • Trait 对象 (例如 dyn MyTrait): 指向实现了某个 trait 的具体类型的实例,而这些具体类型的大小可能不同。

由于 DST 的大小不固定,它们有一些使用限制:

  1. 不能直接在栈上创建 DST 类型的变量。
  2. 函数不能直接按值返回 DST。
  3. 结构体或枚举的最后一个字段可以是 DST,但这样的结构体本身也变成了 DST。

如何使用 DST?

DST 几乎总是通过某种形式的指针或引用来使用。这些指针通常是“胖指针”,除了指向数据的地址外,还携带额外的信息(元数据)来确定 DST 的大小或行为。

  • &str: 胖指针,包含 (数据指针, 长度)。
  • &[T]: 胖指针,包含 (数据指针, 长度)。
  • Box<dyn MyTrait>: 胖指针,包含 (数据指针, 指向虚函数表的指针 vtable)。
  • &dyn MyTrait: 胖指针,包含 (数据指针, vtable 指针)。
rust
fn main() {
    let s1_literal: &'static str = "hello"; // s1_literal 是一个 &str (胖指针)
    let s2_string: String = String::from("world");

    // print_str 函数接受一个 &str 类型的参数
    // &str 是对 str (DST) 的引用
    print_str(s1_literal);    // 传递 &'static str
    print_str(&s2_string);  // 传递 &String,会自动解引用成 &str
}

// s 参数是一个 &str,它是一个胖指针,包含指向实际字符数据的指针和字符串的长度
fn print_str(s: &str) {
    // 在这里,s 的大小(即指针大小 + 长度大小)是已知的
    // 但 s 所指向的 str 数据的大小是在运行时通过 s 的长度元数据确定的
    println!("{}", s);
}

// 这是一个无法编译的例子,因为 str 是 DST
// fn cant_do_this(s: str) { /* ... */ }

Rust 提供了 Sized trait。默认情况下,所有泛型参数都有一个隐式的 Sized 约束,意味着它们必须是大小在编译时已知的类型。如果想让泛型函数接受 DST,可以使用 ?Sized 来放宽这个约束 (例如 fn generic_fn<T: ?Sized>(param: &T) )。

理解 DST 和它们如何通过引用(特别是胖指针)来操作,是掌握 Rust 内存管理和类型系统的关键部分。

Released under the MIT License