Skip to content

字符串相关

str

str 是一个字符串类型的原始定义,是 Rust 语言级别类型。由双引号 " 包围的固定长度不可修改的字符串序列。

字符串字面量被硬编码进可执行的二进制文件中,存放在程序的只读数据段中,不能被修改,也不能被扩展长度,它们被放在独立于栈和堆的空间中(可以理解为字符串常量区)。 这使得字符串字面量快速且高效,这主要得益于字符串字面量的不可变性。

当我们在 Rust 代码中使用字符串字面量时,编译器会将它们转换为&str类型的字符串切片(即,对字符串字面量的引用),且隐式具有'static生命周期,即&'static str类型。 这是因为字符串字面量在 Rust 中是静态分配的,也就是说,它们的值在编译时已经确定

相比之下,动态分配的字符串类型 String 在运行时进行内存分配和释放,因此在性能上会有一些开销。

如果需要通过字符串字面值创建String对象,可以使用to_string()方法来将&str类型转换为String类型:

rust
// &'static str => String
let s1: &'static str = "hello world";
let s2: String = s1.to_string();

由于 Rust 中的 String 类型可能不会在整个程序的生命周期中存在,因此不可能将String类型转换为静态字符串。但是,您可以创建一个静态的字符串切片,如下面的示例所示:

rust
// String => &str
let string: String = "hello world".to_owned();
let str_slice: &str = &string[..];

&str

&str是一个指向已经存在的字符串的只读引用指针和长度信息的组合,长度由底层字符串数据决定,因此没有附加的容量(cap)信息。

rust
let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

hello没有引用整个 String s,而是引用s的一部分内容,通过[0..5]的方式来指定。

这就是创建切片的语法,使用方括号包括的一个序列:[开始索引.. 终止索引],其中开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置,也就是这是一个右半开区间。

在切片数据结构内部会保存开始的位置和切片的长度,其中长度是通过终止索引 - 开始索引的方式计算得来的。

对于let world = &s[6..11];来说,world是一个切片,该切片的指针指向s的第7个字节 (索引从0开始),且该切片的长度是5个字节。

&str&'static str

在 Rust 中,&str&'static str 都是字符串类型的引用。它们都表示一段 UTF-8 编码的字符串数据的引用,但是两者的生命周期是不同的。

  • &str 是一个动态分配的字符串引用,它的生命周期是由其引用的数据所拥有的所有权和借用关系所决定的,通常在运行时创建并销毁,使用场景非常广泛。
  • &'static str 则表示一个指向存储在程序静态内存中(例如编译时就已经存在)的字符串字面值的引用,它的生命周期是整个程序执行期间,即与程序的生命周期相同。因此,&'static str 只能用于访问程序编译时已知的字符串常量或者外部库提供的静态字符串数据。

需要注意的是,&str&'static str 除了生命周期不同之外,在内存结构和使用方式上也存在一些区别:

  • &str 类型的字符串数据通常存储在堆上,需要通过指针来访问,该指针指向一个包含字符串长度和实际字符串数据的内存区域。
  • &'static str 类型的字符串数据则通常存储在程序的只读数据段中,其实现方式类似于 C 语言中的字符串常量。 这种实现方式可以使得 Rust 程序在运行时不需要为其分配和管理内存,因此具有较好的性能和安全性。

综上所述,&str&'static str 两种字符串引用类型各自适用于不同的场景。 在通常情况下,我们应该优先选择使用 &str 类型来表示动态分配的字符串数据,而只在需要访问程序编译时已知的静态字符串常量时才使用 &'static str 类型。

String

String是 Rust 标准库提供的一个可增长、可改变且具有所有权的 UTF-8 编码字符串,存储在堆上,并且可以动态地分配和释放内存。

rust
fn main() {
    let s = String::from("Hello");
    //   let b = &mut a;
    let a = s;
    println!("{a:?}");
}

s依然可以读取,但是若不加mut,赋值后,值就不能改了。

