概述
在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则通过所有权系统保证了内存的有效管理,避免了悬垂引用和其他常见的内存错误。
💡 如果想阅读最新的文章,或者有技术问题需要交流和沟通,可搜索并关注微信公众号“希望睿智”。