30天拿下Rust之字符串

希望睿智 2024-05-24 06:58:24

概述

在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。

let text: &str = "Hello, World";

使用String::new创建空的String

如果我们想要一个可变的、可以增长的字符串,应该使用String类型。在下面的示例代码中,empty_str是一个空的String变量,我们可以向其中添加内容。

fn main() {    let mut empty_str = String::new();    empty_str.push_str("Hello");    println!("{}", empty_str);}

使用字符串字面量初始化String

可以直接将字符串字面量转换为String,这是通过调用to_string方法或to_owned方法来实现的。

fn main() {    let text1 = "Hello, World".to_string();    let str_slice: &str = "Hello, Rust";    let text2 = str_slice.to_owned();    println!("{}", text1);    println!("{}", text2);}

使用format!宏创建String

format!宏是Rust中创建格式化字符串的强大工具,它可以根据提供的格式字符串和参数生成一个 String。

fn main() {    let name: &str = "World";    let info = format!("Hello, {}", name);    println!("{}", info);}

使用String::from创建String

String::from是一个便利的方法,用于从实现了Into<String>特征的任何类型创建String。因为字符串字面量隐式地实现了这个特征,故可以直接使用。

let text = String::from("Hello, World");

字符串的拼接

Rust提供了强大的字符串拼接功能,可以让字符串操作变得更加灵活和高效。

使用+运算符或+=运算符

如果想要将两个String类型进行拼接,可以使用+运算法。

fn main() {    let str1 = String::from("Hello");    let str2 = String::from(" World");    // 不能直接使用str1 + str2    let str = str1 + &str2;    println!("{}", str);    // 编译错误:value borrowed here after move    println!("{}", str1);}

在上面的示例代码中,我们将str1和str2进行了拼接,并得到了str。拼接时,我们使用了&str2,而没有直接使用str2。拼接完成后,str1不再有效。之所以会这样,与使用+运算符时调用的函数签名有关。Rust的+运算符使用了add函数,其签名与下面的函数声明类似。

fn add(self, s: &str) -> String

首先,str2使用了&,意味着我们使用第二个字符串的引用与第一个字符串相加。这是因为add函数只能将&str和String相加,而不能将两个String值相加。在Rust中,可以通过Deref强制转换将&String强转成&str,相当于自动把&str2变成了&str2[..]。其次,add函数直接获取了self的所有权,因为self没有使用&。这意味着,str1的所有权被移动到add函数后,str1将不再有效。

若要对可变的String进行拼接操作,还可以使用+=操作符。但实际上,这并不是简单的连接,而是创建了一个新的String实例,并丢弃了原String分配的内存。

fn main() {    let mut str1 = String::from("Hello");    let str2 = " World";    str1 += str2;    println!("{}", str1);}

注意:使用+=运算符,或者连续使用+运算符进行多次拼接,会导致多次内存分配,效率较低,尤其是在处理大量数据时。如果需要高效地拼接多个字符串,建议使用下面的format!宏。

使用format!宏

format!宏是一种更灵活且高效的字符串拼接方法,尤其适用于包含变量和格式化文本的情况。format!宏可以处理各种复杂的格式化需求,并且它的性能通常优于简单的+拼接。

fn main() {    let name: &str = "World";    let info = format!("Hello, {}", name);    println!("{}", info);}

使用push_str方法或push方法

如果已经有了一个String变量,并且想要将另一个字符串或字符追加到它后面,可以使用push_str方法或push方法。注意:push系列方法不会创建新的String实例,而是直接在原有的String缓冲区上追加内容,这通常比使用+运算符更高效。

fn main() {    let mut text = String::from("Hello ");    text.push_str("Rust");    println!("{}", text);    text.push(' ');    text.push('C');    text.push('S');    text.push('D');    text.push('N');    println!("{}", text);}

字符串的搜索与替换

在Rust中,我们可以使用find、rfind、contains、replace等方法来进行字符串的搜索与替换。在下面的示例代码中,我们首先调用find方法查找子串"World",并返回一个Option类型的值。接下来,我们调用contains方法来检查text字符串是否包含了子串"Hello",若包含,返回true,否则返回false。最后,我们调用replace方法来替换字符串中的子串。

replace方法接收两个参数:第一个参数是要被替换的子串,第二个参数是替换后的新子串。该方法会返回一个新的字符串,其中所有与给定模式匹配的子串都被替换为指定的替换字符串。注意:第一个参数中的原始字符串不会被修改。

fn main() {    let text = "Hello World";    // 搜索子串    let index = text.find("World");    if let Some(value) = index {        println!("found: {}", value);    } else {        println!("not found");    }    // 包含子串    let contain_hello = text.contains("Hello");    println!("contain hello: {}", contain_hello);        // 替换子串    let replaced: String = text.replace("World", "GitHub");    println!("{}", replaced);}

字符串的长度

在Rust中,获取字符串的长度是一个常见的操作。Rust的String类型提供了一个len方法,可以用来获取字符串中字节的数量。需要特别注意的是:这个长度是以字节为单位的,对于ASCII字符串来说,每个字符占用一个字节;但是,对于包含多字节字符(比如:UTF-8编码的Unicode字符)的字符串,len方法返回的是字节的总数,而不是字符的总数。

如果想要获取字符串中Unicode字符的数量,我们应该使用chars方法,然后计算迭代器中元素的数量。chars方法会返回一个迭代器,该迭代器逐个产生字符串中的Unicode字符。

fn main() {    let text = "Hello 霸都";    // 获取字节长度    let byte_len = text.len();    // 输出:12    println!("{}", byte_len);    // 获取字符长度    let char_len = text.chars().count();    // 输出:8    println!("{}", char_len);}

另外,Rust字符串不支持直接通过索引来访问单个字符。这是因为,UTF-8编码格式下,单个字符可能占用1到4个字节,索引操作会带来潜在的非确定性和不一致性问题。如果确实需要通过索引访问字符,可以使用chars()方法。它会返回一个迭代器,产生字符串中的每个Unicode字符。然后,我们可以使用nth方法或者其他集合方法来获取特定位置的字符。

fn main() {    let text = "Hello 霸都";    // 注意:索引从0开始计数    let index = 6;    let cur_char = text.chars().nth(index);    // 输出:index 6: 霸    match cur_char {        Some(c) => println!("index {}: {}", index, c),        None => println!("index out of bounds"),    }}

字符串与字节的转换

Rust中的字符串和字节之间可以方便地进行转换,这在处理二进制数据和编解码时非常有用。

fn main() {    let text = "Hello World";    // 字符串转字节    let bytes = text.as_bytes();    // 输出:[72, 101, 108, 108, 111, 32, 67, 83, 68, 78]    println!("{:?}", bytes);        // 字节转字符串    let bytes2 = [72, 101, 108, 108, 111];    let text2 = std::str::from_utf8(&bytes2).unwrap();    // 输出:Hello    println!("{}", text2);}

总结

由于Rust强调安全性与内存管理,它的字符串设计也体现出了这一点:不可变的&str确保了引用安全,而String则通过所有权系统保证了内存的有效管理,避免了悬垂引用和其他常见的内存错误。

0 阅读:0

希望睿智

简介:软件技术分享,一起学习,一起成长,一起进步