zl程序教程

您现在的位置是:首页 >  后端

当前栏目

彻底梳理 Rust 的字符串类型:String、OsString、CString

rust string 字符串 类型 彻底 梳理 CString
2023-09-27 14:20:16 时间

概述、UTF-8编码格式,可动态修改的字符串类型

本模块包含了 String 类型,提供了 ToString 特性用于把数据转换成字符串,还有几种错误类型也可借助 String 来处理。

例如:有多种方式从字符串文本创建新的 String 变量。

let s = "Hello".to_string();
let s = String::from("world");
let s: String = "also this".into();

也可以利用已存在的 String 变量利用 + 号链接创建新的 String 变量。

let s = "Hello".to_string();
let message = s + " world!";

如果有合法的 UTF-8 字节数组,可以用它创建 String 变量。当然,这个过程是可逆的:

let sparkle_heart = vec![240, 159, 146, 150];

// 因为这些字节数据是合法的,所以直接使用 unwrap() 解包。
let sparkle_heart = String::from_utf8(sparkle_heart).unwrap();
assert_eq!("💖", sparkle_heart);

let bytes = sparkle_heart.into_bytes();
assert_eq!(bytes, [240, 159, 146, 150]);

一、String 和 &str

1、String::from 方法

String 是采用 UTF-8 编码,可在使用过程中动态增加长度的字符串。

String 类型是对字符串内容拥有所有权的最常见的字符串类型。它与借用的原语 str 有着密切的关系。例如:可以使用 String::from` 从字符串文字量 str 创建一个 String:

  • [`String::from`]: From::from
let hello = String::from("Hello, world!");

2、String::push、String::pust_str 方法

由于 String 内容是可以动态增长的,我们可以在原来的基础上继续添加字符或字符串:

let mut hello = String::from("Hello, ");
hello.push('w');
hello.push_str("orld!");

3、String::from_utf8 方法

如果我们有一个 UTF-8 的 Vector 数组,我们可以基于它创建 'String' :

// Vector 数组中存入一些字节数据
let sparkle_heart = vec![240, 159, 146, 150];

// 因为这些字节数据是合法的,所以我们直接用 `unwrap()` 把结果解包:
let sparkle_heart = String::from_utf8(sparkle_heart).unwrap();
assert_eq!("💖", sparkle_heart);

4、UTF-8

String 总是 “有效的” UTF-8。如果你需要一个非UTF-8字符串,则考虑使用 OsString 。它是类似的,但是没有uTF-8约束。

另外,不能用索引访问 String。其实道理很简单,因为有些字符的编码可能是多个字节,String[index] 这种形式访问 String 中的字符,不好处理。因此, Rust 不支持用索引访问 String 中的字符。参见下面的例子:

// ```compile_fail,E0277
let s = "hello";
println!("The first letter of s is {}", s[0]); // ERROR!!!

一般来讲,用索引访问数组里的元素,通常消耗的计算时间是常量。但是,UTF-8编码由于每个字符占用的字节不一定相同,所以无法保证按索引读取元素的时间是常量。况且,根据索引返回什么信息也不是很确定。比如可能返回一个字节、一个代码点(codepoint,Unicode 编码标准中的概念)、或者一个字符(grapheme ,Unicode 编码标准中的概念)。bytes 和 chars 方法可以返回前面两个的迭代器。

  • [`bytes`]: str::bytes
  • [`chars`]: str::chars

5、解引用

String 实现了 Deref<Target=str>,因此继承了 str 的所有方法。这意味着,函数中 &str 类型的参数都可以接受 &String 类型的变量。 

fn takes_str(s: &str) { }
let s = String::from("Hello");
takes_str(&s);

这会从 String 类型的变量创建 &str 类型的变量,并传送给函数。因为就是胖指针之间的转换,因此转化效率很高,所以,函数如果接受字符串参数的话,如果没特殊要求,一般会定义成 &str 类型。

在某些情况下,Rust没有足够的信息进行此转换,称为 Deref 强制。在下面的示例中,字符串片段&str 实现了 TraitExample,函数 example_func 接受实现该特性的任何内容。在这种情况下,由于 String 没有实现特性 TraitExample,没办法直接把 String 类型的变量传递给函数 example_func 。因此,下面的示例将无法编译。

//compile_fail,E0277

trait TraitExample {}
impl<'a> TraitExample for &'a str {}

fn example_func<A: TraitExample>(example_arg: A) {}

let example_string = String::from("example_string");
example_func(&example_string);

有两个办法可以解决这个问题。

第一种方法,把函数调用改成 “example_func(example_string.as_str());”,使用 as_str() 可以显式提取包含该字符串的字符串片段。

第二种方法,把函数调用改成 “example_func(&*example_string)”,首先把 String 类型解引用成  [`str`][`&str`] 类型,然后再通过引用 [`str`][`&str`] 得到  [`&str`] 类型。

第二种方法更为惯用,但是这两种方法都是显式地进行转换,而不是依赖于隐式转换。

6、Representation

String 由三部分组成:指向某些字节的指针、长度和容量。指针指向 String 于存储其数据的内部缓冲区。长度是缓冲区中当前存储的字节数,容量是缓冲区的大小(以字节为单位)。因此,长度将始终小于或等于容量。

  • 缓冲区总是保存在堆上。
  • 可以通过 as_ptr、len、capacity 方法访问这三个量。
use std::mem;

let story = String::from("Once upon a time...");

// FIXME Update this when vec_into_raw_parts is stabilized

// 防止自动释放字符串数据
let mut story = mem::ManuallyDrop::new(story);

let ptr = story.as_mut_ptr();
let len = story.len();
let capacity = story.capacity();

// story 有 19 个字节的数据
assert_eq!(19, len);

