Rust 是一门强调安全、并发、高效的系统编程语言。无 GC 实现内存安全机制、无数据竞争的并发机制、无运行时开销的抽象机制,是 Rust 独特的优越特性。 它声称解决了传统 C 语言和 C++语言几十年来饱受责难的内存安全问题,同时还保持了很高的运行效率、很深的底层控制、很广的应用范围, 在系统编程领域具有强劲的竞争力和广阔的应用前景。
在 Rust 笔记(二)中,讲了基本类型,本文就认识一下 Rust 中的复合类型。
1.字符串类型
在 JS 中:'a'、'abc' 这样的都叫字符串,数据类型是 String,但是在 Rust 中不太一样,字符串还会细分分为三种类型,上一小节的「字符类型」还有「字符串切片类型:String」和「字符串类型: &str」。
rust
let _char: char = 'hello';
let _str: &str = "hello world";
let _string: String = String::from("hello world");
可能会比较懵逼?这 TM 不是一个类型吗?Rust 中最令人困惑的问题之一是字符串和切片(str)概念。 Rust 中的字符串是一个结构,它结合了一个指向字符串的内存分配的指针,"len" 是使用的字节数,"容量"是从内存中分配的缓冲区大小。通常得到的"容量"大于等于"len"。
rust
fn main() {
let _string: String = String::from("hello world");
println!("_string 的长度: {}", _string.len());
println!("_string 的容量: {}", _string.capacity());
}
字符串切片类型和字符串类型类似,对于字符串而言,切片就是对 String 类型中某一部分的引用:
rust
let _s: String = String::from("Hello World");
let _hello: &str = &_s[0..5];
let _world: &str = &_s[6..11];
这其中 _hello 和 _world 就是对 _s 的部分引用,通过[开始索引..终止索引]这样的操作就是创建切片的语法。其中开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置,也就是这是一个 右半开区间。在切片数据结构内部会保存开始的位置和切片的长度,其中长度是通过 终止索引 - 开始索引 的方式计算得来的。
不过在对字符串使用切片语法时需要格外小心,切片的索引必须落在字符之间的边界位置,也就是 UTF-8 字符的边界,例如中文在 UTF-8 中占用三个字节,下面的代码就会崩溃:
rust
let s = "中国人";
let a = &s[0..2];
println!("{}",a);
因为我们只取 s 字符串的前两个字节,但是本例中每个汉字占用三个字节,因此没有落在边界处,也就是连 中字都取不完整,此时程序会直接崩溃退出,如果改成 &s[0..3],则可以正常通过编译。
其实本质上在 Rust 语法中,只有一种字符串类型,那就是 &str 也就是上面说到字符串切片,但是在标准库中还有很多其他用途的字符串类型,比如 String、OsString、OsStr、CsString、CsStr。
字符串操作
push & push_str
字符串使用 push 方法追加字符。 字符串使用 push_str 方法追加字符串。
rust
let mut str = String::from("Hello World");
str.push('baixiaobai');
println!("push 追加字符 {}", str);
str.push_str(", hihihihi");
println!("push_str 追加字符串 {}", str);
insert & insert_str
字符串使用 insert 方法插入字符。 字符串使用 insert_str 方法插入字符串。
rust
let mut str = String::from("Hello World");
str.insert(5, ',');
println!("insert 插入字符 {}", str);
str.insert_str(str.len(), " hihihihi");
println!("insert_str 插入字符串 {}", str);
replace & replacen & replace_range
字符串使用 replace 方法替换字符串,第一个参数是要替换的字符串,第二个参数是新的字符串,replace 是匹配到字符串全部替换。该方法返回的是新字符串。
字符串使用 replacen 方法替换字符串,它和 replace 不同的是,它有第三个参数,表示替换的个数。该方法返回的是新字符串。
字符串使用 replace_range 方法替换字符串,第一个参数要替换字符串的范围,第二个参数是新的字符串。该方法直接操作原字符。
rust
let mut str = String::from("Hello Rust, hello rust, Hello Rust, hello rust");
let replace_str = str.replace("Rust", "rust");
println!("replace 替换字符 {}", replace_str);
let replace_str = str.replacen("Rust", "rust", 1);
println!("replacen 替换字符 {}", replace_str);
str.replace_range(6..7, "_R");
println!("replace_range 替换字符 {}", str);
pop & remove & truncate & clear
字符串使用 pop 方法删除并返回字符串的最后一个字符串。该方法直接操作原字符串,返回值是一个 Option 类型,如果字符串为空,则返回 None。
字符串使用 remove 方法删除并返回字符串中指定位置的字符。该方法只接受一个参数,返回删除位置的字符串,该方法直接操作原字符串,但是需要注意的是,这个方法是按照字节来处理字符串的,如果参数给的位置不是合法的字符边界,就会报错。
字符串使用 truncate 方法删除从指定位置开始到结束的全部字符,没有返回值。该方法直接操作原字符串,如果参数不是合并的字符串边界会报错。
字符串使用 clear 方法清空字符串,该方法直接操作原字符。
rust
let mut str = String::from("_Hello World!");
let str_pop = str.pop();
println!("pop 删除后字符串 {}", str);
dbg!(str_pop);
let str_remove = str.remove(0);
println!("pop 删除后字符串 {},删除的字符串 {}", str, str_remove);
str.truncate(6);
println!("pop 删除后字符串 {}", str);
str.clear();
println!("pop 删除后字符串 {}", str);

