String
是定义在标准库中的类型,分配在堆上,可以动态的增长。它的底层存储是动态字节数组的方式( Vec<u8>
),但是与字节数组不同,String
是 UTF-8
编码。
&str
和 String
的关系类似于 &[T]
和 Vec<T>
。
let s = "Hello, world!";
实际上,s
的类型是 &str
let s: &str = "Hello, world!";
str
类型是硬编码进可执行文件,也无法被修改,但是 String
则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串,当 Rust 用户提到字符串时,往往指的就是 String
类型和 &str
字符串切片类型,这两个类型都是 UTF-8 编码。
除了 String
类型的字符串,Rust 的标准库还提供了其他类型的字符串,例如 OsString
, OsStr
, CsString
和CsStr
等,注意到这些名字都以 String
或者 Str
结尾了吗?它们分别对应的是具有所有权和被借用的变量。
只能将 String
跟 &str
类型进行拼接,并且 String
的所有权在此过程中会被 move。
可以使用 String::from
或 to_string
将 &str
转换成 String
类型。
从 &str
类型生成 String
类型
String::from("hello,world")
"hello,world".to_string()
如何将 String
类型转为 &str
类型? 取引用即可
#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let h = s1[0]; // 错误
}
#![allow(unused)]
fn main() {
let hello = "中国人";
let s = &hello[0..2];
}
切片的索引是通过字节来进行,但是字符串又是 UTF-8 编码,因此你无法保证索引的字节刚好落在字符的边界上。
因此在通过索引区间来访问字符串时,需要格外的小心,一不注意,就会导致你程序的崩溃。
- push
在字符串尾部可以使用 push()
方法追加字符 char
,也可以使用 push_str()
方法追加字符串字面量。这两个方法都是在原有的字符串上追加,并不会返回新的字符串。由于字符串追加操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut
关键字修饰。
fn main() {
let mut s = String::from("Hello ");
s.push('r');
println!("追加字符 push() -> {}", s);
s.push_str("ust!");
println!("追加字符串 push_str() -> {}", s);
}
- Insert
字符串变量必须由 mut
关键字修饰
fn main() {
let mut s = String::from("Hello rust!");
s.insert(5, ',');
println!("插入字符 insert() -> {}", s);
s.insert_str(6, " I like");
println!("插入字符串 insert_str() -> {}", s);
}
- Replace
replace:第一个参数是要被替换的字符串,第二个参数是新的字符串。该方法会替换所有匹配到的字符串。该方法是返回一个新的字符串,而不是操作原来的字符串。
replacen:前两个参数与 replace()
方法一样,第三个参数则表示替换的个数。该方法是返回一个新的字符串,而不是操作原来的字符串。
replace_range:第一个参数是要替换字符串的范围(Range),第二个参数是新的字符串。该方法是直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut
关键字修饰。
- Delete
pop():删除并返回字符串的最后一个字符。该方法是直接操作原来的字符串。但是存在返回值,其返回值是一个 Option
类型,如果字符串为空,则返回 None
。
remove():删除并返回字符串中指定位置的字符。该方法是直接操作原来的字符串。但是存在返回值,其返回值是删除位置的字符串,只接收一个参数,表示该字符起始索引位置。remove(0)。按照字节来处理字符串的
truncate ():删除字符串中从指定位置开始到结尾的全部字符。直接操作原来的字符串。无返回值,truncate(3)。按照字节来处理字符串的
clear():清空字符串。直接操作原来的字符串,相当于 truncate()
方法参数为 0 的时候。clear()。
- 连接
使用 +
或者 +=
连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型,调用 +
的操作符时,相当于调用了 std::string
标准库中的 add()
方法。+
和 +=
都是返回一个新的字符串。所以变量声明可以不需要 mut
关键字修饰。
使用 format!
连接字符串,format!
的用法与 print!
的用法类似
let s = format!("{} {}!", s1, s2);
如果字符串包含双引号,可以在开头和结尾加 #
以 Unicode 字符的方式遍历字符串,最好的办法是使用 chars
方法——字符
#![allow(unused)]
fn main() {
for c in "中国人".chars() {
println!("{}", c);
}
}
返回字符串的底层字节数组表现形式——字节
#![allow(unused)]
fn main() {
for b in "中国人".bytes() {
println!("{}", b);
}
}
数组的类型是 [T; Length]
,数组的长度是类型签名的一部分,因此数组的长度必须在编译期就已知。
- 数组中的所有元素必须是同一类型
- 数组的下标索引从 0 开始,越界索引会导致代码的
panic
``
第一种是速度很快但是长度固定的 array
,第二种是可动态增长的但是有性能损耗的 Vector
- 长度固定
- 元素必须有相同的类型
- 依次线性排列
fn main() {
let a = [9, 8, 7, 6, 5];
let first = a[0]; // 获取a数组第一个元素
let second = a[1]; // 获取第二个元素
}
let a: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3];
assert_eq!(slice, &[2, 3]);
let v: Vec = Vec::new();
如果预先知道要存储的元素个数,可以使用
Vec::with_capacity(capacity)
创建动态数组,这样可以避免因为插入大量新数据导致频繁的内存分配和拷贝,提升性能。
- 切片的长度可以与数组不同,并不是固定的,而是取决于你使用时指定的起始和结束位置
- 创建切片的代价非常小,因为切片只是针对底层数组的一个引用
- 切片类型[T]拥有不固定的大小,而切片引用类型&[T]则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此&[T]更有用,
&str
字符串切片也同理
几个要注意的点:
- 数组类型容易跟数组切片混淆,[T;n]描述了一个数组的类型,而[T]描述了切片的类型, 因为切片是运行期的数据结构,它的长度无法在编译期得知,因此不能用[T;n]的形式去描述
[u8; 3]
和[u8; 4]
是不同的类型,数组的长度也是类型的一部分- 在实际开发中,使用最多的是数组切片[T],我们往往通过引用的方式去使用
&[T]
,因为后者有固定的类型大小
deref
隐式强制转换,具体我们会在 Deref
特征进行详细讲解。
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
&v[100]
的访问方式会导致程序无情报错退出,因为发生了数组越界访问。 但是 v.get
就不会,它在内部做了处理,有值的时候返回 Some(T)
,无值的时候返回 None
,因此 v.get
的使用方式非常安全。
当你确保索引不会越界的时候,就用索引访问,否则用 .get
。例如,访问第几个数组元素并不取决于我们,而是取决于用户的输入时,用 .get
会非常适合。
- 枚举来处理
- trait
在实际使用场景中,特征对象数组要比枚举数组常见很多,主要原因在于特征对象非常灵活,而编译器对枚举的限制较多,且无法动态增加类型。
trait IpAddr {
fn display(&self);
}
struct V4(String);
impl IpAddr for V4 {
fn display(&self) {
println!("ipv4: {:?}", self.0)
}
}
struct V6(String);
impl IpAddr for V6 {
fn display(&self) {
println!("ipv6: {:?}", self.0)
}
}
#[test]
fn vec_diff_test() {
let v: Vec<Box<dyn IpAddr>> = vec![
Box::new(V4("127.0.0.1".to_string())),
Box::new(V6("::1".to_string())),
];
for ip in v {
ip.display();
}
}
元组中的元素可以是不同的类型。元组的类型签名是 (T1, T2, ...)
, 这里 T1
, T2
是相对应的元组成员的类型。
- 可以使用索引来获取元组的成员
- 过长的元组无法被打印输出
- 元组可以用于函数的参数和返回值
fn sum_multiply(nums: (i32, i32)) -> (i32, i32) {
(nums.0 + nums.1, nums.0 * nums.1)
}
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() 返回字符串的长度
(s, length)
}
使用 HashMap
需要手动通过use std::collections::HashMap;
HashMap
也是内聚性的,即所有的 K
必须拥有同样的类型,V
也是如此。
跟
Vec
一样,如果预先知道要存储的KV
对个数,可以使用HashMap::with_capacity(capacity)
创建指定大小的HashMap
,避免频繁的内存分配和拷贝,提升性能
HashMap
的所有权规则与其它 Rust 类型没有区别:
- 若类型实现
Copy
特征,该类型会被复制进HashMap
,因此无所谓所有权 - 若没实现
Copy
特征,所有权将被转移给HashMap
中
如果你使用引用类型放入 HashMap 中,请确保该引用的生命周期至少跟 HashMap
活得一样久。
f32 和 f64 浮点数,没有实现 std::cmp::Eq
特征,因此不可以用作 HashMap
的 Key。
目前,
HashMap
使用的哈希函数是SipHash
,它的性能不是很高,但是安全性很高。SipHash
在中等大小的Key
上,性能相当不错,但是对于小型的Key
(例如整数)或者大型Key
(例如字符串)来说,性能还是不够好。若你需要极致性能,例如实现算法,可以考虑这个库:ahash
先将 Vec
转为迭代器,接着通过 collect
方法,将迭代器中的元素收集后,转成 HashMap:
into_iter
方法将列表转为迭代器,接着通过 collect
进行收集
#[test]
fn vec_to_hashmap_test() {
let teams_list = vec![
("中国队".to_string(), 100),
("美国队".to_string(), 10),
("日本队".to_string(), 50),
];
let mut teams_map = HashMap::new();
for team in &teams_list {
teams_map.insert(&team.0, &team.1);
}
println!("{:?}", teams_map);
// 先将 Vec 转为迭代器,接着通过 collect 方法,将迭代器中的元素收集后,转成 HashMap
let teams_map2: HashMap<_, _> = teams_list.into_iter().collect();
println!("{:?}", teams_map2);
}
{% embed url="https://github.com/cracker8090/rust_tutorial/blob/main/basic_lib/src/my_string.rs" %}