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

在学习 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 上。

需要注意的是&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中字符串的内存模型为下图这种:

rust_str_memn.png

在 rust 中,类型大小和类型数据是分开的,换个说法就是 相同的类型,不同的数据,他们的类型大小是相同的,举个栗子:let a: i32 = 1; let b: i32 = 2; a 和 b 的类型大小都是 4 字节,和具体保存的数据没有关系

在这种 let s: &str = "hello"; 代码中,往往会先入为主的认为 s 的类型大小是固定的,其实是错的,因该是 s 引用所指向的数据是固定不变的了,所以 s 指向的具体数据的大小是 5,注意区分 这里说的是数据大小的, 而实际项目中,没有办法创建出 str 类型的变量,当然不严谨的说 str 这个类型名称看起来就是给 字符串类型 用的,那字符串肯定不可能是固定大小的,所以 str 是动态大小类型,变量要在 stack 中保存,在 stack 中申请内存的前提条件是必须要知道类型大小,所以要声明一个 str 类型的变量,只能通过 &str 引用的方式来声明,因为引用是一个胖指针,类型大小是固定的