12、String(字符串)
String(字符串)
字符串的本质就是一种特殊的容器类型,在程序开发过程中,常被作为一个整体来处理,不同于其它语言的字符串,Rust官方库的String字符串会显得很复杂,不过没关系,再复杂也得一点一点学会
字符串的创建
现在我们学习两种字符串,一种是字符串字面量,另一种是可变长度的字符串对象
1.&str的创建
Rust内置的字符串类型是str,这种类型就是字符串字面量,字符串字面量在定义之后不可有任何形式的修改
,通常以&str
的形式出现。&str
是字符的集合,代表的是不可变的UTF-8
编码的字符串的引用。
创建&str
有如下方式
let str="Hello Rust!";//直接赋值一个简单的字符串,str类型为&str
let Str=String::from("Hello Rust!");//创建String对象
let str=Str.as_str();//将String对象转换为&str
2.String的创建
String是由Rust标准库提供
的、拥有所有权的UTF-8编码
的字符串类型,创建后可以任意地修改。它的本质其实就是一个字段为Vec<u8>
类型的结构体,它将字符内容存放在堆上,由三部分组成,分别是:
- 指向堆上字节序列的指针(as_ptr方法)
- 记录堆上字节序列的长度(len方法)
- 堆分配的容量(capacity方法)
有三种创建String的方法:
//使用String::new()创建空的String对象
let mut str1=String::new();//不加mut无法修改
//使用String::from()从字符串字面量创建String对象
let str2=String::from("Hello Rust!");
//使用to_string方法将字符串字面量转换为String对象
let str="Hello Rust!";
let str3=str.to_string();
字符串的修改
String对象的常见修改操作有追加、插入、连接、替换、删除等。
1.追加
使用push
在String后面追加字符,使用push_str
在String后面追加字符串字面量。
这两个方法都是在原String上追加,不会创建新的String。
fn main(){
let mut str=String::from("Hello ");//必须要加mut,否则无法修改
str.push('R');
str.push_str("ust!");
println!("{}",str);
}
2.插入
使用insert
在String中插入字符,使用insert_str
在String中插入字符串字面量。
这两个方法都接收两个参数,一个是插入位置的索引,一个是插入的字符或字符串字面量。
同样的,这两个方法都是在原String上追加,不会创建新的String
fn main(){
let mut str=String::from("Hello !");
str.insert(6,'R');
str.insert_str(7,"ust");
println!("{}",str);
}
运行结果如下:
一定要注意写mut还有注意方法参数啊,我写的时候就总是忘记写mut(
注意
insert
和insert_str
是基于字节序列的索引进行操作的,内部会通过is_char_boundary
方法判断插入位置的索引是否在合法边界内,如果索引非法将会导致程序错误。
运行结果如下:
现在试一下越界的索引:
运行结果如下:
可以看到,不管是非法UTF-8插入边界还是非法越界的索引,is_char_boundary
方法都可以准确地识别出错误并抛出Panic
总结
不要用索引去切中文等多字节UTF-8字符,可以避免很多问题
3.连接
使用+
或+=
运算符将两个字符串连接为一个新的字符串,运算符的右边必须是字符串字面量,运算符的左边必须是String。
不能对两个String类型字符串使用+
或+=
运算符,也不能对两个字符串字面量使用+
或+=
运算符。
连接会返回新的字符串,也就是使用+
或+=
运算符都会返回新的字符串。
fn main(){
let mut str1=String::from("Hello");
let str2=String::from("Rust");
//let str3=' ';
let str4="!";
//let mut str=str1+str3+str2;
let mut str=str1+str2;
str+=str4;
println!("{}",str);
}
char不能直接拼接到String后面,Rust不支持这样的操作,所以上面的代码会报错
注意
在+
运算符的右边,也可以使用&s(s是String对象)拼接,这样操作,&s就是s这个String对象的“字符串字面量”,可以直接用于连接操作。
这是因为String类型实现了Deref trait[1],执行连接操作时会自动解引用为&str类型,使用as_str
方法就可以将String类型转换为&str类型
对于较为复杂的或带有格式的字符串连接,可以使用格式化宏format!
,它对于String和&str类型的字符串都适用
fn main(){
let str1=String::from("Hello");
let str2="Rust!";
let str3=format!("{} {}",str1,str2);
println!("{}",str3);
}
运行结果如下:
4.替换
使用replace
和replacen
方法,可以将主串中指定的字串替换为一个新的字串
replace(<要被替换掉的字符串子串>,<新字符串>);
replacen(<要被替换掉的字符串子串>,<新字符串>,替换的个数);
fn main(){
let s=String::from("aaabbccdaa");
let s1=s.replace("aa","ee");
let s2=s.replacen("aa","ee",1);
println!("{}",s1);
println!("{}",s2);
}
运行结果如下:
5.删除
使用pop
,remove
,truncate
和clear
方法删除字符串中的字符。
- pop:删除并返回字符串的最后一个字符,返回类型为Option<char>。如果字符串为空,则返回None。
- remove:删除并返回字符串中指定位置的字符,其参数是该字符的起始索引位置(注意,
remove
方法是按照字节处理字符的,所以面对多字节的UTF-8字符时,如果给定的索引不是合法的字符边界,就会直接报错)。 - truncate:删除字符串中从指定位置开始到结尾的全部字符,其参数是起始索引位置,无返回值(注意,
truncate
方法也是按照字节处理字符,如果给定的索引不是合法的字符边界,就会直接报错)。 - clear:删除字符串中的所有字符,无返回值(等价于将
truncate
的参数设置为0)。
fn main(){
let mut str=String::from("Hello Rust!");
println!("{:?}",str.pop());
println!("{}",str);
println!();
println!("{:?}",str.remove(9));
println!("{}",str);
println!();
str.truncate(5);
println!("{}",str);
println!();
str.clear();
println!("{}",str);
}
运行结果如下:
字符串的访问
1.Unicode
Unicode
是一个全球统一的字符编码标准,他为世界上几乎所有的文字系统分配唯一的数字编号(甚至包含各类emoji),称为代码点
(Code Points
)。
比如:
A
的代码点是U+0041
你好
的代码点是U+4F60
😄
的代码点是U+1F604
Unicode只是一套“字符与数字”的映射表。
2.UTF-8
UTF-8
(Unicode Transformation Format-8-bit)是一种具体的编码方式
,用于将Unicode的代码点转换为字节序列
,以便在计算机中存储和运输。
特点:
特征 | 说明 |
---|---|
长度可以变化 | 1-4个字节表示一个字符 |
向后兼容ASCII | 所有ASCII字符(0-127)用一个字节表示,与ASCII完全一致 |
自动纠错 | 有效的UTF-8序列有明确的格式,能自动检测错误 |
你
占 3 个字节,😄
占 4 个字节 —— 单个字符的所占字节数不同,所以我们不能简单地通过“字节数”来计算“字符数”。
3.访问UTF-8字符串
根据上面的简单说明,我们可以得到两个结论:
- 字符串是UTF-8的字节序列,不能直接使用索引来访问字符。
- 字符串操作可以分为
按字节处理
和按字符处理
两种方式,按字节处理使用bytes
方法返回按字节迭代的迭代器,按字符处理使用chars
方法返回按字符迭代的迭代器。
1.使用len
方法获取字符串长度
使用len
方法得到的并不是String的字符数,而是String所占的字节大小。
fn main(){
let str="大风起兮云飞扬";
println!("{}",str.len());
}
运行结果如下:
由此可知,中文字符在UTF-8中占三个字节
2.通过迭代器访问字符串的字符
fn main(){
let str="安得猛士兮走四方";
for b in str.bytes(){
print!("{} ",b);
}
println!();
for c in str.chars(){
print!("{} ",c);
}
}
运行结果如下:
练习
这次的练习要完成的内容比较多
pub struct Ticket {
title: String,
description: String,
status: String,
}
impl Ticket {
// TODO: implement the `new` function.
// The following requirements should be met:
// - Only `To-Do`, `In Progress`, and `Done` statuses are allowed.
// - The `title` and `description` fields should not be empty.
// - the `title` should be at most 50 bytes long.
// - the `description` should be at most 500 bytes long.
// The method should panic if any of the requirements are not met.
// Panic messages: "Only `To-Do`, `In Progress`, and `Done` statuses are allowed",
// "Title cannot be empty", "Description cannot be empty", "Title cannot be longer than 50 bytes",
// "Description cannot be longer than 500 bytes"
// TODO:实现 `new` 函数。
// 应满足以下要求
// - 只允许使用 `To-Do`、`In Progress` 和 `Done` 状态。
// - `title` 和 `description` 字段不能为空。
// -`title` 字段的长度不得超过 50 字节。
// - `description` 字段的长度不得超过 500 字节。
// 如果有任何要求未满足,该方法应恐慌。
// panic 消息:"Only `To-Do`, `In Progress`, and `Done` statuses are allowed"、
// "Title cannot be empty", "Description cannot be empty", "Title cannot be longer than 50 bytes"、
// "Description cannot be longer than 500 bytes"。
// You'll have to use what you learned in the previous exercises,
// as well as some `String` methods. Use the documentation of Rust's standard library
// to find the most appropriate options -> https://doc.rust-lang.org/std/string/struct.String.html
pub fn new(title: String, description: String, status: String) -> Self {
if title./* TODO */ Self {
title,
description,
status,
}
Self {
title,
description,
status,
}
}
}
- status只能是
To-Do
,In Progress
和Done
三种字符串其一。 title
和description
字段不能为空。title
字段的长度不能超过50字节。description
字段的长度不能超过500字节。- 如果以上的任何一条不满足,都应该触发Panic。
这个练习其实并不难,就是要写的代码比较多,我敲了一小会儿,也是完成了
pub fn new(title: String, description: String, status: String) -> Self {
if status!="To-Do"&&status!="In Progress"&&status!="Done"{
panic!("Only `To-Do`, `In Progress`, and `Done` statuses are allowed");
}
if title.is_empty(){
panic!("Title cannot be empty");
}
if description.is_empty(){
panic!("Description cannot be empty");
}
if title.len()>50{
panic!("Title cannot be longer than 50 bytes");
}
if description.len()>500{
panic!("Description cannot be longer than 500 bytes");
}
Self {
title,
description,
status,
}
}
代码无误!
这一节就先到这里了,后面学到所有权,生命周期估计会有很多补充。