理解Rust中字符串的str, &str与String类型的区别

字符串的类型大小和存储位置的概念定义及区分

在学习Rust的过程中,我们经常会遇到Cannot know size until runtime: str这样的报错,而大多数的解释都是搬运Rust官方文档的解释,这是由于str是动态大小类型(Dynamic Sized Type,DST) ,因此编译器无法确定str类型的大小。然而给我们直观的感受是"hello"字面量只有5个字节,为什么在编译期间不能确定其大小呢?

上述问题困扰了我很久,后来发现这是由于我对类型大小值大小这两个概念搞混了。假设字符串字面量的类型是str,那么考虑如下代码:

let hello: str = "hello";
let other: str = "other_hello";
let str_arr = vec![hello, other];

我们会发现hello又5个字节,而other则有11个字节,并且vec中又要求所有成员的大小是一致的,那么str到底采用几个字节?这就解释了为什么str是动态大小类型。

同时还有一个比较容易混淆的点,str是动态大小类型,但是我们经常提到str字符串的长度是固定的,比较奇怪的点在于既然str是动态大小类型,为什么其长度又是固定的呢?这个问题其实和上一个问题一样,我们说str字符串的长度固定,指的是其具体的字符串字面量的是不可变的;而动态大小类型,则指的是类型大小不确定。

由于变量是分配在stack上,而stack上类型的大小必须固定,因此Rust中为了能够使得变量绑定字符串,引入了字符串切片类型&str。需要注意的是&str是一种类型,可以理解为str的引用类型。&str类型由两部分构成:指针和长度,这两者长度都是固定的,因此&str类型可以存储在stack上。

由于字符串字面量不可变,因此Rust引入了可变字符串类型String,其实String与&str很像,而不是str,因为从上述分析可知能存储在栈上的都是固定大小类型,而str类型大小不确定。String类型由三部分构成:指针、长度和容量,相比于&str类型增加了一个容量字段,由于其所有的str长度可变,因此String存储在heap上;同时String还对其指针指向的字符串str拥有所有权,而&str类型对其指向的str没有所有权。

需要注意的是&str类型指向的数据并不一定分配在heap上,它可以在如下存储区域:

  • 静态存储区: 字符串字面量"hello"是&'static str类型,这部分数据直接硬编码到程序编译的二进制文件中
  • Heap分配: 由String类型字符串s的切片生成s[1..]
  • Stack分配: 对于如下分配到栈上的字节数组,可以将其转换为&str类型的字符串:
use std::str;

let x: &[u8] = &[b'a', b'b', b'c'];
let stack_str: &str = str::from_utf8(x).unwrap();

总结下来Rust中字符串的心智模型为:

str.png

评论