因此,使用 str 类型通常是比较少见的。大多数情况下我们会使用 &strString 类型来处理字符串数据,具体取决于我们的需求和场景。 通常,如果我们需要处理不可变的静态字符串,比如文本常量或者字面量,就可以使用 &str 类型;如果我们需要处理可变的动态字符串,则可以使用 String 类型。

当 Rust 用户提到字符串时,往往指的就是String类型和&str字符串切片类型,这两个类型都是 UTF-8 编码。

需要注意的是,由于 String 类型是可变的,因此它比 &str 类型更加灵活和方便。但同时也需要注意,由于 String 类型是动态分配内存的,所以使用时需要特别关注内存的分配和释放,避免出现性能问题。

除了 String 类型的字符串,Rust 的标准库还提供了其他类型的字符串,例如OsStringOsStrCsStringCsStr等,注意到这些名字都以 String 或者 Str 结尾了吗? 它们分别对应的是具有所有权和被借用的变量。

[T]类型

[T] 类型表示一个元素类型为 T 的切片(slice),它是由一系列类型相同的值组成的动态长度序列。这个区间可以是一个数组的一部分,也可以是一个动态分配的缓冲区。 切片类型的大小是运行时确定的,并且可以通过指针访问任意内存位置的一段连续内存区域作为数据来源。因此,它比数组更具有实用价值。

和其他切片类型一样,[T] 类型实际上是一个包含有关引用和长度的元数据结构。它们在编译时被推导出来:

  • 指针(Pointer):指向首元素的指针,这个指针是可变的。
  • 长度(Length):切片包含的元素数量。

在 Rust 中,切片类型是不可变的,它们引用底层数组的一部分连续元素,并具有固定长度。因此,切片容纳元素的数量与其长度(即 len() 方法返回值)相同。

当然,如果底层数组还有额外空间,我们可以使用特殊的类型 &mut [T] 更新切片中已经存在的元素或者添加新元素以增加切片大小。

如果您使用的是动态可变向量类型 Vec<T>,当其内部的缓冲区空间占满时,Vec 会自动将其内存扩展到指定大小(例如两倍于当前大小),并将所有元素复制到新的内存中。 这样,您就可以在扩展后的 Vec 中容纳更多的元素。扩展 Vec 内存的过程称为“重新分配”。

由于切片只是一小段内存的引用,因此它不属于被分配给切片的内存区域。当切片离开作用域时,不需要释放任何内存,以免出现两次释放同一块内存导致程序错误。

这个类型在 Rust 中非常常见,它广泛应用于各种场景,例如:

  • 处理文件和网络数据时,可以使用 [u8] 类型的切片来读写二进制数据。
  • 在某些算法实现中,可以使用 [T] 类型的切片来表示输入和输出数据。
  • 在实现自定义集合类型时,可以使用 [T] 类型的切片来存储元素。

总而言之,切片类型在 Rust 中使用非常广泛,我们可以看到几乎所有基于集合的 API 都提供了用于处理切片数据的方法和函数。 该类型的动态长度特性使得它能够更加灵活地处理变长数据结构,也为快速访问大型数据集合提供了可靠而高效的方法。

DST

在 Rust 中,DSTs (Dynamically Sized Types) 指的是类型大小在运行时才能确定的类型,这种类型通常是通过指针、切片和 trait 对象来实现的。

一个例子是 Rust 中的 str 类型。在 Rust 中,str 表示一段 UTF-8 编码的文本,由于每个字符的大小都不同,因此该类型的大小只能在运行时确定。 在内存中,str 类型实际上是一个指向字符串内存的指针和一个长度字段的元组 (ptr: *const u8, len: usize)

另一个例子是切片类型 [T]。在 Rust 中,切片表示对一个数组或向量的引用,由于数组或向量的长度在编译时未知,因此切片的大小只能在运行时确定。 在内存中,切片类型实际上是一个指向数组或向量内存的指针和一个长度字段的元组 (ptr: *const T, len: usize)

这些类型的大小只能在运行时确定,因此需要特殊的语法和机制来处理它们,例如使用裸指针和特殊的 trait。 Rust 还提供了 Sized trait 来限制泛型参数必须是静态大小类型,从而保证编译时可以确定类型的大小。

