rust 笔记 4:彻底搞清楚各种字符串

1. String, &strstr

想要搞清楚这三者的差别,要从 str 开始说起。

1.1. str&str

str 是字符串切片(string slice),是对部分连续的 UTF8 字符序列的引用。

所有的切片类型,例如 str[u8][i32] ,都是 Unsized 的(不定长类型,或称动态大小类型,Dynamicly Sized Types, DST),它们占用的内存空间的具体大小在编译期是不可知的。

我们知道,变量都是存放在栈上的,而栈上是不允许存在一个大小不可知的数据的,因此如果我们想创建某个类型的变量,那么这个类型必须满足 Sized 约束,即定长类型。而对于这些 Unsized 类型,我们使用的时候就必须包裹一个 Sized 的间接层,比如引用 & 或者 Box

拿我们常见的 &str&[u8] 来举例:

  • &str 是对字符串切片的引用,其占用的内存空间是固定的,包含两个成员:一个是指针,指向连续的 u8 数据(存放着 UTF-8 字符),另一个是 usize 类型,表示字节的数量。

  • 其它切片引用,例如 &[T],占用的内存空间也是固定的,同样包含两个成员:一个指向连续数据的指针,和一个表示元素数量的计数。

同理,特征对象 dyn Trait 本身也是不定长类型,也要加上引用或者 Box 才能操作。

读到这里,你可能会有疑惑:绝大多数切片都是 [] 包起来的形式,但为什么要为字符串切片单独设计个 str 呢?[char] 又是什么?先不急,我们后面会讲。

从代码角度来看,&str 可能长这样(注,只是说明,可能并非真实代码):

struct &str {
    data: *const u8,
    len: usize,
}

1.2. String

String 是一个可变的、堆上分配的 UTF-8 的 u8 缓冲区。这意味着 String 拥有一块堆上分配的内存的所有权,它用这块空间存放数据,并可以调整大小。

在标准库中我们可以看到 String 的原型:

pub struct String {
    vec: Vec<u8>,
}

1.3. &strString 的区别

在前面两个小节中,我们分别对这三种类型做出了定义:str 本质是字符串切片,&str 是字符串切片的引用,String 是对数据拥有所有权的堆上字节缓冲区。从这些定义出发,我们可以发现 &strString 有如下几项重要的区别:

  • 所有权String 拥有数据的所有权;而 &str 只是切片,并不拥有所有权

  • 空间拓展性:显然,切片作为(部分)引用,不能扩展其指向的数据空间;而 String 用有这块堆上数据的所有权,因此在需要的时候可以进行扩容。

  • 底层数据存放位置String 的字符串数据一定存放在堆上;而 &str 由于是切片,只是一个引用,其数据存放的位置要视借用对象而定。可以分成如下几种情况:

    • 引用的对象是字面量(String literal),则数据存放在 .data 段中;
    • 引用的对象是数组,则数据存放在栈上;
    • 引用的对象是 String ,则数据存放在堆上。
    • 特别地,如果数组过于巨大,放在栈上会导致爆栈,此时 rust 编译器会转而将其存放到堆上,&str 的数据存放位置也在堆上了。

1.4. 另一个区别:可修改性

由于 String 拥有数据的所有权,因此只要 String 变量是 mut 的,就可以随意对数据进行增删改,例如:

let mut s = String::from("Hello ");
s.push_str("rust");

但是,字符串切片,即使被定义成 &mut str 的形式,也只能经由 unsafe 的方法来修改数据。换言之,在 safe rust 中,&str 是无论如何都不能修改数据的。这一点与其他类型的切片大相径庭。

// u8 类型切片,可以修改引用的数据
let mut arr: [u8; 5] = [1, 2, 3, 4, 5];
let reference = &mut arr[1..2];
reference[0] = 1;
println!("{arr:?}"); // 打印:[1, 1, 3, 4, 5]

// 字符串切片,只能通过 unsafe 方法转为 &mut [u8] 来修改
let mut my_string = String::from("Hello, world!");
let my_str_slice: &mut str = &mut my_string[..];
let bytes: &mut [u8] = unsafe { my_str_slice.as_bytes_mut() };
bytes[7..12].copy_from_slice(b"Rust!");
println!("{}", my_string); // 打印:Hello, Rust!!

因此,&str 在 safe rust 中只能作为一种只读切片,尽管其他的切片类型都可以被定义成可写的。

1.5. &mut str 在 safe rust 中不能修改指向数据的原因

存在这个限制的主要原因是 &str (还有 String)强制采用 UTF-8 编码。UTF-8 编码中,一个字符的长度是不固定的,可能是 1~4 字节,视其第一个字节的前几位而定:

  • 第一个 bit 是 0,则是一个单字节字符,即 ASCII
  • 前三个 bit 是 110,则是个二字节字符
  • 前四个 bit 是 1110,则是个三字节字符
  • 前五个 bit 是 11110,则是个四字节字符

如果我们想通过 &mut str 修改指向的数据,那么可能会出现需要修改数据长度的情况,例如把某个单字节字符改成了多字节字符。然而,切片作为一种引用,并不具有数据的所有权,因而无权对数据的长度进行变动。所以,这种操作在 safe rust 中是不可行的。rust 设计者们的想法是,提供一个 unsafe 方法,让调用者自己保证修改后的结果依然是正确的 UTF-8 编码。

2. char

char 是 rust 中的另一种字符类型。前面我们提到, &strString 都要求是 UTF-8 编码,但 char 是 unicode 编码,其每个字符长度是固定的 4 字节。

此外,char 本身就是一种基本类型,而 &strString 本质上都是基于另一种基本类型—— u8 的。

由于以上这两点,char&str String 之间存在着显著的区别。当然,我们也能比较方便地在二者之间转换。

到底使用哪一方,主要还是视我们业务场景中需要使用到的编码类型来考虑。如果需要 unicode 编码,则我们需要使用 Vec<char>&[char] ;如果 UTF-8 可以满足需求,那么就可以使用 String&str ,从而节省内存空间。

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