Rust字符类型
Rust字符类型
两种字符串
Rust 语言提供了的字符串:
- 字符串切片。
&str
,字符串字面量属于这种 - 字符串。标准库中的一个 公开
pub
结构体。字符串对象String
。
(有点类似于C字符串和Cpp String类型。Cpp中,前者是一个指针,后者是一个封装了更多功能的对象)
当 Rust 用户提到字符串时,往往指的就是 String
类型和 &str
字符串切片类型,这两个类型都是 UTF-8 编码
更多类型
不过其实还有更多:
除了 String
类型的字符串,Rust 的标准库还提供了其他类型的字符串,例如 OsString
, OsStr
, CsString
和 CsStr
等,注意到这些名字都以 String
或者 Str
结尾了吗?它们分别对应的是具有所有权和被借用的变量。
或者非标准库:utf8_slice。想要准确的从 UTF-8 字符串中获取子串是较为复杂的事情,需要借这个库
转换
在之前的代码中,已经见到好几种从 &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
特征进行详细讲解。
类型 - 字符串字面量/字符串切片
更详细的见容器类型。
因为切片是对集合的部分引用,因此不仅仅字符串有切片,其它集合类型也有。所以不放在此处
字符串字面量的核心代码可以在模块 std::str 中找到。Rust 中的字符串字面量被称之为 字符串切片。因为它的底层实现是 切片 (Slice)。
let my_str: &str = "hello";
字面量 &str 就是在 编译时 就知道其值的字符串类型,它也是字符的集合,被硬编码赋值给一个变量。
字符串字面量模式是 静态 的,所以,字符串字面量从创建时开始会一直保存到程序结束。
类型 - 字符串对象
特点
字符串对象并不是 Rust 核心内置的数据类型,它只是标准库中的一个 公开 pub 的结构体。
pub struct String
字符串对象是使用 UTF-8 作为底层数据编码格式,长度可变的集合。
字符串对象在 堆 heap 中分配,可以在运行时提供字符串值以及相应的操作方法。
大小:每个字符占 1~4 字节,比较省空间,与之相反的是,自符类型是站 4 字节的 Unicode 类型
字符串索引/切片
Rust 不允许索引字符串,而字符串切片也是极其危险的操作
在说原因和注意项之前,我们先来理解字符串:
理解字符串,不同字符串的内部
来看不同的字符串,他们的各个表现形式和编码形式:
英文
let hello = String::from("Hola");
// 大小: `Hola` 的长度是 `4` 个字节。英文字母在 UTF-8 编码中仅占用 1 个字节
中文
let hello = String::from("中国人");
// 大小:
// 字符数量为3
// 字节大小为9,因为大部分常用汉字在 UTF-8 中的长度是 `3` 个字节
// 索引问题:
// 此时访问 `&hello[0]` 没有任何意义,因为你取不到 `中` 这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的返回值。
梵文
// 现在看一下用梵文写的字符串 `“नमस्ते”`
// 字节数组角度。18 字节,底层存储形式
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
// 字符角度。第四和六两个字母根本就不存在,没有任何意义
['न', 'म', 'स', '्', 'त', 'े']
// 字母串角度
["न", "म", "स्", "ते"]
为什么字符串不允许被索引,为什么切片非常危险
Rust 字符串不允许被索引的原因:
- 按字节来索引可能存在问题。
Rust 提供了不同的字符串展现方式 (见下),这样程序可以挑选自己想要的方式去使用,而无需去管字符串从人类语言角度看长什么样。 - 索引操作,我们总是期望它的性能表现是 O(1)。
然而对于String
类型来说,无法保证这一点,因为 Rust 可能需要从 0 开始去遍历字符串来定位合法的字符。
话说我记得某个语言 (忘了是啥, python?) 的字符串索引,是按第几个字符来的,而不是第几个字节
Rust 字符串被切片非常危险的原因:
- 切片的索引是通过字节来进行,但是字符串又是 UTF-8 编码,因此你无法保证索引的字节刚好落在字符的边界上
所以我们需要非常注意
demo,报错示例
Rust 字符串不允许被索引的Demo:
不允许索引字符串的例子:
在其它语言中,使用索引的方式访问字符串的某个字符或者子串是很正常的行为,但是在 Rust 中就会报错:
let s1 = String::from("hello");
let h = s1[0];
该代码会产生如下错误
3 | let h = s1[0];
| ^^^^^ `String` cannot be indexed by `{integer}`
|
= help: the trait `Index<{integer}>` is not implemented for `String`
Rust 字符串被切片非常危险的Demo:
let hello = "中国人";
let s = &hello[0..2];
会直接造成崩溃
thread 'main' panicked at 'byte index 2 is not a char boundary; it is inside '中' (bytes 0..3) of `中国人`', src/main.rs:4:14
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
解决方案
想要准确的从 UTF-8 字符串中获取子串是较为复杂的事情,例如想要从 holla中国人नमस्ते
这种变长的字符串中取出某一个子串,使用标准库你是做不到的。
你需要在 crates.io
上搜索 utf8
来寻找想要的功能。
可以考虑尝试下这个库:utf8_slice。
类型 - 字符串对象方法
特殊 (增删 转换)
还有部分内容见 #转换
new() 创建
创建一个新的字符串对象
新建字符串对象
String::new() // 创建一个新的空字符串,它是静态方法。
String::from() // 从具体的字符串字面量创建字符串对象。
代码示例
let s1 = String::new();
let s2 = String::from("你好");
println!("s1:{}, s1-len:{}", s1, s1.len()); // 输出 s1:, s1-len:0
println!("s2:{}, s2-len:{}", s2, s2.len()); // 输出 s2:面向加薪学习, s2-len:6
as_str() 字符串对象转字面量
返回一个字符串对象的 字符串 字面量。
fn show_name(name:&str){
println!("Hello {}", name);
}
let s7 = String::from("World");
show_name(s7.as_str()); // 输出 Hello World
to_string() 字面量转字符串对象
将字符串转换为字符串对象,方便以后可以有更多的操作。
let s6 = "ABCD".to_string();
println!("{}", s6); // 输出 ABCD
增删改查
push/push_str 追加
是在原字符上追加字符,而不是返回一个新的字符串
// 追加字符
let mut s3 = String::new();
s3.push('O');
s3.push('K');
println!("{}",s3); // 输出 OK
// 追加字符串
let mut s3 = String::new();
s3.push_str("OK");
println!("{}",s3); // 输出 OK
insert/insert_str 插入
可以使用 insert()
方法插入单个字符 char
,也可以使用 insert_str()
方法插入字符串字面量,与 push()
方法不同,这俩方法需要传入两个参数,第一个参数是字符(串)插入位置的索引,第二个参数是要插入的字符(串),索引从 0 开始计数,如果越界则会发生错误。由于字符串插入操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut
关键字修饰。
示例代码如下:
fn main() {
let mut s = String::from("Hello rust!");
s.insert(5, ',');
println!("insert -> {}", s); // 输出 insert -> Hello, rust!
s.insert_str(6, " I like");
println!("insert_str -> {}", s); // 输出 insert_str -> Hello, I like rust!
}
replace/replacen/replace_range 替换
指定字符串子串替换成另一个字符串。该方法是返回一个新的字符串,而不是操作原来的字符串。
// replace
let s4 = String::from("I like rust. Learning rust is my favorite!");
let result = s4.replace("rust", "RUST");
println!("{}",result); // 输出 I like RUST. Learning RUST is my favorite!
// replacen
// 和replace用法差不多,多了第三个参数,表示替换的个数
// replace_range
// 和replace用法差不多,**该方法是直接操作原来的字符串,不会返回新的字符串**
pop/remove/truncate/clear 删除
与字符串删除相关的方法有 4 个,它们分别是 pop()
,remove()
,truncate()
,clear()
。这四个方法仅适用于 String
类型。
pop
—— 删除并返回字符串的最后一个字符remove
—— 删除并返回字符串中指定位置的字符truncate
—— 删除字符串中从指定位置开始到结尾的全部字符clear
—— 清空字符串
trim 去除空白符
去除字符串头尾的空白符。空白符是指 制表符 \t、空格 ``、回车 \r、换行 \n 和回车换行 \r\n 等等。
let s8 = " \tGo语言极简一本通\tGo语言微服务架构核心22讲 \r\n从0到Go语言微服务架构师\r\n ";
println!("length is {}", s8.len()); // 输出 length is 103
println!("length is {}", s8.trim().len()); // 输出 length is 94
println!("s8:{}",s8);
// 输出
s8: Go语言极简一本通 Go语言微服务架构核心22讲
从0到Go语言微服务架构师
+
/add/format 字符串拼接
内部实现是重写了 add() 方法。
add(self,&str) -> String { ... }
返回新的字符串对象。
let s11 = "Go语言极简一本通".to_string();
let s12 = " 欢喜".to_string();
let result2 = s11 + &s12;
println!("{}",result2);
// 输出 Go语言极简一本通 欢喜
另一个方法是使用 format!
连接字符串
fn main() {
let s1 = "hello";
let s2 = String::from("rust");
let s = format!("{} {}!", s1, s2);
println!("{}", s);
}
返回非字符串
len() 获取长度
返回字符串中的 总字节数。该方法会统计包括 制表符 \t、空格 ``、回车 \r、换行 \n 和回车换行 \r\n 等等。
let s5 = String::from("面向加薪学习 www.go-edu.cn\n");
println!("length is {}", s5.len()); // 输出 33
split() 分隔
将字符串根据某些指定的 字符串子串 分割,返回分割后的字符串子串组成的切片上的迭代器。
let s9 = "Go语言极简一本通、Go语言微服务架构核心22讲、从0到Go语言微服务架构师";
for item in s9.split('、'){
println!("充电科目: {}",item);
}
// 输出
// 充电科目: Go语言极简一本通
// 充电科目: Go语言微服务架构核心22讲
// 充电科目: 从0到Go语言微服务架构师
chars() 转字符数组
将一个字符串打散为所有字符组成的数组
let s10 = "从0到Go语言微服务架构师";
for c in s10.chars(){
println!("字符: {}",c);
}
// 输出
字符: 从
字符: 0
字符: 到
字符: G
字符: o
字符: 语
字符: 言
字符: 微
字符: 服
字符: 务
字符: 架
字符: 构
字符: 师
按字符/字节遍历
// 按字符遍历
for c in "中国人".chars() {
println!("{}", c);
}
// 输出:
中
国
人
// 按字节遍历
for b in "中国人".bytes() {
println!("{}", b);
}
// 输出:
228
184
173
229
155
189
228
186
186