// 可以用 ptr、 len、 和 capacity 重新构建 String。这个过程是不安全的,
// 我们必须保证自己的代码的可靠性。
let s = unsafe { String::from_raw_parts(ptr, len, capacity) } ;
assert_eq!(String::from("Once upon a time..."), s);
  • [`as_ptr`]: str::as_ptr
  • [`len`]: String::len
  • [`capacity`]: String::capacity

如果 String 拥有足够的容量,添加元素的时候就不用重新分配内存了。例如下面的程序:

let mut s = String::new();
println!("{}", s.capacity());
for _ in 0..5 {
    s.push_str("hello");
    println!("{}", s.capacity());

}

这个程序的输出内容是:

0
5
10
20
20
40

最初,完全没有分配任何内存吗,于是,随着我们向 String 添加内容,其容量会依据一定规则是当地增加。另外,我们也可以用 with_capacity 方法申请一定数量的初始内存,参看下面的程序:

let mut s = String::with_capacity(25);
println!("{}", s.capacity());
for _ in 0..5 {
    s.push_str("hello");
    println!("{}", s.capacity());
}
  • [`with_capacity`]: String::with_capacity

我们最终得到了不同的输出:

25
25
25
25
25
25

这一次,在循环的执行过程中不需要再申请分配更多的内存。

  • [`str`]: prim@str
  • [`&str`]: prim@str
  • [`Deref`]: core::ops::Deref
  • [`as_str()`]: String::as_str

二、CString 和 &CStr

CString 是一种类型,表示一个拥有的、C兼容的、以nul结尾的字符串,中间没有nul字节。

这种数据类型的目的是基于 Rust 的字节切片或 vector 数组生成 C 语言兼容的字符串。这种类型的实例需要确保字节数据中间不包含内部 0 字节(“nul字符”),最后一个字节为0(“nul终止符”)。

 CString 与 &CStr 的关系就像 String 和 &str 的关系一样:CString、String 自身拥有字符串数据,而 &CStr、&str 只是借用数据而已。

1、 创建一个 CString 变量

CString 可以基于字节数组切片或者 vector 字节数组创建,也可以用其他任何实现了  Into<Vec<u8>> 任何事物来创建。例如,可以直接从 String 或 &str 创建 CString,因为二者都实现了这个 trait。

CString::new 方法会检查所提供的 &[u8] 切片内是否有 0 字节,如果发现则返回错误。

2、 输出指向 C 字符串的裸指针

CString 基于 Deref trait 实现了 [as_ptr][CStr::as_ptr] 方法。该方法给出一个 *const c_char 类型的指针,可以把这个指针传递给外部能够处理 nul 结尾的字符串的函数,例如 C 语言的 strdup() 函数。如果 C 语言代码往该指针所知的内存写入数据,将导致无法预测的结果。因为 C 语言所接受的这样的裸指针不包含字符串长度信息。

3、输出 C 字符串的切片

也可以使用 CString::as_bytes 方法从 CString 获取 &[u8] 切片。以这种方式生成的切片不包含尾部 nul 终止符。这在调用一个外部函数时非常有用,该函数接受一个不一定以 nul结尾的 *const u8参数,再加上另一个字符串长度的参数,比如 C 的 strndup()。当然,您可以使用 len 方法获得切片的长度。

如果想得到一个以 nul 结尾的 &[u8] 切片,可以使用 CString::as_bytes_with_nul 方法。

无论获得 nul 结尾的,还是没有 nul 结尾的切片,都可以调用切片的 as_ptr 方法获得只读的裸指针,以便传递给外部函数使用。有关如何确保原始指针生命周期的讨论,请参阅该函数的文档。

4、 例子

fn abs(x: f32) -> f32 {
    if x > 0 {
        x
    } else {
        -x
    }
}

三、OsString 和 &OsStr

OsString 是一种字符串类型,可以表示自有的、可变的平台本机字符串,但可以低代价地与 Rust 字符串相互转换。

这种类型的需求源于以下事实:

  • 在 Unix 系统上,字符串通常是非零字节的任意序列,在许多情况下被解释为UTF-8。
  • 在 Windows 上,字符串通常是非零16位值的任意序列,在有效时解释为UTF-16。
  • 在 Rust 中,字符串总是有效的UTF-8,其中可能包含零。

OsString和[OsStr]通过同时表示Rust和平台本机字符串值,特别是允许将Rust字符串转换为“OS”字符串(如果可能的话),从而弥补了这一差距。这样做的结果是OsString实例不是NUL终止的;为了传递到例如Unix系统调用,您应该创建一个CStr。

OsString 与 &OsStr 的关系,与 String 和 &str 的关系一样:每对中的前一个字符串都是拥有的字符串;后者是借来的引用数据。

注意,OsString 和 [OsStr] 内部不一定以平台固有的形式保存字符串;在 Unix 上,字符串存储为8位值序列,而在 Windows 上,字符串是基于16位值的,正如前面所讨论的,字符串实际上也存储为 8 位值序列,用一种不太严格的 UTF-8 变体编码。这有助于了解处理容量和长度值的时间。

1、创建OsString

  • 从 Rust 字符串创建:OsString 实现 From<String>,因此您可以使用 my_string.From 从普通Rust 字符串创建OsString。
  • From 切片创建:就像您可以从空的 Rust 字符串开始,然后将 String::push_str &str子字符串切片放入其中一样,您可以使用 OsString::new 方法创建一个空的 OsString,然后使用OsString::push 方法将字符串切片推入其中。


2、提取对整个OS字符串的借用引用

您可以使用 OsString::as_os_str 方法从 OsString 获取 &[OsStr];这实际上是对整个字符串的借用引用。

3、转换

有关 OsString 实现从/到本机表示转换的特性的讨论,请参阅模块的顶级转换文档。