元组
元组是将多种类型的多个值合到一个复合类型中的一种基本方式。元组的元素数量是固定的,声明之后,无法增长或缩小。可以用于函数的多值返回。
rust
fn main() {
// 声明一个元组类型的变量
let tup: (i32, u8, f64) = (500, 6, 1.0);
// 元组解构,将元组的元素赋值给单个变量
let (x, y, z) = tup;
println!("x: {}, y:{}, z:{}", x, y, z);
// 通过索引访问
println!("{}, {}, {}", tup.0, tup.1, tup.2);
// 特殊类型,被称为单元类型。如果表达式不返回任何其他值
let unit_tuple = ();
println!("{:?}", unit_tuple);
println!("{:?}", add(1, 2))
}
fn add(a: i32, b: i32) -> (i32, i32, i32) {
return (a, b, a + b);
}
结构体
和元组类似,结构体每一部分可以是不同类型。与元组不同的是,结构体需要命名各个元素以便清楚的表明值的含义。元组的优点在于创建简单,可以快速使用,缺点就是可读性不高,访问的时候通过下标访问。
rust
fn main() {
let mut u1 = User {
name: String::from("name"),
content: String::from("content"),
};
// 更新属性
u1.name = String::from("name2");
u1 = newUser(u1.name, u1.content);
// 从现有结构体值中创建新的值,并更新部分值
let u2 = User {
name: String::from("name2"),
..u1 // 剩余字段未显式设置值的字段则使用 u1 的字段值
};
// 无法使用 u1.content, 因为 u1.content 的所有权发生转移, 转移到了 u2
// println!(
// "u2.name: {}, u2.content: {}, u1.name:{}, u1.content{}",
// u2.name, u2.content, u1.name, u1.content
// )
println!(
"u2.name: {}, u2.content: {}, u1.name:{}",
u2.name, u2.content, u1.name
)
}
// 如果参数名和结构体属性名相同,可以简化写法,避免指定属性赋值
fn newUser(name: String, content: String) -> User {
User { name, content }
}
struct User {
name: String,
content: String,
}
此外,可以定义元组结构体。元组结构体有着结构体名称提供的含义,但是没有具体的字段名,只有字段类型。对于一些不需要知道结构体属性名字的场景,可以使用元组结构体,从而对元组类型进行区分。
rust
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
// 接收 Point 类型的函数不能接收 Color 类型的值
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
对于没有任何字段的结构体,称之为类单元结构体。常用于在某个类型上实现 tarit, 但不需要在类型中存储具体的数据。
rust
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
结构体方法 :与函数类似,同样使用 fn
关键字和名称声明,可以拥有参数和返回值。与函数不同的是,他们在结构体的上下文中被定义,且它们的第一个参数总是 self
,代表调用该方法的结构体实例。如下面代码所示,对于结构体方法,第一个参数必须是实例本身,可以为 self
「所有权转移」、&self
「不可变引用」、&mut self
「可变引用」,且需要定义在 impl
中。此外,对于第一参数非 self
的函数,我们称之为关联函数,调用方式为 User::fly()
。这个方法位于结构体的命名空间中,::
语法用于关联函数和模块创建的命名空间。
rust
fn main() {
let mut u = User {
name: String::from("hello"),
content: String::from(""),
age: 19,
};
// 调用 u.get_name, 会将 u 的所有权转移到函数,无法使用 u.is_adult()
// println!("user: {} adult: {}", u.get_name(), u.is_adult());
println!("user: {} adult: {}", u.name, u.is_adult());
u.set_age(17);
println!("user:{} adult: {}", u.name, u.is_adult());
// 调用关联函数
println!("fly: {}", User::fly())
}
struct User {
name: String,
content: String,
age: i32,
}
impl User {
// 非可变引用
fn is_adult(&self) -> bool {
return self.age > 18;
}
// 可变引用
fn set_age(&mut self, age: i32) {
self.age = age
}
// 所有权发生转移
fn get_name(self) -> String {
self.name
}
// 关联函数
fn fly() -> String {
String::from("fly")
}
}
结构体可以创建出在领域中有意义的自定义类型。通过结构体,可以将关联的数据片段和方法联系起来,使得代码更加清晰。
枚举
在上面介绍的类型中,我们可以设置一个变量为标量类型、元组、结构体。那么有没有办法设置一个变量,它的值是可列举的?这里就需要使用枚举类型。当然,在 Rust 中,枚举类型除了列举值,还可以存储数据,配置 match
进行模式匹配,使用 if let
简化枚举结构的处理。
定义枚举
下面的例子中,使用 enum
定义枚举类型,用于标识 ip
类型。使用 ::
引用枚举成员。
rust
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
ip: String,
kind: IpAddrKind,
}
fn main() {
let v4 = IpAddr {
ip: "127.0.0.1".to_string(),
kind: IpAddrKind::V4,
};
let v6 = IpAddr {
ip: "::1".to_string(),
kind: IpAddrKind::V6,
};
process_ip(v6)
}
fn process_ip(ip: IpAddr) {}
在上面的例子中,定义了新一个新的结构体 IpAddr
来存储枚举成员和 String
值。在 Rust 中,可以将数据直接放进每一个枚举成员,而不是将枚举作为结构体的一部分。直接将数据附加到枚举成员上,无需创建一个新的结构体。
rust
#[derive(Debug)]
enum IPAddr {
v4(String),
v6(String),
}
fn main() {
let v4 = IPAddr::v4("127.0.0.1".to_string());
let v6 = IPAddr::v6("::1".to_string());
println!("{:?}, {:?}", v4, v6)
}
枚举的成员中可以存储任意类型的数据。上节中,我们知道结构体也可以存储任意类型的数据。那么枚举和结构体的区别是什么?什么时候使用枚举?什么时候使用结构体?枚举定义了一个变量的取值列举,即枚举类型变量的值只能是枚举值中的一个。在枚举的成员中,可以不包含任何数据、包含数据和匿名结构体。在函数传参中可以直接传递一个枚举类型,如果使用结构体,则需要将不同的结构体定义为函数的入参。在业务开发中,通常将一组互斥的行为定义为枚举。例如:消息的退出、移动、写操作。
rust
enum Message {
Quit, // 不关联任何数据
Move { x: i32, y: i32 }, // 匿名结构体
Write(String), // 包含 String
Color(i32, i32, i32), // 包含三个 i32
}
impl Message { // 使用枚举定义方法
fn call(&self) {
// 在这里定义方法体
}
}
// 使用结构体定义
struct QuitMessage; // 类单元结构体
struct MoveMessage {
// 结构体
x: i32,
y: i32,
}
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体
在 Rust 中,内置了一个 Option
的枚举类型,用于标识一个变量是否为空值。Option
枚举包含两个成员,一个表示空值「None
」,一个用于存储具体值「Some(T)
」。
rust
pub enum Option<T> {
None,
Some(T),
}
fn main() {
let someValue = Some(8);
// let uncertainValue = None; //---- type must be known at this point
// 对于不确定具体值的 Option 类型,需要定义存储值的类型
let uncertainValue: Option<i32> = None;
}
match 控制流运算符
上节中介绍了枚举类型的定义以及内置的 Option
类型。在 Rust 中可以使用 match
关键字对枚举类型进行模式匹配并执行相关代码。编译器确保了 match
的所有情况都应得到处理。
rust
enum IpAddrKind {
V4,
V6,
}
fn process(kind: IpAddrKind) {
match kind {
// 需要处理所有的枚举类型
IpAddrKind::V4 => {
println!("connect v4")
}
IpAddrKind::V6 => {
println!("connect v6")
}
}
}
enum IPAddr {
v4(String),
v6(String),
}
fn processIP(ip: IPAddr) {
match ip {
// 获取枚举中包含的值
IPAddr::v4(v4) => {
println!("v4: {}", v4)
}
// 使用 _ 占位符忽略枚举中包含的值
IPAddr::v6(_) => {
println!("not support v6")
}
}
}
fn main() {
let k = 9;
match k {
1 => {}
// 满足穷举性, 前面都匹配不到会走到这个 case, other 表示具体的值
other => {
println!("math without 1 value: {}", other)
}
}
match k {
1 => {}
// 满足穷举性, 在最后的分支中忽略值
_ => {
println!("math without 1")
}
}
}
If let 简单控制流
在使用 Option
时,可以使用 if let
来简化代码。可以将 if let
看做是 match
中的一个分支,简化重复代码。
rust
fn main() {
let someValue = Some(8);
match someValue {
None => {}
Some(v) => {
println!("{}", v)
}
}
// 使用 if let 简化代码, 其实就是 match 的语法糖, 只走了 Some(v) 这个分支
if let Some(v) = someValue {
println!("{}", v)
}
}
数组
数组是编程中经常用到的数据结构,它能够存储多个同类型的数据。数组有以下特点:固定大小、存储同类型元素、随机访问。在 Rust 中,数组是直接分配到栈上的,读写速度比较快。然而,在真实业务场景中,往往会对数组进行裁剪,追加。由于数组的大小是固定的,只能创建新的数组,并将元素赋值到新的数组,增加了处理复杂度。那有没有其他方案呢?
rust
fn main() {
// 初始化一个数组, 大小为 3, 初始值为 5
let arr = [3; 5];
println!("arr: {:?}", arr);
// 初始化大小为 5 的数组
let arr: [i32; 5] = [1, 2, 3, 4, 5];
println!("arr: {}", arr.len());
// 遍历
for a in arr {
println!("{}", a)
}
// /索引, 如果索引溢出会直接退出函数
let mut arr1 = [0; 6];
for (i, a) in arr.iter().enumerate() {
arr1[i] = *a
}
arr1[5] = 6;
println!("{:?}", arr1)
}
可以使用 vector 来解决数组固定大小的问题。vector 其实是一个结构体,它由三个字段组成 ptr
: 指向堆内存的连续空间、len
: 空间使用的长度、capacity
: 空间总容量。往 vector 里面 push 数组时, 判断容量是否使用完,如果使用完则申请新的连续空间并拷贝数据,否则将 len 加一。从而避免不断在栈上复制数组。
rust
fn main() {
// 初始化
let mut v: Vec<i32> = Vec::new();
println!("{}, {}", v.len(), v.capacity());
v.push(8); // 扩容,容量为 4
v.push(9);
println!("{}, {}", v.len(), v.capacity());
// 遍历
for i in &v {
println!("{}", i);
}
// 遍历更新
for i in &mut v {
*i += 50
}
// 索引
println!("{}", v[1]);
// 索引溢出,直接退出程序
// println!("{}", v[9]);
// 使用 get 方法,返回一个 Option
if let Some(v) = v.get(9) {
println!("{}", v);
} else {
println!("can not get value with index 9")
}
}
字符、字符串、切片
介绍字符相关操作之前,需要先介绍一下编码的相关概念。编码是信息从一种格式转化到另一种格式的过程。我们知道, 计算机数据存储时, 都是使用二进制的形式。编码 : 就是将字符「a」转换到二进制「1100001」进行存储; 解码: 就是将二进制「1100001」转换为字符「a」。那如何确定字符 「a」 要转换成 「1100001」 而不是 「1100000」 呢, 这就需要大家约定一个规则「对照表」, 来保证能够正常编码和解码。
Unicode ****是一个编解码对照表, 它收集了世界上所有的字符, 并为每一个字符分配了一个唯一的 Unicode 码点。对字符进行存储的时候, 可以将一个字符使用 int32「四个字节, 支持 2^32 个字符」 。
统一使用四个字节表示一个字符的方式比较简单。但是会浪费太多的存储的空间, 那有没有一种更好的编码方式呢?UTF-8 是一个将 Unicode 码点编码为字节序列的变长编码。在 UTF-8 编码中, 使用 1 到 4 个字节来表示每个Unicode 码点。每个符号编码后第一个字节的高端 bit 位用于表示编码总共有多少个字节。如果第一个字节的高端bit为 0,则表示对应 7bit 的 ASCII 字符; 如果第一个字节的高端 bit 是110, 则说明需要2个字节, 后续的每个高端bit都以 10 开头。更大的 Unicode 码点也是采用类似的策略处理。使用 UTF-8 可以节省存储空间, 但无法直接判断字节序列包含的字符个数, 也无法直接通过下标访问第 n 个字符。
字符「char」是 Rust 的标量类型,使用 Unicode 进行编码,一个字符占用四个字节
rust
fn main() {
println!("Size of char: {}", std::mem::size_of::<char>());
}
字符串 「String」是字符组成的连续集合,封装了各种对字符串处理的方法。字符串是使用 UTF-8 编码,即字符串中的字符所占字节数是变化的「1-4」。在 Rust 中,String 是一个结构体,里面包含三个字段 ptr、len、capacity,用于处理字符串相关操作。
rust
fn main() {
// len 表示 s1 持有 vec 使用的空间,capacity 持有 vec 的总容量,当容量不足时需要进行扩容
let str = String::from("hello");
println!("str len: {}, cap: {}", str.len(), str.capacity());
// 使用 utf-8 编码
let (str1, str2) = (String::from("中"), String::from("a"));
println!(
"str1 len: {}, cap: {}, str1 len: {}, cap: {}",
str1.len(),
str1.capacity(),
str2.len(),
str2.capacity()
);
for c in str.chars() {
print!("{}", c)
}
}
对于字符串而言,切片「&str」是对 String 类型中某一部分的引用。在下面的例子中,str
是 String
类型,hello
和 word
是 &str
类型,也就是 str
的引用类型。
rust
fn main() {
let str = String::from("hello world");
let hello = &str[0..5];
let world = &str[6..11];
println!("{} {}", hello, world)
}
从上面的 case 介绍了字符
、字符串
、字符串``切片
,它们分别属于不同的数据类型「char、String、&str
」。char
是一个 uncode 码点,占用四个字节。String
是一个可变大小的结构体,用于字符的拼接、替换等操作,编码后的数据存储在堆中。str
是字符串字面值,编译时就知道其内容,最终被直接硬编码到可执行文件中。字符串字面值是不可变的,通常以引用「&str
」的形式出现。
在具体开发中,可以使用 &str
作为函数的入参和出参进行处理。使用 &str
不会对所有权进行转移,且可以转换为 String
。
rust
fn main() {
// "initial contents" 是一个字面值直接编译到可执行文件中, data 类型为 &str
let data = "initial contents";
// copy 一份到 s 上
let mut s1 = data.to_string();
let s2 = String::from("initial contents");
s1.push_str(" * ");
println!("{}", s1);
// 字符串拼接, + 调用了 String 的 add 函数, 底层调用的 push 函数
let s3 = s1 + &s2; // 这里 s1 所有权移交,不能继续使用
println!("{}; {}", s3, s2);
// len 返回占用字节数
println!("{}", String::from("中国").len());
// 由于字符串使用的 utf-8 编码,因此无法直接通过索引来查看对应的字符
for c in String::from("中国").chars() {
print!("{}", c)
}
}
哈希 map
哈希 map 用来存储键值对。通过一个哈希函数来实现映射,决定如何将键值对放入内存中。数组是将相同类型的元素存储在连续的内存中,可通过索引进行访问。map 存储的是相同类型键值对「key, value」,可以直接通过 key 进行索引,快速找到对应的 value。
rust
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("zhang"), 10);
scores.insert(String::from("wang"), 11);
// get
let k = String::from("zhang");
if let Some(v) = scores.get(&k) {
println!("key: {}, value: {}", k, v)
}
//遍历
for (k, v) in &scores {
println!("key: {}, value:{}", k, v)
}
// 覆盖
scores.insert(String::from("zhang"), 20);
//遍历
for (k, v) in &scores {
println!("key: {}, value:{}", k, v)
}
// 键不存在时插入
scores.entry(String::from("zhang")).or_insert(30);
//遍历
for (k, v) in &scores {
println!("key: {}, value:{}", k, v)
}
// 根据旧值更新新值
for (_, v) in &mut scores {
*v += 1;
}
for (k, v) in &scores {
println!("key: {}, value:{}", k, v)
}
}
在 map
中,所有权仍然遵守 rust 的所有权机制。在 map
存储的 k, v
中,如果类型实现了 Copy,insert 时直接 copy 数据,否在将所有权转移到 map
中。当然,你也可以选择存储引用,但是必须要保证引用的生命周期至少要和 map
一样久。
rust
fn main() {
let k = String::from("k");
let mut m = HashMap::new();
// 所有权移交到 m
m.insert(k, 0);
// println!("k: {}, v: {}", k, 0) ^ value borrowed here after move
println!("{:?}", m);
let mut m: HashMap<&String, i32> = HashMap::new();
{
let k = String::from("k");
// m.insert(&k, 0); ^^ borrowed value does not live long enough
// k 被回收
}
println!("{:?}", m)
}
fn m() {
let mut scores = HashMap::new();
scores.insert(String::from("zhang"), 10);
scores.insert(String::from("wang"), 11);
// get
let k = String::from("zhang");
if let Some(v) = scores.get(&k) {
println!("key: {}, value: {}", k, v)
}
//遍历
for (k, v) in &scores {
println!("key: {}, value:{}", k, v)
}
// 覆盖
scores.insert(String::from("zhang"), 20);
//遍历
for (k, v) in &scores {
println!("key: {}, value:{}", k, v)
}
// 键不存在时插入
scores.entry(String::from("zhang")).or_insert(30);
//遍历
for (k, v) in &scores {
println!("key: {}, value:{}", k, v)
}
// 根据旧值更新新值
for (_, v) in &mut scores {
*v += 1;
}
for (k, v) in &scores {
println!("key: {}, value:{}", k, v)
}
}