char
在老牌语言中,char通常是 1 Byte 的长度,储存一个ASCII字符。但是在rust中,字符是utf-8编码的,而utf-8采用的是变长编码 (长度为 1-4 Byte)。为了内存对齐,rust中最小的char长度应当是可以是2的幂次的,大于等于4的最小长度,也就是4。
字符串的实现
字符串储存了文本的字面量,首先要明确这一点
然后需要理解的是,所有的类型,都是固定长度的,比如i32
就是4个字节。而字符串的长度显然是不定的,是可变的,作为 byte sequence [u8]
被直接储存在可执行文件的 data section 中。
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ H │ e │ l │ l │ o │ │ W │ o │ r │ l │ d │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ 72 │ 101 │ 108 │ 108 │ 111 │ 32 │ 87 │ 111 │ 114 │ 108 │ 100 │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
C 的方法
在 C 中,\0
作为字符串的结束符 append
在字符串的末尾。为了确定字符串的长度,就需要遍历每个 byte。
Rust 的方法
let arr = [1, 2, 3, 4]; // array
// 以下是slice
let slice1 = &arr[..];
let slice2 = &arr[1..3];
let slice3 = &arr[..3];
let slice4 = &arr[1..];
rust中的切片类型就是 &[T]
,表示连续内存序列(数组) [T]
的视图
,这个指针在最底层是通过称为胖指针(FatPtr
)的结构体来模拟的:
// src/libcore/ptr/mod.rs
// Repr<T> 联合体定义了不同种类的指针,用于引用数组(切片)的数据。这个联合体使用了 repr(C) 属性,这表示 Rust 应该按照 C 语言的内存布局规则来布局这个联合体,以便与其他语言进行交互。
#[repr(C)]
pub(crate) union Repr<T> {
pub(crate) rust: *const [T],
rust_mut: *mut [T],
pub(crate) raw: FatPtr<T>,
}
#[repr(C)]
pub(crate) struct FatPtr<T> {
data: *const T,
pub(crate) len: usize,
}
在内存布局(memory layout)上, 切片变量和FatPtr
类型的变量共享同一片内存空间,而FatPtr中则保存了”切片”的必要特征:
- data: 指向若干同质连续数据内存首地址的指针;
- len:
data
指针所指向的连续内存段中存放的元素数目;
而借助于Rust类型系统的优势,标准库在[T]
类型上定义的方法和trait则完全封装了底层负责解释指针含义的工作(这部分解释工作需要依赖unsafe rust来实现)。
str 在本质上是 ‘static str
,对应的,s: &’static str = “foo”
。
str在rust中是很特殊的一个存在,由于utf-8动态编码的性质,如果你想要获得str的第n个字符
&str.chars().nth(n)
浅浅对比 String
pub struct String {
vec: Vec<u8>,
}
rust的String实际上就很直观了,底层就是一个vec,只不过因为是private的,必须要通过暴露的借口进行初始化。
pub const fn new() -> String {
String { vec: Vec::new() }
}
同时,在rust中,str被硬编码在了程序的二进制文件中(static data),而&str正是硬编码后的str在文件中的地址。而string则是被创建在堆上的。