30 天拿下 Rust 之字符串
💡 如果想阅读最新的文章,或者有技术问题需要交流和沟通,可搜索并关注微信公众号“希望睿智”。
概述
在 Rust 中,字符串是一种非常重要的数据类型,用于处理文本数据。Rust 的字符串是以 UTF-8 编码的字节序列,主要有两种类型:&str 和 String。其中,&str 是一个对字符数据的不可变引用,更像是对现有字符串数据的“视图”,而 String 则是一个独立、可变更的字符串实体。
&str 和 String
&str 和 String 是 Rust 中两种主要的字符串类型,它们在以下 6 个方面存在比较明显的区别。
所有权和可变性
&str:是 Rust 核心语言中唯一的字符串类型,它是一个不可变的字符串切片,是对字符串数据的引用,并不拥有数据的所有权。&str 可以安全地使用,但它的内容是不可变的,也就是说,不能改变它指向的字符串的内容。&str 可以指向 String 的内容,也可以指向静态字符串字面量。
String:这是一个在堆上分配的、可变的字符串类型。String 类型由 Rust 标准库提供,而不是编入核心语言。它拥有其内容的所有权,这意味着 String 可以被修改。String 本质上是一个封装了动态大小数组(Vec<u8>)的结构体,该数组存储了 UTF-8 编码的字节。
生命周期
&str:生命周期取决于它的来源。如果是字符串字面量,则生命周期为'static。如果来自某个作用域内的 String 或其他类型,则其生命周期与该作用域相同。
String:没有明确的生命周期限制,只要 String 实例存在,它就可以被使用。
存储位置
&str:可能是指向静态内存中的字符串字面量(&'static str),比如:编译时确定的常量字符串。也可能是指向堆上分配的 String 的一部分,或者任何其他类型的 UTF-8 编码数据的区域。
String:始终在堆上动态分配。
性能
&str:由于它只是一个引用,没有额外的内存分配成本,因此在某些情况下可能更高效。
String:由于它在堆上分配,因此会有额外的内存分配和复制成本,尤其是在字符串拼接时。
使用场景
&str:当只需要读取字符串内容,或者想要避免额外的内存分配时,使用 &str。此外,在函数参数中,使用 &str 可以允许函数接受不同类型的字符串参数,包括:String 和静态字符串字面量。
String:当需要一个可变的字符串,或者不关心字符串的具体来源时,使用 String。
与 C/C++语言的比较
&str:类似于 C 语言中的 const char *,它只是一个指向字符串数据的指针,并不拥有数据。在 Rust 中,&str 比 C 语言中的裸指针更安全,因为它有一个生命周期参数来确保引用的有效性。
String:类似于 C++中的 std::string,是一个字符的容器,并且拥有其内容。
字符串的创建
在 Rust 中,创建字符串有多种方法。根据具体需求,我们可以选择不同的方法。如果需要一个可变的字符串并且打算在程序运行时修改它,那么 String 类型是最佳选择。如果只是需要一个对静态文本的引用,那么 &str 就足够了。
使用字符串字面量创建 &str
字符串字面量是在代码中直接写入的文本,它们被存储在程序的只读数据段中,并且是不可变的。字符串字面量隐式地具有 &str 类型。在下面的示例代码中,text 是一个指向字符串字面量的引用,其类型为 &str。
使用 String::new 创建空的 String
如果我们想要一个可变的、可以增长的字符串,应该使用 String 类型。在下面的示例代码中,empty_str 是一个空的 String 变量,我们可以向其中添加内容。
使用字符串字面量初始化 String
可以直接将字符串字面量转换为 String,这是通过调用 to_string 方法或 to_owned 方法来实现的。
使用 format!宏创建 String
format!宏是 Rust 中创建格式化字符串的强大工具,它可以根据提供的格式字符串和参数生成一个 String。
使用 String::from 创建 String
String::from 是一个便利的方法,用于从实现了 Into<String>特征的任何类型创建 String。因为字符串字面量隐式地实现了这个特征,故可以直接使用。
字符串的拼接
Rust 提供了强大的字符串拼接功能,可以让字符串操作变得更加灵活和高效。
使用+运算符或+=运算符
如果想要将两个 String 类型进行拼接,可以使用+运算法。
在上面的示例代码中,我们将 str1 和 str2 进行了拼接,并得到了 str。拼接时,我们使用了 &str2,而没有直接使用 str2。拼接完成后,str1 不再有效。之所以会这样,与使用+运算符时调用的函数签名有关。Rust 的+运算符使用了 add 函数,其签名与下面的函数声明类似。
首先,str2 使用了 &,意味着我们使用第二个字符串的引用与第一个字符串相加。这是因为 add 函数只能将 &str 和 String 相加,而不能将两个 String 值相加。在 Rust 中,可以通过 Deref 强制转换将 &String 强转成 &str,相当于自动把 &str2 变成了 &str2[..]。其次,add 函数直接获取了 self 的所有权,因为 self 没有使用 &。这意味着,str1 的所有权被移动到 add 函数后,str1 将不再有效。
若要对可变的 String 进行拼接操作,还可以使用+=操作符。但实际上,这并不是简单的连接,而是创建了一个新的 String 实例,并丢弃了原 String 分配的内存。
注意:使用+=运算符,或者连续使用+运算符进行多次拼接,会导致多次内存分配,效率较低,尤其是在处理大量数据时。如果需要高效地拼接多个字符串,建议使用下面的 format!宏。
使用 format!宏
format!宏是一种更灵活且高效的字符串拼接方法,尤其适用于包含变量和格式化文本的情况。format!宏可以处理各种复杂的格式化需求,并且它的性能通常优于简单的+拼接。
使用 push_str 方法或 push 方法
如果已经有了一个 String 变量,并且想要将另一个字符串或字符追加到它后面,可以使用 push_str 方法或 push 方法。注意:push 系列方法不会创建新的 String 实例,而是直接在原有的 String 缓冲区上追加内容,这通常比使用+运算符更高效。
字符串的搜索与替换
在 Rust 中,我们可以使用 find、rfind、contains、replace 等方法来进行字符串的搜索与替换。在下面的示例代码中,我们首先调用 find 方法查找子串"World",并返回一个 Option 类型的值。接下来,我们调用 contains 方法来检查 text 字符串是否包含了子串"Hello",若包含,返回 true,否则返回 false。最后,我们调用 replace 方法来替换字符串中的子串。
replace 方法接收两个参数:第一个参数是要被替换的子串,第二个参数是替换后的新子串。该方法会返回一个新的字符串,其中所有与给定模式匹配的子串都被替换为指定的替换字符串。注意:第一个参数中的原始字符串不会被修改。
字符串的长度
在 Rust 中,获取字符串的长度是一个常见的操作。Rust 的 String 类型提供了一个 len 方法,可以用来获取字符串中字节的数量。需要特别注意的是:这个长度是以字节为单位的,对于 ASCII 字符串来说,每个字符占用一个字节;但是,对于包含多字节字符(比如:UTF-8 编码的 Unicode 字符)的字符串,len 方法返回的是字节的总数,而不是字符的总数。
如果想要获取字符串中 Unicode 字符的数量,我们应该使用 chars 方法,然后计算迭代器中元素的数量。chars 方法会返回一个迭代器,该迭代器逐个产生字符串中的 Unicode 字符。
另外,Rust 字符串不支持直接通过索引来访问单个字符。这是因为,UTF-8 编码格式下,单个字符可能占用 1 到 4 个字节,索引操作会带来潜在的非确定性和不一致性问题。如果确实需要通过索引访问字符,可以使用 chars()方法。它会返回一个迭代器,产生字符串中的每个 Unicode 字符。然后,我们可以使用 nth 方法或者其他集合方法来获取特定位置的字符。
字符串与字节的转换
Rust 中的字符串和字节之间可以方便地进行转换,这在处理二进制数据和编解码时非常有用。
总结
由于 Rust 强调安全性与内存管理,它的字符串设计也体现出了这一点:不可变的 &str 确保了引用安全,而 String 则通过所有权系统保证了内存的有效管理,避免了悬垂引用和其他常见的内存错误。
版权声明: 本文为 InfoQ 作者【希望睿智】的原创文章。
原文链接:【http://xie.infoq.cn/article/aefc8a9ca365163c29dfd0226】。文章转载请联系作者。
评论