在 Rust 中,String&str (字符串切片) 是处理文本的两个基本类型。初学者常常对何时使用它们感到困惑,而这种困惑的根源在于它们 截然不同的内部实现和内存模型。作为 Rust 专家,我们必须清晰地认识到:String 是所有者,而 &str 是借用者。这种差异体现在它们的数据结构、内存位置和核心职责上。

1. String:一个“拥有所有权”的堆分配缓冲区

String 是 Rust 标准库提供的、保证为有效 UTF-8 的、可增长的、拥有所有权的字符串类型。

核心解读:
String 在栈上(或作为另一个数据结构的一部分)存储的是一个控制结构(structuct),而它管理的数据则 始终位于堆(Heap)上

这个控制结构(在内部实现上类似于 `Vec<u8>核心组件,在 64 位系统上,这总共占 24 字节:

  1. ptr(指针): 一个指向堆上内存块的原始指针 (*mut u8),这里实际存储着 UTF-8 编码的字节数据。

  2. len(长度): 一个 usize,表示当前已使用的字节数(注意:是字节数,不是字符数)。

  3. capacity(容量): 一个 usize,表示在堆上总共分配了多少字节的内存。

专业思考(capacity 的意义):
capacityString 性能优化的关键。len <= capacity 永远为真。当你向 Stringpush 字符时:

  • 如果 len < capacityString 只需将新数据写入堆上未使用的空间,并增加 len。这是一个 O(1) 操作。

  • 如果 len == capacityString 会在堆上 重新分配 一个更大的内存块(通常是当前容量的两倍),将旧数据复制过去,释放旧内存,然后才添加新数据。这是一个昂贵的操作(`O(n`)。

String 的设计目标是 数据修改所有权管理。当你需要构建、修改字符串,或者从函数返回一个新生成的字符串时,String 是不二之选。


2. &str:一个“借用”的、任意位置的视图

&str(字符串切片)是一个 不可变借用。它不拥有任何数据,它只是一个指向某处有效 UTF-8 字节序列的“视图”。

核心解读:
&str 的内部实现是一个 **指针”(Fat Pointer)**。它由两个 usize 大小的组件构成(在 64 位系统上占 16 字节):

  1. ptr(指针): 一个指向数据起始字节的原始指针 (*const u8)。

  2. len(长度): 一个 usize,表示该切片所包含的字节数

**&str 没有capacity`**!因为它只是一个视图,它不关心原始数据有多少容量,它只关心这个视图本身有多长。

**内存布局示意:


3. 实践深度:&str 的数据源在何处?

&str 的强大之处在于其 ptr 可以指向任何内存位置。这正是“实践深度”所在:

实践 A:&str 指向 String 的堆内存

这是最常见的场景。

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

// &s 会创建一个 &str
// 这个 &str 的 {ptr, len} 是 {s.ptr, s.len}
let slice: &str = &s; 

// slice.ptr 指向 s 在堆上的数据
// slice.len == 5

深度思考(Deref Coercion):
当你将 &String 传递给一个需要 &str 的函数时,Rust 的 Deref 强制转换 会自动生效。String 实现了 Deref<Target=str>。编译器会自动将 &String 转换为 &str,这个转换在编译时完成,零成本,它本质上就是创建了一个如上所述的胖指针。

实践 B:&str 指向静态数据区

这是 &str 最神奇的用法之一:字符串字面量。

// "world" 被硬编码到程序的二进制文件(静态数据区)
// 在程序启动时加载到只读内存中
let static_slice: &'static str = "world";

// static_slice.ptr 指向内存的静态数据区
// static_slice.len == 5
// 'static 生命周期意味着它在整个程序运行期间都有效

专业思考:
这就是为什么函数应该优先接受 &str 作为参数

// 糟糕的设计:
// fn print_string(s: String) { ... }
// 调用者如果只有 &str,必须执行一次昂贵的堆分配:
// print_string("hello".to_string());

// 优秀的设计:
fn print_str(s: &str) {
    println!("{}", s);
}

// 它可以接受任何来源的字符串视图,零分配:
let s1: String = String::from("I am owned");
let s2: &'static str = "I am static";

print_str(&s1);       // 实践 A:从 String 创建 &str (Deref Coercion)
print_str(s2);        // 实践 B:直接传递 &str
print_str("Literal"); // 实践 B:直接传递 &str

实践 C:&str 指向栈(罕见但可能)

虽然字符串字面量通常是静态的,但 &str 也可以指向栈上的数据(如果这些数据是有效的 UTF-8)。

use std::str;

// 栈上的一个字节数组
let stack_array: [u8; 4] = [0xE4, 0xBD, 0xA0, 0x好]; // "你好"

// 我们可以从栈上的字节创建 &str
// (注意:这里有生命周期限制,slice 不能活得比 stack_array 久)
if let Ok(slice_from_stack) = str::from_utf8(&stack_array) {
    // slice_from_stack.ptr 指向 stack_array 在栈上的地址
    // slice_from_stack.len == 4
    print_str(slice_from_stack); 
}

4. 结论:String vs &str 的核心权衡

特性 String (拥有者) &str (借用者 / 视图)
内部实现 结构体 { ptr, `len, capacity } 胖指针 { ptr, len }
内存位置 ptr 指向 ptr 指向 任何地方 (堆, 栈, 静态区)
所有权 拥有 数据 借用 数据
可变性 可变 (Growable) 不可变 (Fixed-size view)
主要 构建、修改、拥有数据 读取、传递、查看数据
成本 创建/增长时有堆分配/重分配 创建零成本(只是复制两个 usize
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