我们可以使用一个例子来说明 Rust 中动态大小类型的使用:

rust
fn main() {
    let s1 = "hello"; // 字符串字面量
    let s2 = String::from("world"); // 动态分配的字符串

    print_str(s1);
    print_str(&s2);
}

fn print_str(s: &str) {
    println!("{}", s);
}

在上面的代码中,我们定义了两个不同类型的字符串 s1s2。其中,s1 是一个字符串字面量,它的大小在编译时已经确定,因此其类型是 &'static str; 而 s2 则是一个动态分配的字符串,其大小只能在运行时确定,因此其类型是 String

接着,我们定义了一个函数 print_str,该函数接收一个参数 s,类型为 &str,即字符串切片类型。在调用该函数时,我们分别将 s1s2 作为参数传递进去。 由于 s1&s2 都是字符串切片类型,因此它们可以作为函数参数进行传递,而无需考虑其类型大小的问题。

需要注意的是,在函数 print_str 内部,我们可以通过 s.len() 方法获取字符串切片的长度,但是不能直接对其进行索引访问, 例如 s[0] 是无法通过编译的,因为编译器无法确定字符串切片的大小。

数组和字符串

在 Rust 中,数组和字符串都是一组相同类型的值的集合。它们之间的区别在于,数组是定长的,而字符串通常是不定长的。

具体来说,在 Rust 中,数组长度是固定的,在定义数组时必须指定其元素数量,如[i32; 5]表示包含5i32类型元素的数组,a[0]a[4]将是数组的每个元素。 一旦定义了一个数组,就不能直接改变其长度或大小,否则会导致编译错误。

如果操作的是数组切片([T]),则这个整数索引必须是非负整数并小于该数组的长度。而对于字符串切片(&str)和字节数组([u8])而言,由于它们是字节序列, 因此索引应该是非负整数且小于该序列的字节长度。

需要注意的是,由于 UTF-8 字符集中的每个字符都可能占用不同数量的字节,因此在使用字符串切片(&str)时,如果不注意边界问题,可能会导致出现意料之外的行为。 在处理字符串时,我们通常建议先确定用于字符串处理的数据类型,并选择相应的数据结构和方法,以确保程序的正确性和安全性。

在 Rust 中,数组的每个元素都占据相同的内存空间。这意味着如果一个数组元素类型是字符串(String&str[u8] 等), 则每个元素都将持有一个指向实际字符串数据的指针。

由于 Rust 的字符串类型代表 Unicode 字符序列,因此字符串的长度可能不同,而这些长度不一致的字符串都需要被存储为单独的、不同大小的内存块。 换句话说,虽然具有相同类型的数组元素的内存占用是相等的,但它们可以持有不同长度和内容的字符串数据。当同时打算存储长度不同的字符串时,可以使用动态分配类型 Vec<String> 或者使用存储 &str 类型的切片类型 [&str] 等数据结构。

当使用字符串类型作为数组元素类型时,在内存中的存储方式类似于 String 内存结构。 每个数组元素都将包含一个指向实际字符串数据的指针,而这些指针所指向的字符串数据则可能具有不同的长度和内容。

需要注意的是,如果数组元素类型是&str或者[u8]等其他类型的字符串类型,则指针所指向的字符串数据可能处于不可变状态,这样就可以避免所有者和引用问题。 为了防止悬垂指针问题,应该在程序中保证每个指针只引用其指向的字符串数据有效期间内的存储位置。

&str类型的生命周期

对于 &str 类型来说,它是一个静态字符串切片,具有 'static 生命周期的,意味着它的生命周期会持续整个程序的运行期间。 因为字符串字面量通常作为静态常量存储在程序的只读数据段中,且无法被修改

对于 struct 类型来说,默认情况下它并不具备 'static 生命周期,而是依赖于它的成员变量的生命周期。 如果一个结构体的所有成员变量都具有 'static 生命周期,那么这个结构体也可以被认为是具有 'static 生命周期的。

