在我们第一次接触Rust时,都会对字符串类型有点困惑。如果你发现自己处于类似的境地,不要担心,因为好消息是:虽然它们看起来很复杂,主要是因为Rust的借用、生命周期和内存管理概念,但一旦你掌握了底层内存布局,一切都非常简单。

有时,当你想要一个字符串时,你可能会发现自己有一个str,或者你可能最终得到了String,但有一个函数需要一个&str。从一个到另一个并不难,但一开始可能看起来很混乱。今天就主要来了解一下。

将底层数据(连续的字符序列)与用于与它们交互的接口分开很重要。Rust中只有一种字符串,但有多种方法可以处理字符串的分配和对该字符串的引用。

String vs str

让我们从澄清一些事情开始:首先,Rust中确实有两种独立的核心字符串类型(Stringstr)。虽然它们在技术上是不同的类型,但它们在很大程度上是一样的。它们都代表任意长度的UTF-8字符序列,存储在内存的连续区域中。Stringstr之间唯一的实际区别是内存是如何管理的。此外,要了解所有核心Rust类型,从内存管理方式的角度来思考它们是有帮助的。因此,这两种Rust字符串类型可以概括为:

  • str:在栈上分配的UTF-8字符串,可以借用但不能移动或修改(注意&str可以指向堆分配的数据;我们稍后会详细讨论这个问题)
  • String:在堆上分配的UTF-8字符串,可以借用和修改

在像C和C++这样的语言中,堆分配数据和栈分配数据之间的区别可能很模糊,因为C指针不会告诉你内存是如何分配的。充其量,它们告诉你有一个特定类型的内存区域,该区域可能是有效的,长度可能在0到N个元素之间。在Rust中,内存分配是明确的;因此,除了元素的数量之外,您的类型本身通常还定义内存的分配方式。

在C中,您可以在堆栈上分配字符串并对其进行修改,但在Rust中不允许在不使用unsafe关键字的情况下这样做。毫不奇怪,这是C中编程错误的主要来源。

让我们用一些C代码来说明:

char *stack_string = "stack-allocated string";
char *heap_string  = strndup("heap-allocated string");

在这段代码中,我们有两种相同的指针类型,指向不同类型的内存。第一种,stack_string 是指向栈分配内存的指针。栈上分配的内存通常由编译器处理,分配基本上是瞬时的。heap_string是相同类型的指针,指向堆分配的字符串。strndup()是一个标准的C库函数,它使用malloc()在堆上分配内存区域,将输入复制到该区域,并返回新分配区域的地址。

如果我们较真的话,我们可以说前面示例中的堆分配字符串最初是在栈上分配的,但在调用strndup()后转换为堆分配的字符串。您可以通过检查编译器生成的二进制文件来证明这一点,该文件将包含二进制文件中的文字堆分配字符串。

现在,就C而言,所有字符串都是一样的:它们只是任意长度的连续内存区域,以空字符(十六进制字节值0x00)结尾。所以如果我们回到对Rust的思考,我们可以认为str等价于第一行的stack_stringString等价于第二行的heap_string。虽然这有点过于简单化,但它是一个很好的模型,可以帮助我们理解Rust中的字符串。

如何选择使用哪种类型

大多数时候,在Rust中工作时,您主要还是使用String&str,永远不会使用str。Rust标准库的不可变字符串函数是针对&str类型实现的,但可变函数仅针对String类型实现。

不能直接创建str,您只能借用对它的一个引用。&str是最常用的,例如用作函数参数时,因为您始终可以借用一个String作为&str

让我们快速讨论一下静态生命周期:在Rust中,'static是一个特殊的生命周期说明符,它定义了一个在进程的整个生命周期内有效的引用(或借用变量)。在一些特殊情况下,您可能需要显式的&'static str,但在实践中,它很少遇到。

决定使用String或静态字符串归结为可变性,如图所示。如果您不需要可变性,静态字符串几乎总是最佳选择。

如何选择String和str

&'static str&str之间唯一真正的区别是,虽然String可以作为&str借用,但String永远不能作为&'static str借用,因为String的生命周期永远不会和进程一样长。当字符串超出范围时,它会以Drop trait释放。

本质上,字符串实际上只是UTF-8字符的Vecstr只是UTF-8字符序列的一个切片,下表总结了您将遇到的核心字符串类型以及如何区分它们。

类型描述底层数据使用场景
str栈上分配的UTF-8字符串切片指向字符序列的指针,以及长度不可变字符串,例如日志记录或调试语句或您可能拥有不可变栈分配字符串的任何其他地方
String堆上分配的UTF-8字符串实际是一个 Vec可变的、可调整大小的字符串,可以根据需要分配和释放
&str不可变字符串借用指向strString的指针,再加上它的长度可以在任何您想不可变地借用strString的地方使用
&'static str不可变静态字符串借用指向str的指针加上它的长度对具有显式静态生命周期的str的引用

strString的另一个区别是String可以移动,而str不能。事实上,不可能拥有str类型的变量,而只能保留对str的引用。为了说明,请考虑以下列表。

fn print_String(s: String) {
    println!("print_String: {}", s);
}
 
fn print_str(s: &str) {
    println!("print_str: {}", s);
}
 
fn main() {
    // let s: str = "impossible str";
    print_String(String::from("String"));
    print_str(&String::from("String"));
    print_str("str");
    // print_String("str");
}

前面的代码在运行时打印以下输出:

print_String: String
print_str: String
print_str: str

以上内容来自Brenden Matthews的书籍《Code Like a Pro in Rust》