字符串-切片-数组-元组-hashpmap
字符串切片
字符串切片
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 与 &str 的转换
可以使用 String::from
或 to_string
将 &str
转换成 String
类型。
从 &str
类型生成 String
类型
String::from("hello,world")
"hello,world".to_string()
如何将 String
类型转为 &str
类型? 取引用即可
字符串索引
切片的索引是通过字节来进行,但是字符串又是 UTF-8 编码,因此你无法保证索引的字节刚好落在字符的边界上。
因此在通过索引区间来访问字符串时,需要格外的小心,一不注意,就会导致你程序的崩溃。
操作字符串
push
在字符串尾部可以使用 push()
方法追加字符 char
,也可以使用 push_str()
方法追加字符串字面量。这两个方法都是在原有的字符串上追加,并不会返回新的字符串。由于字符串追加操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut
关键字修饰。
Insert
字符串变量必须由 mut
关键字修饰
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);
转义
如果字符串包含双引号,可以在开头和结尾加 #
UTF-8
以 Unicode 字符的方式遍历字符串,最好的办法是使用 chars
方法——字符
返回字符串的底层字节数组表现形式——字节
数组
数组的类型是 [T; Length]
,数组的长度是类型签名的一部分,因此数组的长度必须在编译期就已知。
数组中的所有元素必须是同一类型
数组的下标索引从 0 开始,越界索引会导致代码的
panic
第一种是速度很快但是长度固定的 array
,第二种是可动态增长的但是有性能损耗的 Vector
长度固定
元素必须有相同的类型
依次线性排列
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
特征进行详细讲解。
&v[100]
的访问方式会导致程序无情报错退出,因为发生了数组越界访问。 但是 v.get
就不会,它在内部做了处理,有值的时候返回 Some(T)
,无值的时候返回 None
,因此 v.get
的使用方式非常安全。
当你确保索引不会越界的时候,就用索引访问,否则用 .get
。例如,访问第几个数组元素并不取决于我们,而是取决于用户的输入时,用 .get
会非常适合。
存储不同类型
枚举来处理
trait
在实际使用场景中,特征对象数组要比枚举数组常见很多,主要原因在于特征对象非常灵活,而编译器对枚举的限制较多,且无法动态增加类型。
元组
元组中的元素可以是不同的类型。元组的类型签名是 (T1, T2, ...)
, 这里 T1
, T2
是相对应的元组成员的类型。
可以使用索引来获取元组的成员
过长的元组无法被打印输出
元组可以用于函数的参数和返回值
hashmap
使用 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
进行收集
代码参考
Last updated