&str 类型表示字符串切片,它引用了程序的静态数据区中的字符串字面量。因为这些字符串字面量的生命周期在整个程序的执行期间都是有效的,所以 &str 类型也拥有类似的生命周期。 例如,以下代码定义了一个具有 'static 生命周期的字符串字面量并将其作为参数传递给一个具有 'static 约束的函数:

rust
fn print_static(s: &'static str) {
    println!("{}", s);
}

fn main() {
    let s: &'static str = "hello";
    print_static(s);
}

因此,使用字符串字面量作为参数传递时,通常可以省略'static生命周期参数,

struct 类型的生命周期取决于其字段的生命周期和所有权关系。如果结构体的所有字段都具有 'static 生命周期,则该结构体也具有 'static 生命周期。 否则,它的生命周期将根据字段之间的生命周期关系和借用规则来确定。 然而,如果您希望确保结构体具有 'static 生命期,可以通过在结构体定义中添加 lifetime 'static 约束来实现:

rust
struct Person<'a> where 'a: 'static {
    name: &'a str,
    age: u32,
    // ...
}

fn main() {
    let p: Person<'static> = Person {
        name: "Alice",
        age: 30,
        // ...
    };
    // ...
}

在这个示例中,我们定义了一个具有 'static 生命周期的结构体 Person。 为了确保该结构体生命周期为 'static,我们将其泛型生命周期参数 'a 添加到结构体定义中,并使用 where 语句来强制指定 'a: 'static 约束条件。 这确保了在 Person 结构体的实例化中,所有引用类型成员都必须是静态的。

需要注意的是,虽然默认情况下 &strstruct 类型都是具有 'static 生命周期的,但如果它们依赖于其他引用类型或具有更短的生命周期,则可能不满足 'static 约束条件

因此,在编写代码时,请始终考虑到生命周期和所有权关系,以确保程序的内存安全。

具有静态生命周期的类型

除了 &str 和一些具有 'static 生命周期约束的自定义数据类型之外,还有一些其他类型也是具有静态生命周期的。