+ & +=
字符串使用 + 或者 += 来连接字符串,但是右边的参数必须是字符串切片类型。
rust
let str1 = String::from("Hello World!");
let str2 = String::from(" hihihihi");
let res = str1 + &str2;
println!("res: {}", res);

chars 方法 & bytes 方法 字符串使用 char 方法遍历字符。 字符串使用 bytes 方法遍历字节。
rust
let str1 = String::from("Rust 笔记(三)复合类型");
for s in str1.chars() {
println!("字符: {}", s);
}
for b in str1.bytes() {
println!("字节: {}", b);
}

String 与 &str 类型转换
String 转换为 &str 很简单,取引用就行。
rust
fn main() {
let s: String = String::from("Hello, world!");
con(&s);
}
fn con(s: &str) {
println!("{}", s);
}
&str 转换为 String 类型有两种方案:
- String::from("xxx")
- "xxx".to_string(()
rust
let _s: String = String::from("Hello World");
let _hello: &str = &_s[0..5];
let _world: &str = &_s[6..11];
let _hello_string = String::from(_hello);
let _world_string = _world.to_string();

字符串的索引
在 JS 中使用索引来范围字符串很正常,但是,但是,但是,Rust 不太行。 原因很简单,Rust 字符串底层存储格式是 u8,举个例子:对于 "hello" 来说,它每个字母咱 UTF-8 中都只是占用一个字符,但是对于"中国"这个字符来说,它每个汉字都占三个字符。
所以使用 [0] 索引只访问到 1/3 ,而不是完整的 "中"这个汉字。
2.元组
元组就是多种类型组合在一起,长度固定,类型固定。
rust
let _tup: (i32, f64, u8, u32) = (500, 6.4, 1, 300);
而获取元组的内容,可以使用匹配模式或者 . 来获取元组中的内容。
rust
let _tup: (i32, f64, u8, u32) = (500, 6.4, 1, 300);
let (x, y, z, m) = _tup;
println!("x = {}, y = {}, z = {}, m = {}", x, y, z, m);
let x1 = _tup.0;
let y1 = _tup.1;
let z1 = _tup.2;
let m1 = _tup.3;
println!("x1: {}, y1: {}, z1: {}, m1: {}", x1, y1, z1, m1);

3.枚举
整体enum的定义非常简单也符合我们的直观感受。
rust
enum Gender {
Unspecified = 0,
Female = 1,
Male = 2,
}
但是访问的时候稍微麻烦一点儿,因为我们需要在运行期间判断具体的类型,所以match匹配语法就成了必需品。if let是匹配语法的一种缩写形式。整体的匹配语法还是很友好的。
rust
#[derive(Debug)]
enum Gender {
Unspecified,
Female,
Male,
GenderInfo{x: i32, y: i32}
}
fn person_enum() {
// 枚举成员
let res1 = Gender::Unspecified;
let res2 = Gender::Female;
let res3 = Gender::Male;
let mut res4 = Gender::GenderInfo {x: 1, y: 2};
println!("{:?} {:?} {:?}", res1, res2, res3);
// 枚举查询
if let Gender::GenderInfo{x, y} = res4 {
println!("x: {}, y: {}", x, y);
}
// 枚举更新
match res4 {
Gender::GenderInfo{ref mut x, ref mut y} => {
*x = 123;
*y = 321;
},
_ =>()
}
if let Gender::GenderInfo{x, y} = res4 {
println!("x: {}, y: {}", x, y);
}
}
// x: 1, y: 2
// x: 123, y: 321
4.数组
Rust 的数组定义/初始化/更新都十分直观,唯一需要主要的是[i32;4]语法,这里分割类型和数量使用的是分号,比较特别需要记忆一下。 在 Rust 中最常用的数组有两种:
- 长度固定的 array,但是速度快,它称为数组,存储在栈上。
- 长度不固定的 VEVTOR,但是性能偏弱,它称为动态数组,存储在堆上。
rust
fn person_array() {
// 初始化
let mut arr: [i32; 5] = [1, 2, 3, 4, 5];
// 访问
println!("[0]: {}, [1]: {}, [2]: {}, [3]: {}, [4]: {}", arr[0], arr[1], arr[2], arr[3], arr[4]);
// 更新
arr[0] = 100;
arr[1] = 200;
arr[2] = 300;
arr[3] = 400;
arr[4] = 500;
// 访问
println!("[0]: {}, [1]: {}, [2]: {}, [3]: {}, [4]: {}", arr[0], arr[1], arr[2], arr[3], arr[4]);
// 二维数组初始化
let mut arr2: [[i32; 3]; 3] = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
// 访问
println!("[0][0]: {}, [0][1]: {}, [0][2]: {}", arr2[0][0], arr2[0][1], arr2[0][2]);
println!("[1][0]: {}, [1][1]: {}, [1][2]: {}", arr2[1][0], arr2[1][1], arr2[1][2]);
println!("[2][0]: {}, [2][1]: {}, [2][2]: {}", arr2[2][0], arr2[2][1], arr2[2][2]);
// 更新
arr2[0][0] = 100;
}
其实不管字符串可以切片,数组也可以切片。
rust
let a: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3];
assert_eq!(slice, &[2, 3]);
示例代码中数组切片 slice 的类型是&[i32],与之对比,数组的类型是[i32;5],简单总结下切片的特点:
- 切片的长度可以与数组不同,并不是固定的,而是取决于你使用时指定的起始和结束位置。
- 创建切片的代价非常小,因为切片只是针对底层数组的一个引用。
- 切片类型[T]拥有不固定的大小,而切片引用类型&[T]则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此&[T]更有用,&str字符串切片也同理。
5.结构体
结构体是最为最为常用的类型,这个类型等价于其他一些面向对象语言的对象类型。结构体的定义包含几个部分:
- struct 关键字
- 名称
- 结构体内容
rust
struct Person {
name: String,
age: u32
}
创建结构体实例也比较简单。但是在创建的时候记得每一个字段都要初始化。
rust
let mut person = Person {
name: String::from("Mr. baixiaobai"),
age: 18
};
我们可以通过 . 来访问结构体的字段。
rust
println!("person: {}", person.name); // person: Mr. Hello
也可以修改结构体字段内容。
rust
person.name = String::from("Mr. World");
根据已有的结构体实例,创建新的结构体实例,例如根据已有的 user1 实例来构建 person,
rust
let person = Person {
name: user1.name,
age: user1.age,
sex: 0
};
这里其实可以如 TS 一样,不用一个一个赋值,而是使用 ..。.. 语法表明凡是我们没有显式声明的字段,全部从 user1 中自动获取。需要注意的是 ..user1 必须在结构体的尾部使用。并且这里是两个点,不是三个。
rust
let person = Person {
sex: 0,
..user1
};
如果你想你的结构体没有具体的字段名称,那你可以使用元祖结构体,元组结构体在你希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。
rust
struct Point(i32, i32, i32);
如果你想你的结构体没有任何的字段和属性,那你可以使用单元结构体。
rust
struct Project;
我们在做代码调试的时候,需要注意使用 #[derive(Debug)] 对结构体进行了标记,这样才能使用 println!("{:?}", s);
rust
#[derive(Debug)]
struct Person {
name: String,
age: u32,
sex: i32
}
fn main() {
let person = Person {
name: String::from("Mr. baixiaobai"),
age: 18,
sex: 0
};
println!("person: {:?}", person);
}
当结构体较大时,我们可能希望能够有更好的输出表现,此时可以使用 {:#?} 来替代 {:?}。
还有一个简单的输出 debug 信息的方法,那就是使用 dbg! 宏。
rust
dbg!(&person);
总结
本章的重点在复合类型上,复合类型是由其它类型组合而成的,最典型的就是结构体 struct 和枚举 enum。本文 Rust 中的复合类型就到这里了。