rust笔记-字符串
字符串类型与切片
切片并不是 Rust 独有的概念,在 Go 语言中就非常流行,它允许你引用集合中部分连续的元素序列,而不是引用整个集合。
我们来看个例子:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
println!("{}, {}", hello, world);
}
这里我们创建了一个字符串变量 s,然后通过 s[0..5] 的方式截取了字符串的前 5 个字符,也就是 “hello”,接着通过 s[6..11] 的方式截取了字符串的后 5 个字符,也就是“world”。这就是使用切片的方式截取字符串的方法,切片是字符串的一部分,这就是创建切片的语法,使用方括号包括的一个序列:[开始索引..终止索引],其中开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置,也就是这是一个 右半开区间。在切片数据结构内部会保存开始的位置和切片的长度,其中长度是通过 终止索引 - 开始索引 的方式计算得来的。
在对字符串使用切片语法时需要格外小心,切片的索引必须落在字符之间的边界位置,也就是 UTF-8 字符的边界,例如中文在 UTF-8 中占用三个字节,下面的代码就会崩溃:
let s = "中国人";
let a = &s[0..2];
println!("{}",a);
因为我们只取 s 字符串的前两个字节,但是本例中每个汉字占用三个字节,因此没有落在边界处,也就是连 中 字都取不完整,此时程序会直接崩溃退出,如果改成 &s[0..3],则可以正常通过编译。 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点.
字符串
Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4),这样有助于大幅降低字符串所占用的内存空间。
Rust 在语言级别,只有一种字符串类型: str,它通常是以引用类型出现 &str,也就是上文提到的字符串切片。虽然语言级别只有上述的 str 类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是 String 类型。
str 类型是硬编码进可执行文件,也无法被修改,但是 String 则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串,当 Rust 用户提到字符串时,往往指的就是 String 类型和 &str 字符串切片类型,这两个类型都是 UTF-8 编码。
除了 String 类型的字符串,Rust 的标准库还提供了其他类型的字符串,例如 OsString, OsStr, CsString 和 CsStr 等,注意到这些名字都以 String 或者 Str 结尾了吗?它们分别对应的是具有所有权和被借用的变量。
String与&str的转换
从 &str 类型生成 String 类型
- String::from("hello,world")
- "hello,world".to_string()
String 类型转为 &str 类型直接取引用即可,如下例子:
fn main() {
let s = String::from("hello,world!");
say_hello(&s);
say_hello(&s[..]);
say_hello(s.as_str());
}
fn say_hello(s: &str) {
println!("{}",s);
}
这种灵活用法是因为 deref 隐式强制转换,Deref 特征后续在深入了解
字符串索引
字符串索引与切片索引类似,都是通过 [start_index..end_index] 的方式,但是字符串索引的边界是字符,而不是字节。字符串的底层数据存储是一个字节数组[u8],对于let = hello = String::from("hello"); 来说,hello的长度是5,因为每个字母在UTF-8编码中占一个字节,所以hello的长度是5。但是对于像let hello = String::from("你好呀"); 这样的字符串,hello的长度是9,因为汉字在UTF-8编码中占3个字节(大部分汉字是3个字节)。所以在这个字符串访问的时候使用&hello[0] 没有任何意义,因为你取不到 中 这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的值。还有一个原因导致了 Rust 不允许去索引字符串:因为索引操作,我们总是期望它的性能表现是 O(1),然而对于 String 类型来说,无法保证这一点,因为 Rust 可能需要从 0 开始去遍历字符串来定位合法的字符。
字符串切片
字符串切片是非常危险的操作,因为切片的索引是通过字节来进行,但是字符串又是 UTF-8 编码,因此你无法保证索引的字节刚好落在字符的边界上,例如:
fn main() {
let hello = String::from("你好");
let slice = &hello[0..2];
println!("{}", slice);
}
这个程序会报错,因为切片的索引是通过字节来进行,此时切片索引落在了“你”字内部。
字符串的操作
push
使用 push() 方法追加字符 char,也可以使用 push_str() 方法追加字符串字面量。这两个方法都是在原有的字符串上追加,并不会返回新的字符串。由于字符串追加操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut 关键字修饰。
fn main() {
let mut s = String::from("hello");
s.push_str(",world");
s.push('!');
println!("{}", s);
}
insert
insert() 方法插入单个字符 char,也可以使用 insert_str() 方法插入字符串字面量,第一个参数是字符(串)插入位置的索引,第二个参数是要插入的字符(串)。
fn main() {
let mut s = String::from("hello");
s.insert_str(5, "world");
s.insert(5, ',');
println!("{}", s);
}
replace
replace() 方法替换字符串的一部分,第一个参数是字符(串)替换位置的索引,第二个参数是要替换的字符(串),该方法会替换所有匹配到的字符串,返回一个新的字符串,而不是原来操作的字符串。该方法可适用于 String 和 &str 类型。
fn main() {
let s = String::from("hello");
let r1 = s.replace("l", "r");
let r2 = s.replace("l", "rr");
println!("{}, {}, {}", s, r1, r2);
}
replacen
replacen() 方法可以指定替换的次数,第一个参数是字符(串)替换位置的索引,第二个参数是要替换的字符(串),第三个参数是替换的次数,它跟replace() 方法一样,也是替换所有匹配到的字符串,返回一个新的字符串,而不是原来操作的字符串。该方法可适用于 String 和 &str 类型。
fn main() {
let s = String::from("hello");
let r1 = s.replacen("l", "r", 1);
println!("{}, {}", s, r1);
}
replacereplace_range
replacereplace_range() 接收两个参数,第一个参数是要替换字符串的范围(Range),第二个参数是新的字符串。该方法是直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut 关键字修饰。该方法仅适用于 String 类型。
fn main() {
let mut s = String::from("hello");
s.replace_range(1..3, "rr");
println!("{}", s);
}
pop()
pop() 方法会删除字符串的最后一个字符,并返回该字符。该方法仅适用于 String 类型。该方法直接操作原来的字符串,不会返回新的字符串。返回值为一个option类型,如果字符串为空,则返回 None,否则返回 Some(char)。实例代码如下:
fn main() {
let mut s = String::from("hello");
let ch = s.pop();
dbg!(ch);
dbg!(s);
}
上面例子中使用了dbg! 这个宏,dbg! 宏可以将变量打印出来,并会输出变量信息。相较于使用println!在调试使用时更加方便,输出也更清晰,下面是示例函数的输出:
ch = Some('o')
s = "hell"
remove
remove() 方法会删除字符串中第一个匹配到的字符串,并返回一个新的字符串。该方法仅适用于 String 类型。该方法直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut 关键字修饰。如果参数给的位置不是合法的边界,会发生错误。
fn main() {
let mut string_remove = String::from("测试remove方法");
println!(
"string_remove 占 {} 个字节",
std::mem::size_of_val(string_remove.as_str())
);
// 删除第一个汉字
string_remove.remove(0);
// 下面代码会发生错误
// string_remove.remove(1);
// 直接删除第二个汉字
// string_remove.remove(3);
dbg!(string_remove);
}
truncate
truncate() 方法会删除字符串中从开始位置到结束位置的字符,并返回一个新的字符串。该方法仅适用于 String 类型。该方法直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut 关键字修饰。如果参数给的位置不是合法的边界,会发生错误。
fn main() {
let mut string_truncate = String::from("测试truncate方法");
println!(
"string_truncate 占 {}个字节",
std::mem::size_of_val(string_truncate.as_str())
);
string_truncate.truncate(3);
dbg!(string_truncate);
}
clear
clear() 方法会删除字符串中的所有字符,并返回一个新的字符串。该方法仅适用于 String 类型。该方法直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut 关键字修饰。
fn main() {
let mut string_clear = String::from("测试clear方法");
println!(
"string_clear 占 {}个字节",
std::mem::size_of_val(string_clear.as_str())
);
string_clear.clear();
dbg!(string_clear);
}
连接字符串
使用 + 或者 += 连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型。其实当调用 + 的操作符时,相当于调用了 std::string 标准库中的 add() 方法,这里 add() 方法的第二个参数是一个引用的类型。因此我们在使用 +, 必须传递切片引用类型。不能直接传递 String 类型。+ 是返回一个新的字符串,所以变量声明可以不需要 mut 关键字修饰。
字符串的连接
字符串的连接可以使用 + 操作符:
fn main() {
let string_add = String::from("测试");
let test: String = String::from("test string");
let result = string_add + &test;
dbg!(result);
}
format宏
format! 宏是标准库中提供的宏,它用于格式化字符串。format! 宏的语法如下:
fn main() {
let s1 = String::from("hello");
let s2 = "rust";
let res = format!("{} {}", s1, s2);
dbg!(res);
}
字符串转义
可以通过转义的方式 \ 输出 ASCII 和 Unicode 字符
fn main() {
// 通过 \ + 字符的十六进制表示,转义输出一个字符
let byte_escape = "I'm writing \x52\x75\x73\x74!";
println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape);
// \u 可以输出一个 unicode 字符
let unicode_codepoint = "\u{211D}";
let character_name = "\"DOUBLE-STRUCK CAPITAL R\"";
println!(
"Unicode character {} (U+211D) is called {}",
unicode_codepoint, character_name
);
// 换行了也会保持之前的字符串格式
// 使用\忽略换行符
let long_string = "String literals
can span multiple lines.
The linebreak and indentation here ->\
<- can be escaped too!";
println!("{}", long_string);
}
可以使用在字符串前后加“#”保持字符串的格式,比如:
fn main() {
println!("{}", "hello \\x52\\x75\\x73\\x74");
let raw_str = r"Escapes don't work here: \x3F \u{211D}";
println!("{}", raw_str);
// 如果字符串包含双引号,可以在开头和结尾加 #
let quotes = r#"And then I said: "There is no escape!""#;
println!("{}", quotes);
// 如果还是有歧义,可以继续增加,没有限制
let longer_delimiter = r###"A string with "# in it. And even "##!"###;
println!("{}", longer_delimiter);
}
字符串的遍历
以字符方式遍历字符串,可以使用 for 循环配合char方法:
fn main() {
for c in "字符串".chars() {
println!("{}", c);
}
}
以字节方式遍历字符串,可以使用 for 循环配合as_bytes方法,这种方式是返回字符串的底层字节数组:
fn main() {
for b in "字符串".as_bytes() {
println!("{}", b);
}
}
字符串的截取
标准库暂时没有提供字符串的截取方法,但是crates.io 上搜索 utf8 来寻找想要的功能。可以用这个库:https://crates.io/crates/utf8_slice
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· .NET Core 中如何实现缓存的预热?
· 三行代码完成国际化适配,妙~啊~
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?