以下是一些常见的具有 'static 生命周期的类型:

  • 字符串字面量(例如:"hello")
  • 整型、浮点型和字符型字面量(例如:42, 3.14, 'a'
  • 函数字面量(例如:fn()
  • 静态变量(用 static 关键字定义)

例如,下面的代码演示了如何使用具有静态生命周期的字符串字面量、函数和静态变量:

rust
static COUNT: i32 = 0;

fn count() {
    println!("Count: {}", COUNT);
}

fn main() {
    let s: &'static str = "hello";
    println!("{}", s);

    count();
}

在这个例子中,我们定义了一个静态变量 COUNT,该变量是一个整数字面量,并且其生命周期与整个程序的生命周期一样长。我们还定义了一个具有 'static 生命周期的函数 count(),该函数输出静态变量 COUNT 的值。最后,我们使用字符串字面量 s 调用 println! 宏,该字符串也具有 'static 生命周期。

需要注意的是,在 Rust 中,具有静态生命周期的引用具有编译时确定的生命周期,而不是运行时确定的。因此,使用静态生命周期可以在编译时捕获内存错误,从而提高代码的可靠性和安全性。

UTF-8 编码 与 Unicode 字符

Unicode 源于一个很简单的想法:将全世界所有的字符包含在一个集合里,计算机只要支持这一个字符集,就能显示所有的字符,再也不会有乱码了。

Unicode 标量值是一个表示字符的抽象概念,用于统一地标识字符。 它是 Unicode 字符的唯一整数表示形式,范围最初的U+0000U+D7FF,包含了最常见的字符;以及后续U+E000U+10FFFF,包含了更多特殊字符和辅助字符。 两个范围的字符一起构成了 Unicode 编码的完整范围(共 1,114,111 个码点)。

因为 Unicode 字符集中包含了许多特殊字符和表情符号等复杂字符,有些字符可能由多个标量值组合而成。 在 UTF-8 编码中,Unicode 码点范围从U+0000U+007F的字符使用1个字节(编码长度)表示,而码点范围从U+0080U+07FF的字符使用2个字节表示。 对于多字节的字符,UTF-8 使用一些特殊的字节模式来表示不同字节的位置。

Unicode 码位是用来表示单个字符的唯一标识符。 每个字符都有一个对应的码位,由十六进制数字表示。 例如,U+1F469表示码位位于十进制值128105的字符。

实际代码中,可以通过 \u{} 的转义序列表示具体的字符。

对于 "👩‍👩‍👧‍👦" 这个表情符号,实际上包含了四个字符的码位:

  • U+1F469: 👩 代表一个名为 "WOMAN" 的女性符号。
  • U+200D: 零宽度连接符(Zero-width joiner (⨝))不是一个独立的码位,它是一种 Unicode 字符编排标记,用于指示两个字符之间应该进行零宽度连接。
  • U+1F469: 👩 代表一个名为 "WOMAN" 的女性符号。
  • U+200D: 零宽度连接符(Zero Width Joiner)。
  • U+1F467: 👧 代表一个名为 "GIRL" 的女孩符号。
  • U+200D: 零宽度连接符(Zero Width Joiner)。
  • U+1F466: 👦 代表一个名为 "BOY" 的男孩符号。
rust
fn main() {
    let emoji = "👩‍👩‍👧‍👦";
    let u = '\u{1F469}';    // unicode 字符
    let c = emoji.chars().count();
    let l = emoji.len();
    println!("{} {} {}", u, c, l)
}
  • 零宽连字符在 UTF-8 编码中占据了 3 个字节。
  • 其他每个字符都占据了 4 个字节。

在 Rust 中,对于包含 Unicode 字符的字符串,str::chars方法以及Iterator::count方法计算的是 Unicode 标量值的数量,并不是字符的数量。 由于 Rust 中的字符串处理是以字节数为单位的,它会将每个字节都计算到字符串的长度中。同时,UTF-8 编码中的一些字符需要使用多字节来表示。

上面的示例中:3 个零宽连字符,4 个字符,所以输出:👩 7 25

字符操作示例

下面我们来试着计算字符 'З' 的字节长度如下所示:

rust
fn main() {
  let c = 'З';
  let c_ = '\u{0417}';
  let b = c.to_string();
  let c_bytes = b.as_bytes();
  let c_bytes_length = c_bytes.len();
  let hex_value = c as u32;
  let scalar_value = format!("{:04X}", hex_value);
  println!("'{}' 即字符 '{}' 的 Unicode 标量值:U+{};十进制:{};字节长度为:{}; 字节序列为:{:?}。", c_, c, scalar_value, hex_value, c_bytes_length, c_bytes);
}

这段代码首先将字符'З'转换为一个包含单个字符的字符串,并使用as_bytes()方法将其转换为对应的 UTF-8 字节序列。 然后,通过调用len()方法获取字节序列的长度,即可得到字符'З'的字节长度2。 通过将 c 转换为u32类型,我们可以获取到这个字符的十进制 Unicode 标量值, 然后通过format!宏和{:04X}格式指令将这个十进制值转换为带有前导零的四位十六进制表示(正确显示 Unicode 标量值的表示形式),并将其存储在变量scalar_value中。

因此,在 Rust 中,虽然 char 类型本身占用4个字节,但字符的实际字节长度取决于该字符在 UTF-8 编码中的表示形式。

请注意,在 Rust 中,默认使用 UTF-8 编码来表示字符串。 UTF-8 是一种变长编码方式,使用14个字节来编码不同范围的 Unicode 字符。

对于大部分的 Unicode 字符,包括常见的字符和字母(例如 ASCII 字符),UTF-8 编码使用13个字节来表示。 而对于一些罕见的或辅助平面字符(例如包括许多汉字在内的大部分非 ASCII 字符,UTF-8 编码可能需要4个字节来表示。

在 Rust 中,默认使用 UTF-8 编码来表示字符串。当您使用字符字面量'З'时,它会被解析为一个 Unicode 标量值,而不是实际的字节序列。 在 UTF-8 编码中,Unicode 字符"З"(Unicode 标量值U+0417)需要占用2个字节表示,其字节序列为[208, 151]

Released under the MIT License