集合类型
数组和切片
数组
在 Rust 中,最常用的数组有两种,第一种是因为在栈上分配内存所以速度很快但是长度固定的 array
;第二种是可动态增长的但是因为在堆上分配内存所以有性能损耗的 Vector
,在本书中,我们称 array
为数组,Vector
为动态数组。
数组(array)是一组拥有相同类型 T
的对象的集合,在内存中是连续存储的。数组使用中括号 []
来创建,且它们的大小在编译时会被确定。数组的类型标记为 [T; length]
(T
为元素类型,length
表示数组大小)。示例:
rust
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
// 或者
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
由此可得数组的特点是:长度固定不变、元素类型相同、元素依次排列。此外,数组存储在栈上,二动态数组存储在堆上。
因为数组是连续存放元素的,因此可以通过索引的方式来访问存放其中的元素:
rust
fn main() {
let a = [1, 2, 3, 4, 5];
let _first = a[0]; // 获取a数组第一个元素
let _second = a[1]; // 获取第二个元素
}
动态数组
动态数组类型用 Vec
表示,也被称为 vector。vector 允许我们在一个单独的数据结构中储存多个值,它在内存中彼此相邻地排列所有的值。vector 只能储存相同类型的值。
创建新的、空的动态数组,可以调用 Vec::new
函数,示例:
rust
fn main() {
let v: Vec<i32> = Vec::new();
}
注意这里增加了一个类型注解 Vec<i32>
。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。
还可以使用宏 vec!
来创建数组,与 Vec::new
有所不同,vec!
能在创建同时给予初始化值:
rust
let v = vec![1, 2, 3];
此处无需标注类型,因为编译器会推断元素类型。
此外还可以在声明动态数组的同时,声明数组的容量:
rust
// 声明数组的容量为10
let mut vec = Vec::with_capacity(10);
对于新建一个 vector 并向其增加元素,可以使用 push
方法:
rust
fn main() {
let mut v1 = Vec::new();
let mut v2 = vec![1, 2, 3];
v1.push(6);
v2.push(6);
}
动态数组的元素读取方式有两种,索引语法或者 get
方法:
rust
fn main() {
let v = vec![1, 2, 3, 4, 5];
// 索引法:
let third: &i32 = &v[2];
println!("The third element is {}", third);
// get 方法:
match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
}
在上例中,一旦程序获取了一个有效的引用,借用检查器将会执行所有权和借用规则(进阶会讲)来确保 vector 内容的这个引用和任何其他引用保持有效。
当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候,如果尝试在函数的后面引用这个元素是行不通的:
rust
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {}", first);
}
这是因为,在 vector 的结尾增加新元素时,在没有足够空间将所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。
数组切片
切片(slice)允许你引用集合中部分连续的元素。数组切片允许我们引用数组的一部分。示例:
rust
#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
// 引用第2、3个元素,不包括序号为3的元素
let slice: &[i32] = &a[1..3]; // 注意类型
assert_eq!(slice, &[2, 3]);
}
上面的数组切片 slice
的类型是&[i32]
,而数组的类型是[i32;5]
,简单总结下切片的特点:
- 切片的长度取决于你使用时指定的起始和结束位置
- 创建切片的代价非常小,因为切片只是针对底层数组的一个引用
- 切片类型
[T]
拥有不固定的大小,是动态大小类型(DST),也就是在编译时无法知道其大小,只能在运行时确定;而切片引用类型&[T]
则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此&[T]
更有用
字符串
Rust有两种主要类型的字符串。String
和&str
。
&str
是一个总是指向有效 UTF-8 序列的切片。当写let my_variable = "Hello, world!"
时,会创建一个&str
。String
是一个可动态增长的字符串。String
是一个智能指针,在堆上分配内存,实际上是一个字节序列(Vec<u8>
)。
在上边的基本类型中的字符中提到,Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4),这样有助于大幅降低字符串所占用的内存空间。
str
类型是硬编码进可执行文件,也无法被修改,但是 String
则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串,当 Rust 用户提到字符串时,往往指的就是 String
类型和 &str
字符串切片类型,这两个类型都是 UTF-8 编码。
创建String
的方法:
- 利用
String::from()
函数。String::from("This is the string text");
这是String的一个方法,它接受文本并创建一个String。 - 利用
.to_string()
转化。"This is the string text".to_string()
. 这是&str的一个方法,使其成为一个String。 format!
宏。 这和println!
一样,只是它创建了一个字符串。使用示例:
rust
fn main() {
let my_name = "Billybrobby";
let my_country = "USA";
let my_home = "Korea";
let together = format!(
"I am {} and I come from {} but I live in {}.",
my_name, my_country, my_home
);
}
Rust 的字符串不支持索引,在 Rust 中使用索引会报错:
rust
// 错误示例
#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let h = s1[0];
}
这是因为,String
是一个 Vec
的封装,是一个字节数组。
对于 let hello = String::from("hello");
这行代码来说,hello
的长度是 5
个字节,因为 "hello"
中的每个字母在 UTF-8 编码中仅占用 1 个字节,但是对于下面的字符串呢?
rust
let hello = String::from("中国人");
该字符串有9个字节的长度,这种情况下对 hello
进行索引,访问 &hello[0]
没有任何意义,因为你取不到 中
这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的返回值。
由于 String
是可变字符串,下面介绍 Rust 字符串的修改,添加等常用方法:
rust
fn main() {
let mut s = String::from("Hello ");
// 增加字符串(一):增加字符
s.push('r');
println!("追加字符 push() -> {}", s);
// 增加字符串(二):增加字符串
s.push_str("ust!");
println!("追加字符串 push_str() -> {}", s);
// 插入字符(一):插入字符
s.insert(5, ',');
println!("插入字符 insert() -> {}", s);
// 插入字符(二):插入字符串
s.insert_str(6, " I like");
println!("插入字符串 insert_str() -> {}", s);
}
Rust 标准库中提供了非常丰富的方法处理字符串和字符串切片,很多时候在处理字符串和字符串的切片时使用标准库函数会非常方便和安全,具体可参考对应链接,String标准库函数,&str标准库函数。
更多操作字符串的方法参考:https://doc.rust-lang.org/std/string/struct.String.html
其他字符串类型
这些字符串类型在FFI的场景、与其他的字符串类型转换中使用较为常见。如果暂时没有使用需求,或者刚开始接触Rust,本小节的内容可以暂时跳过。
CString & CStr
Rust 使用 CString
来代表一个自有的、与 C 兼容的、以 nul 结尾的字符串(中间没有 nul 字节)的类型。该类型的作用是能够安全地从 Rust 字节片或向量生成 C 兼容字符串。该类型的实例是一种静态保证,即底层字节不包含内部 0 字节("nul 字符"),且最终字节为 0("nul 终结符")。代码可以首先从 Rust 语言的普通字符串创建 CString
类型,然后将其作为参数传递给使用 C-ABI 约定的字符串函数。实现从 Rust 语言到 C 语言的字符串传递。
创建 CString 的方式如下例所示:
rust
use std::ffi::CString;
// 创建CString
fn main() {
let c_to_print = CString::new("Hello, world!");
let hello = String::from("Hello World!");
let c_str_to_print = CString::new(hello).unwrap();
}
在 C 语言中生成的字符串,Rust 使用 CStr
来表示,它和str
类型对应,表明并不拥有这个字符串的所有权。所以CStr
表示一个以终止符\n结尾的字节数组的引用,如果它是有效的 UTF-8 字符串,则可以将其转换为 Rust 语言中的&str
。实现从 C 语言到 Rust 语言的字符串传递。
OsString & OsStr
OsString 可以表示拥有的、可变的平台原生字符串的类型,可以与 Rust 字符串相互转换。之所以需要这种类型,是因为:
- 在Unix系统中,字符串通常是由非零字节组成的任意序列,在许多情况下被解释为UTF-8。
- 在Windows上,字符串通常是非零的16位值的任意序列,在有效的情况下被解释为UTF-16。
- 在Rust中,字符串总是有效的UTF-8,其中可能包含0。
OsStr 这种类型代表了对操作系统首选表示法中的字符串的借用引用。
rust
// 创建OsString
use std::ffi::OsString;
let os_string = OsString::from("foo");
// 创建OsStr
use std::ffi::OsStr;
let os_str = OsStr::new("foo");
PathBuf & Path
在 Rust 中,PathBuf 和 Path 都是用来处理文件路径的类型。PathBuf 具有所有权并且可被修改,类似于 String。Path 路径切片,类似于 str。
PathBuf示例:
rust
#![allow(unused)]
fn main() {
use std::path::PathBuf;
let mut path = PathBuf::new();
path.push(r"C:\");
path.push("windows");
path.push("system32");
path.set_extension("dll");
}
Path 示例:
rust
#![allow(unused)]
fn main() {
use std::ffi::OsStr;
use std::path::Path;
// Note: 这个例子可以在 Windows 上使用
let path = Path::new("./foo/bar.txt");
let parent = path.parent();
assert_eq!(parent, Some(Path::new("./foo")));
let file_stem = path.file_stem();
assert_eq!(file_stem, Some(OsStr::new("bar")));
let extension = path.extension();
assert_eq!(extension, Some(OsStr::new("txt")));
}