Rust 语法梳理与总结(上)
楔子
关于 Rust 的基础知识我们已经介绍一部分了,下面来做一个总结。因为 Rust 是一门难度非常高的语言,在学习完每一个阶段之后,对学过的内容适当总结一下是很有必要的。
那么下面就开始吧,将以前说过的内容再总结一遍,并且在这个过程中还会补充一些之前遗漏的内容。
原生类型
首先是 Rust 的原生类型,原生类型包含标量类型和复合类型。
另外在 Rust 里面,空元组也被称为单元类型。
在声明变量的时候,可以显式地指定类型,举个例子:
fn main(){
let x: i64 = 123;
let y: bool = true;
let z: [u8; 3] = [1, 2, 3];
println!("x = {}", x);
println!("y = {}", y);
println!("z = {:?}", z);
/*
x = 123
y = true
z = [1, 2, 3]
*/
}
另外数字比较特殊,还可以通过后缀指定类型。
fn main(){
// u8 类型
let x = 123u8;
// f64 类型
let y = 3.14f64;
println!("x = {}", x);
println!("y = {}", y);
/*
x = 123
y = 3.14
*/
}
如果没有显式指定类型,也没有后缀,那么整数默认为 i32,浮点数默认为 f64。
fn main(){
// 整数默认为 i32
let x = 123;
// 浮点数默认为 f64
let y = 3.14;
}
最后 Rust 还有一个自动推断功能,会结合上下文推断数值的类型。
fn main(){
// 本来默认 x 为 i32,y 为 f64
let x = 123;
let y = 3.14;
// 但是这里我们将 x, y 组合成元组赋值给了 t
// 而 t 是 (u8, f32),所以 Rust 会结合上下文
// 将 x 推断成 u8,将 y 推断成 f32
let t: (u8, f32) = (x, y);
}
但如果我们在创建 x 和 y 的时候显式地规定了类型,比如将 x 声明为 u16,那么代码就不合法了。因为 t 的第一个元素是 u8,但传递的 x 却是 u16,此时就会报错,举个例子:
Rust 对类型的要求非常严格,即便都是数值,类型不同也不能混用。那么这段代码应该怎么改呢?
fn main(){
let x = 123u16;
let y = 3.14;
let t: (u8, f32) = (x as u8, y);
}
通过 as 关键字,将 x 转成 u8 就没问题了。
然后我们上面创建的整数都是十进制,如果在整数前面加上 0x, 0o, 0b,还可以创建十六进制、八进制、二进制的整数。并且在数字比较多的时候,为了增加可读性,还可以使用下划线进行分隔。
fn main(){
let x = 0xFF;
let y = 0o77;
// 数字较多时,使用下划线分隔
let z = 0b1111_1011;
// 以 4 个数字为一组,这样最符合人类阅读
// 但 Rust 语法则没有此要求,我们可以加上任意数量的下划线
let z = 0b1_1_1_1_______10______1_1;
println!("x = {}, y = {}, z = {}", x, y, z);
/*
x = 255, y = 63, z = 251
*/
}
至于算术运算、位运算等操作,和其它语言都是类似的,这里不再赘述。
元组
再来单独看看元组,元组是一个可以包含各种类型值的组合,使用括号来创建,比如 (T1, T2, ...)
,其中 T1、T2 是每个元素的类型。函数可以使用元组来返回多个值,因为元组可以拥有任意多个值。
Python 的多返回值,本质上也是返回了一个元组。
fn main(){
// t 的类型就是 (i32, f64, u8, f32)
let t = (12, 3.14, 33u8, 2.71f32);
// 当然你也可以这么做
let t: (i32, f64, u8, f32) = (12, 3.14, 33, 2.71);
// 但下面的做法是非法的
// 因为 t 的第一个元素要求是 i32,而我们传递的 u8
/*
let t: (i32, f64) = (12u8, 3.14)
*/
// 应该改成这样
/*
let t: (i32, f64) = (12i32, 3.14)
*/
// 只不过这种做法有点多余,因为 t 已经规定好类型了
// 所以没必要写成 12i32,直接写成 12 就好
}
元组里面的元素个数是固定的,类型也是固定的,但是每个元素之间可以是不同的类型。
fn main(){
// 此时 t 的类型就会被推断为
// ((i32, f64, i32), (i32, u16), i32)
let t = ((1, 2.71, 3), (1, 2u16), 33);
}
然后是元组的打印,有两种方式。
fn main(){
let t = (1, 22, 333);
// 元组打印需要使用 "{:?}"
println!("{:?}", t);
// 或者使用 "{:#?}" 美化打印
println!("{:#?}", t);
/*
(1, 22, 333)
(
1,
22,
333,
)
*/
}
有了元组之后,还可以对其进行解构。
fn main() {
let t = (1, 3.14, 7u16);
// 将 t 里面的元素分别赋值给 x、y、z,这个过程称为元组的结构
// 变量多元赋值也是通过这种方式实现的
let (x, y, z) = t;
println!("x = {}, y = {}, z = {}", x, y, z);
// x = 1, y = 3.14, z = 7
// 当然我们也可以通过索引,单独获取元组的某个元素
// 只不过方式是 t.索引,而不是 t[索引]
let x = t.0;
}
再补充一点,创建元组的时候使用的是小括号,但我们知道小括号也可以起到一个限定优先级的作用。因此当元组只有一个元素的时候,要显式地在第一个元素后面加上一个逗号。
fn main() {
// t1 是一个 i32,因为 (1) 等价于 1
let t1 = (1);
// t2 才是元组,此时 t2 是 (i32,) 类型
let t2 = (1,);
println!("t1 = {}", t1);
println!("t2 = {:?}", t2);
/*
t1 = 1
t2 = (1,)
*/
// 同样的,当指定类型的时候也是如此
// 如果写成 let t3: (i32),则等价于 let t3: i32
let t3: (i32,) = (1,);
}
至于将元组作为函数参数和返回值,也是同样的用法,这里就不赘述了。
数组和切片
数组(array)是一组拥有相同类型 T 的对象的集合,在内存中是连续存储的,所以数组不仅要求长度固定,每个元素类型也必须一样。数组使用中括号来创建,且它们的大小在编译时会被确定。
fn main() {
// 数组的类型被标记为 [T; length]
// 其中 T 为元素类型,length 为数组长度
let arr: [u8; 5] = [1, 2, 3, 4, 5];
println!("{:?}", arr);
/*
[1, 2, 3, 4, 5]
*/
// 不指定类型,可以自动推断出来
// 此时会被推断为 [i32; 5]
let arr = [1, 2, 3, 4, 5];
// Rust 数组的长度也是类型的一部分
// 所以下面的 arr1 和 arr2 是不同的类型
let arr1 = [1, 2, 3]; // [i32; 3] 类型
let arr2 = [1, 2, 3, 4]; // [i32; 4] 类型
// 所以 let arr1: [i32; 4] = [1, 2, 3] 是不合法的
// 因为声明的类型是 [i32; 4],但传递的值的类型是 [i32; 3]
}
如果创建的数组所包含的元素都是相同的,那么有一种简便的创建方式。
fn main() {
// 有 5 个元素,且元素全部为 3
let arr = [3; 5];
println!("{:?}", arr);
/*
[3, 3, 3, 3, 3]
*/
}
然后是元素访问,这个和其它语言一样,也是基于索引。
fn main() {
let arr = [1, 2, 3];
println!("arr[1] = {}", arr[1]);
/*
arr[1] = 2
*/
// 如果想修改数组的元素,那么数组必须可变
// 无论是将新的数组赋值给 arr,还是通过 arr 修改当前数组内部的值
// 都要求数组可变,其它数据结构也是如此
let mut arr = [1, 2, 3];
// 修改当前数组的元素,要求 arr 可变
arr[1] = 222;
println!("arr = {:?}", arr);
/*
arr = [1, 222, 3]
*/
// 将一个新的数组绑定在 arr 上,也要求 arr 可变
arr = [2, 3, 4];
println!("arr = {:?}", arr);
/*
arr = [2, 3, 4]
*/
}
说完了数组,再来说一说切片(slice)。切片允许我们对数组的某一段区间进行引用,而无需引用整个数组。
fn main() {
let arr = [1, 2, 3, 4, 5, 6];
let slice = &arr[2..5];
println!("{:?}", slice); // [3, 4, 5]
println!("{}", slice[1]); // 4
}
我们来画一张图描述一下:
这里必须要区分一下切片和切片引用,首先代码中的 arr[2..5] 是一个切片,由于截取的范围不同,那么切片的长度也不同。所以切片它不能够分配在栈上,因为栈上的数据必须有一个固定的、且在编译期就能确定的大小,而切片的长度不固定,那么大小也不固定,因此它只能分配在堆上。
既然分配在堆上,那么就不能直接使用它,必须要通过引用。在 Rust 中,凡是堆上的数据,都是通过栈上的引用访问的,切片也不例外,而 &a[2..5] 便是切片引用。切片引用是一个宽指针,里面存储的是一个指针和一个长度,因此它不光可以是数组的切片,字符串也是可以的。
可能有人好奇 &arr[2..5] 和 &arr 有什么区别?首先在变量前面加上 & 表示获取它的引用,并且是不可变引用,而加上 &mut,则表示获取可变引用。注意:这里的可变引用中的可变两个字,它指的不是引用本身是否可变,它描述的是能否通过引用去修改指向的值。
因此 &arr 表示对整个数组的引用,&arr[2..5] 表示对数组某个片段的引用。当然如果截取的片段是整个数组,也就是 &arr[..],那么两者是等价的。
然后再来思考一个问题,我们能不能通过切片引用修改底层数组呢?答案是可以的,只是对我们上面那个例子来说不可以。因为上面例子中的数组是不可变的,所以我们需要声明为可变。
fn main() {
// 最终修改的还是数组,因此数组可变是前提
let mut arr = [1, 2, 3, 4, 5, 6];
// 但数组可变还不够,引用也要是可变的
// 注意:只有当变量是可变的,才能拿到它的可变引用
// 因为可变引用的含义就是:允许通过引用修改指向的值
// 但如果变量本身不可变的话,那可变引用还有啥意义呢?
// 因此 Rust 不允许我们获取一个'不可变变量'的可变引用
let slice = &mut arr[2..5];
// 通过引用修改指向的值
slice[0] = 11111;
println!("{:?}", arr);
/*
[1, 2, 11111, 4, 5, 6]
*/
// 变量不可变,那么只能拿到它的不可变引用
// 而变量可变,那么不可变引用和可变引用,均可以获取
// 下面的 slice 就是不可变引用
let slice = &arr[2..5];
// 此时只能获取元素,不能修改元素
// 因为'不可变引用'不支持通过引用去修改值
}
所以要想通过引用去修改值,那么不仅变量可变,还要获取它的可变引用。然后切片引用的类型 &[T],由于数组是 i32 类型,所以这里就是 &[i32]。
fn main() {
let mut arr = [1, 2, 3, 4, 5, 6];
// 切片的不可变引用
let slice: &[i32] = &arr[2..5];
println!("{:?}", slice); // [3, 4, 5]
// 切片的可变引用
let slice: &mut [i32] = &mut arr[2..5];
println!("{:?}", slice); // [3, 4, 5]
// 注意这里的 slice 现在是可变引用,但它本身是不可变的
// 也就是说我们没有办法将一个别的切片引用赋值给它
// slice = &mut arr[2..6],这是不合法的
// 如果想这么做,那么 slice 本身也要是可变的
let mut slice: &mut [i32] = &mut arr[2..5];
println!("{:?}", slice); // [3, 4, 5]
// 此时是允许的
slice = &mut arr[2..6];
println!("{:?}", slice); // [3, 4, 5, 6]
}
以上便是 Rust 的切片,当然我们不会直接使用切片,而是通过切片的引用。
自定义类型
Rust 允许我们通过 struct 和 enum 两个关键字来自定义类型:
struct:定义一个结构体;
enum:定义一个枚举;
而常量可以通过 const 和 static 关键字来创建。
结构体
结构体有 3 种类型,分别是 C 风格结构体、元组结构体、单元结构体。先来看后两种:
// 单元结构体,不带有任何字段,一般用于 trait
struct Unit;
// 元组结构体,相当于给元组起了个名字
struct Color(u8, u8, u8);
fn main() {
// 单元结构体实例
let unit = Unit{};
// 元组结构体实例
// 可以看到元组结构体就相当于给元组起了个名字
let color = Color(255, 255, 137);
println!(
"r = {}, g = {}, b = {}",
color.0, color.1, color.2
); // r = 255, g = 255, b = 137
// 然后是元组结构体的解构,如果是普通的元组,那么直接 let (r, g, b) = ... 即可
// 但元组结构体则不行,不是说它不支持解构,而是在解构的时候要指定结构体名称,像下面这样
let Color(r, g, b) = color;
println!("{} {} {}", r, g, b); // 255 255 137
}
注意最后元组结构体实例的解构,普通元组的类型是 (T, ...)
,所以在解构的时候通过 let (变量, ...)
。但元组结构体是 Color(T, ...)
,所以解构的时候通过 let Color(变量, ...)
。
再来看看 C 风格的结构体。
// C 风格结构体
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
// 结构体也可以嵌套
#[derive(Debug)]
struct Rectangle {
// 矩形左上角和右下角的坐标
top_left: Point,
bottom_right: Point,
}
fn main() {
let p1 = Point { x: 3, y: 5 };
let p2 = Point { x: 6, y: 10 };
// 访问结构体字段,通过 . 操作符
println!("{}", p2.x); // 6
// 以 Debug 方式打印结构体实例
println!("{:?}", p1); // Point { x: 3, y: 5 }
println!("{:?}", p2); // Point { x: 6, y: 10 }
// 基于 Point 实例创建 Rectangle 实例
let rect = Rectangle {top_left: p1, bottom_right: p2};
// 计算矩形的面积
println!(
"area = {}",
(rect.bottom_right.y - rect.top_left.y) * (rect.bottom_right.x - rect.top_left.x)
) // area = 15
}
最后说一下 C 风格结构体的解构:
struct Point {
x: i32,
y: f64,
}
fn main() {
let p = Point { x: 3, y: 5.2 };
// 用两个变量保存 p 的两个成员值,可以这么做
// 我们用到了元组,因为多元赋值本质上就是元组的解构
let (a, b) = (p.x, p.y);
// 或者一个一个赋值也行
let a = p.x;
let b = p.y;
// 结构体也支持解构
// 将 p.x 赋值给变量 a,将 p.y 赋值给变量 b
let Point { x: a, y: b } = p;
println!("a = {}, b = {}", a, b); // a = 3, b = 5.2
// 如果赋值的变量名,和结构体成员的名字相同,那么还可以简写
// 比如这里要赋值的变量也叫 x、y,以下写法和 let Point { x: x, y: y } = p 等价
let Point { x, y } = p;
println!("x = {}, y = {}", x, y); // x = 3, y = 5.2
}
最后,如果结构体实例想改变的话,那么也要声明为 mut。
枚举
enum 关键字允许创建一个从数个不同取值中选其一的枚举类型。
enum Cell {
// 成员可以是单元结构体
NULL,
// 也可以是元组结构体,如果 () 里面没有指定类型,那么就是空的元组结构体
Integer(i64),
Floating(f64),
DaysSales(u32, u32, u32, u32, u32),
// 普通结构体,或者说 C 风格结构体
// 如果 {} 里面没有指定成员和类型,那么就是空的元组结构体
TotalSales {cash: u32, currency: &'static str}
}
fn deal(c: Cell) {
match c {
Cell::NULL => println!("空"),
Cell::Integer(i) => println!("{}", i),
Cell::Floating(f) => println!("{}", f),
Cell::DaysSales(mon, tues, wed, thur, fri) => {
println!("{} {} {} {} {}", mon, tues, wed, thur, fri)
},
Cell::TotalSales { cash, currency } => {
println!("{} {}", cash, currency)
}
}
}
fn main() {
// 枚举的任何一个成员,都是枚举类型
let c1: Cell = Cell::NULL;
let c2: Cell = Cell::Integer(123);
let c3: Cell = Cell::Floating(3.14);
let c4 = Cell::DaysSales(101, 111, 102, 93, 97);
let c5 = Cell::TotalSales {cash: 504, currency: "USD"};
deal(c1); // 空
deal(c2); // 123
deal(c3); // 3.14
deal(c4); // 101 111 102 93 97
deal(c5); // 504 USD
}
所以当你要保存的数据的类型不确定,但属于有限的几个类型之一,那么枚举就特别合适。另外枚举在 Rust 里面占了非常高的地位,像空值处理、错误处理都用到了枚举。然后是起别名,如果某个枚举的名字特别长,那么我们可以给该枚举类型起个别名。当然啦,起别名不仅仅针对枚举,其它类型也是可以的。
enum GetElementByWhat {
Id(String),
Class(String),
Tag(String),
}
fn main() {
// 我们发现这样写起来特别的长
let ele = GetElementByWhat::Id(String::from("submit"));
// 于是可以起个别名
type Element = GetElementByWhat;
let ele = Element::Id(String::from("submit"));
}
给类型起的别名应该遵循驼峰命名法,起完之后就可以当成某个具体的类型来用了。但要注意的是,类型别名并不能提供额外的类型安全,因为别名不是新的类型。除了起别名之外,我们还可以使用 use 关键字直接将枚举成员引入到当前作用域。
enum GetElementByWhat {
Id(String),
Class(String),
Tag(String),
}
fn main() {
// 将 GetElementByWhat 的 Id 成员引入到当前作用域
use GetElementByWhat::Id;
let ele = Id(String::from("submit"));
// 也可以同时引入多个,这种方式和一行一行写是等价的
use GetElementByWhat::{Class, Tag};
// 如果你想全部引入的话,也可以使用通配符
use GetElementByWhat::*;
}
然后 enum 也可以像 C 语言的枚举类型一样使用。
// 这些枚举成员都有隐式的值,Zero 等于 0,one 等于 1,Two 等于 2
enum Number {
Zero,
One,
Two,
}
fn main() {
// 既然是隐式的,就说明不能直接用,需要显式地转化一下
println!("Zero is {}", Number::Zero as i32);
println!("One is {}", Number::One as i32);
/*
Zero is 0
One is 1
*/
let two = Number::Two;
match two {
Number::Zero => println!("Number::Zero"),
Number::One => println!("Number::One"),
Number::Two => println!("Number::Two"),
}
/*
Number::Two
*/
// 也可以转成整数
match two as i32 {
0 => println!("{}", 0),
1 => println!("{}", 1),
2 => println!("{}", 2),
// 虽然我们知道转成整数之后,可能的结果只有 0、1、2 三种
// 但 Rust 不知道,所以还要有一个默认值
_ => unreachable!()
}
/*
2
*/
}
既然枚举成员都有隐式的值,那么可不可以有显式的值呢?答案是可以的。
// 当指定值的时候,值必须是 isize 类型
enum Color {
R = 125,
G = 223,
B,
}
fn main() {
println!("R = {}", Color::R as u8);
println!("G = {}", Color::G as u8);
println!("B = {}", Color::B as u8);
/*
R = 125
G = 223
B = 224
*/
}
枚举的成员 B 没有初始值,那么它默认是上一个成员的值加 1。但需要注意的是,如果想实现具有 C 风格的枚举,那么必须满足枚举里面的成员都是单元结构体。
// 这个枚举是不合法的,需要将 B(u8) 改成 B
enum Color {
R,
G,
B(u8),
}
还是比较简单的。
常量
Rust 的常量,可以在任意作用域声明,包括全局作用域。
// Rust 的常量名应该全部大写
// 并且声明的时候必须提供类型,否则编译错误
const AGE: u16 = 17;
// 注意:下面这种方式不行
// 因为这种方式本质上还是在让 Rust 做推断
// const AGE = 17u16;
fn main() {
// 常量可以同时在全局和函数里面声明,但变量只能在函数里面
const NAME: &str = "komeiji satori";
println!("NAME = {}", NAME);
println!("AGE = {}", AGE);
/*
NAME = komeiji satori
AGE = 17
*/
}
注意:常量接收的必须是在编译期间就能确定、且不变的值,我们不能把一个运行时才能确定的值绑定在常量上。
fn count () -> i32 {
5
}
fn main() {
// 合法,因为 5 是一个编译期间可以确定的常量
const COUNT1: i32 = 5;
// 下面也是合法的,像 3 + 2、4 * 8 这种,虽然涉及到了运算
// 但运算的部分都是常量,在编译期间可以计算出来
// 所以会将 3 + 2 换成 5,将 4 * 8 换成 32
// 这个过程有一个专用术语,叫做常量折叠
const COUNT2: i32 = 3 + 2;
// 但下面不行,count() 是运行时执行的
// 我们不能将它的返回值绑定在常量上
// const COUNT: i32 = count();
// 再比如数组,数组的长度也必须是常量,并且是 usize 类型
const LENGTH: usize = 5;
let arr: [i32; LENGTH] = [1, 2, 3, 4, 5];
// 但如果将 const 换成 let 就不行了
// 因为数组的长度是常量,而 let 声明的是变量
// 因此以下代码不合法
/*
let LENGTH: usize = 5;
let arr: [i32; LENGTH] = [1, 2, 3, 4, 5];
*/
}
另外我们使用 let 可以声明多个同名变量,这在 Rust 里面叫做变量的隐藏。但常量不行,常量的名字必须是唯一的,而且也不能和变量重名。
除了 const,还有一个 static,它声明的是静态变量。但它的生命周期和常量是等价的,都贯穿了程序执行的始终。
// 静态变量在声明时同样要显式指定类型
static AGE: u8 = 17;
// 常量是不可变的,所以它不可以使用 mut 关键字
// 即 const mut xxx 是不合法的,但 static 可以
// 因为 static 声明的是变量,只不过它是静态的
// 存活时间和常量是相同,都和执行的程序共存亡
static mut NAME: &str = "satori";
fn main() {
// 静态变量也可以在函数内部声明和赋值
static ADDRESS: &str = "じれいでん";
println!("AGE = {}", AGE);
println!("ADDRESS = {}", ADDRESS);
/*
AGE = 17
ADDRESS = じれいでん
*/
// 需要注意:静态变量如果声明为可变
// 那么在多线程的情况下可能造成数据竞争
// 因此使用的时候,需要放在 unsafe 块里面
unsafe {
NAME = "koishi";
println!("NAME = {}", NAME);
/*
NAME = koishi
*/
}
}
注意里面用到了 unsafe ,关于啥是 unsafe 我们后续再聊,总之静态变量我们一般很少会声明为可变。
变量绑定
接下来复习一下变量绑定,给变量赋值在 Rust 里面有一个专门的说法:将值绑定到变量上。都是一个意思,我们理解就好。
fn main() {
// 绑定操作通过 let 关键字实现
// 将 u8 类型的 17 绑定在变量 age 上
let age = 17u8;
// 将 age 拷贝给 age2
let age2 = age;
}
如果变量声明了但没有使用,Rust 会抛出警告,我们可以在没有使用的变量前面加上下划线,来消除警告。
可变变量
#[derive(Debug)]
struct Color {
R: u8,
G: u8,
B: u8,
}
fn main() {
let c = Color{R: 155, G: 137, B: 255};
// 变量 c 的前面没有 mut,所以它不可变
// 我们不可以对 c 重新赋值,也不可以修改 c 里的成员值
// 如果想改变,需要使用 let mut 声明
let mut c = Color{R: 155, G: 137, B: 255};
println!("{:?}", c);
/*
Color { R: 155, G: 137, B: 255 }
*/
// 声明为 mut 之后,我们可以对 c 重新赋值
c = Color{R: 255, G: 52, B: 102};
println!("{:?}", c);
/*
Color { R: 255, G: 52, B: 102 }
*/
// 当然修改 c 的某个成员值也是可以的
c.R = 0;
println!("{:?}", c);
/*
Color { R: 0, G: 52, B: 102 }
*/
}
所以要改变变量的值有两种方式:
- 1)给变量赋一个新的值,这是所有变量都支持的,比如
let mut t = (1, 2)
,如果想将第一个元素改成 11,那么t = (11, 2)
即可; - 2)针对元组、数组、结构体等,如果你熟悉 Python 的话,会发现这类似于 Python 里的可变对象。也就是不赋一个新的值,而是对当前已有的值进行修改,比如
let mut t = (1, 2)
,如果想将第一个元素改成 11,那么t.0 = 11
即可。
但不管是将变量的值整体替换掉,还是对已有的值进行修改,本质上都是在改变变量的值。如果想改变,那么变量必须声明为 mut。
作用域和隐藏
绑定的变量都有一个作用域,它被限制只能在一个代码块内存活,其中代码块是一个被大括号包围的语句集合。
fn main() {
// 存活范围是整个 main 函数
let name = "古明地觉";
{
// 新的作用域,里面没有 name 变量
// 那么会从所在的外层作用域中寻找
println!("{}", name); // 古明地觉
// 创建了新的变量
let name = "古明地恋";
let age = 16;
println!("{}", name); // 古明地恋
println!("{}", age); // 16
}
// 再次打印 name
println!("{}", name); // 古明地觉
// 但变量 age 已经不存在了
// 外层作用域创建的变量,内层作用域也可以使用
// 但内层作用域创建的变量,外层作用域不可以使用
}
我们上面创建了两个 name,但它们是在不同的作用域,所以彼此没有关系。但如果在同一个作用域创建两个同名的变量,那么后一个变量会将前一个变量隐藏掉。
fn main() {
let mut name = "古明地觉";
println!("{}", name); // 古明地觉
// 这里的 name 前面没有 let
// 相当于变量的重新赋值,因此值的类型要和之前一样
// 并且 name 必须可变
name = "古明地恋";
println!("{}", name); // "古明地恋"
let num = 123;
println!("{}", num); // 123
// 重新声明 num,上一个 num 会被隐藏掉
// 并且两个 num 没有关系,是否可变、类型都可以自由指定
let mut num = 345u16;
println!("{}", num); // 345
}
变量的隐藏算是现代静态语言中的一个比较独特的特性了。
另外变量声明的时候可以同时赋初始值,但将声明和赋值分为两步也是可以的。
fn main() {
let name;
{
// 当前作用域没有 name,那么绑定的就是外层的 name
name = "古明地觉"
}
println!("{}", name); // 古明地觉
// 注意:光看 name = "古明地觉" 这行代码的话,容易给人一种错觉,认为 name 是可变的
// 但其实不是的,我们只是将声明和赋值分成了两步而已
// 如果再赋一次值的话就会报错了,因为我们修改了一个不可变的变量
// name = "古明地恋"; // 不合法,因为修改了不可变的变量
}
如果变量声明之后没有赋初始值,那么该变量就是一个未初始化的变量。而 Rust 不允许使用未初始化的变量,因为会产生未定义行为。
原生类型的转换
接下来是类型转换,首先 Rust 不提供原生类型之间的隐式转换,如果想转换,那么必须使用 as 关键字显式转换。
fn main() {
let pi = 3.14f32;
// 下面的语句是不合法的,因为类型不同
// let int: u8 = pi
// Rust 不支持隐式转换,但可以使用 as
let int: u8 = pi as u8;
// 转换之后会被截断
println!("{} {}", pi, int); // 3.14 3
// 整数也可以转成 char 类型
let char = 97 as char;
println!("{}", char); // a
// 但是整数在转化的时候要注意溢出的问题
// 以及无符号和有符号的问题
let num = -10;
// u8 无法容纳负数,那么转成 u8 的结果就是
// 2 的 8 次方 + num
println!("{}", num as u8); // 246
let num = -300;
// -300 + 256 = -44,但 -44 还小于 0
// 那么继续加,-44 + 256 = 212
println!("{}", num as u8); // 212
// 转成 u16 就是 2 的 16 次方 + num
println!("{}", num as u16); // 65526
// 以上有符号和无符号,然后是溢出的问题
let num = 300u16;
println!("{}", num as u8); // 44
// 转成 u8 相当于只看最后 8 位
// 那么 num as u8 就等价于
println!("{}", num & 0xFF); // 44
}
as 关键字只允许原生类型之间的转换,如果你想把包含 4 个元素的 u8 数组转成一个 u32 整数,那么 as 就不允许了。尽管在逻辑上这是成立的,但 Rust 觉得不安全,如果你非要转的话,那么需要使用 Rust 提供的一种更高级的转换,并且还要使用 unsafe。
fn main() {
// 转成二进制的话就是
// arr[0] -> 00000001
// arr[1] -> 00000010
// arr[2] -> 00000011
// arr[3] -> 00000100
let arr: [u8; 4] = [1, 2, 3, 4];
// 4 个 u8 可以看成是一个 u32
// 由于 Rust 采用的是小端存储,所以转成整数就是
let num = 0b00000100_00000011_00000010_00000001;
println!("{}", num);
// 我们也可以使用 Rust 提供的更高级的类型转换
unsafe {
println!("{}", std::mem::transmute::<[u8; 4], u32>(arr))
}
/*
67305985
67305985
*/
}
可以看到结果和我们想的是一样的。然后关于 unsafe 这一块暂时无需关注,包括里面那行复杂的类型转换暂时也不用管,我们会在后续解释它们,目前只需要知道有这么个东西即可。
自定义类型的转换
看完了原生类型的转换,再来看看自定义类型,也就是结构体和枚举。针对于自定义类型的转换,Rust 是基于 trait 实现的,在 Rust 里面有一个叫 From 的 trait,它内部定义了一个 from 方法。因此如果类型 T 实现 From trait,那么通过 T::from
便可以基于其它类型的值生成自己。
#[derive(Debug)]
struct Number {
val: i32
}
// From 定义了一个泛型 T,因此在实现 From 的时候还要指定泛型的具体类型
// 所以这里就是为结构体 Number 实现 From<i32> trait
impl From<i32> for Number {
// 在调用 Number::from(xxx) 的时候,就会自动执行这里的 from 方法
// 因为实现的是 From<i32>,那么 xxx 也必须是 i32
// 再注意一下这里的 Self,它表示的是当前的结构体类型
// 但显然我们写成 Number 也是可以的,不过更建议写成 Self
fn from(item: i32) -> Self {
Number { val: item }
}
}
fn main() {
println!("{:?}", Number::from(666));
/*
Number { val: 666 }
*/
// 再比如 String::from,首先 String 也是个结构体
// 显然它实现了 From<&str>
println!("{}", String::from("你好"));
/*
你好
*/
}
既然有 From,那么就有 Into,Into 相当于是把 From 给倒过来了。并且当你实现了 From,那么自动就获得了 Into。
#[derive(Debug)]
struct Number {
val: u16
}
impl From<u16> for Number {
fn from(item: u16) -> Self {
Number { val: item }
}
}
fn main() {
println!("{:?}", Number::from(666));
/*
Number { val: 666 }
*/
// 由于不同的类型都可以实现 From<u16> trait
// 那么在调用 666u16.into() 的时候,编译器就不知道转成哪种类型
// 因此这里需要显式地进行类型声明
let n: Number = 666u16.into();
println!("{:?}", n); // Number { val: 666 }
}
另外里面的 666u16 写成 666 也是可以的。因为调用了 into 方法,Rust 会根据上下文将其推断为 u16。但如果我们指定了类型,并且类型不是 u16,比如 666u8,那么就不行了。因为 Number 没有实现 From<u8>
,它实现的是 From<u16>
,除非我们再单独实现一个 From<u8>
。
然后除了 From 和 Into 之外,还有 TryFrom 和 TryInto,它们用于易出错的类型转换,返回值是 Result 类型。我们看一下 TryFrom 的定义:
trait TryFrom<T> {
type Error;
fn try_from(value: T) -> Result<Self, Self::Error>;
}
如果简化一下,那么就是这个样子,我们需要实现 try_from 方法,并且要给某个类型起一个别名叫 Error。
// TryFrom 和 TryInto 需要先导入
use std::convert::TryFrom;
use std::convert::TryInto;
#[derive(Debug)]
struct IsAdult {
age: u8
}
impl TryFrom<u8> for IsAdult {
type Error = &'static str;
fn try_from(item: u8) -> Result<Self, Self::Error> {
if item >= 18 {
Ok(IsAdult{age: item})
} else {
Err("未成年")
}
}
}
fn main() {
let p1 = IsAdult::try_from(18);
let p2 = IsAdult::try_from(17);
println!("{:?}", p1);
println!("{:?}", p2);
/*
Ok(IsAdult { age: 18 })
Err("未成年")
*/
// 实现了 TryFrom 也自动实现了 TryInto
let p3: Result<IsAdult, &'static str> = 20.try_into();
let p4: Result<IsAdult, &'static str> = 15.try_into();
println!("{:?}", p3);
println!("{:?}", p4);
/*
Ok(IsAdult { age: 20 })
Err("未成年")
*/
}
最后再来介绍一个叫 ToString 的 trait,只要实现了这个 trait,那么便可以调用 to_string 方法转成字符串。因为不管什么类型的对象,我们都希望能将它打印出来。
use std::string::ToString;
struct IsAdult {
age: u8
}
// ToString 不带泛型参数,只有一个 to_string 方法,我们实现它即可
impl ToString for IsAdult {
fn to_string(&self) -> String {
format!("age = {}", self.age)
}
}
fn main() {
let p = IsAdult{age: 18};
println!("{}", p.to_string());
/*
age = 18
*/
}
但很明显,对于当前这个例子来说,即使我们不实现 trait、只是单纯地实现一个方法也是可以的。
流程控制
任何一门编程语言都会包含流程控制,在 Rust 里面有 if/else, for, while, loop 等等,让我们来看一看它们的用法。
if / else
Rust 的 if / else 和其它语言类似,但 Rust 的布尔判断条件不必使用小括号包裹,且每个条件后面都跟着一个代码块。并且 if / else 是一个表达式,所有分支都必须返回相同的类型。
fn degree(age: u8) -> String {
if age > 90 {
// &str 也实现了 ToString trait
"A".to_string()
} else if age > 80 {
"B".to_string()
} else if age > 60 {
"C".to_string()
} else {
"D".to_string()
}
// if 表达式的每一个分支都要返回相同的类型
// 然后执行的某个分支的返回值会作为整个 if 表达式的值
}
fn main() {
println!("{}", degree(87));
println!("{}", degree(97));
println!("{}", degree(57));
/*
B
A
D
*/
}
Rust 没有提供三元运算符,因为在 Rust 里面 if 是一个表达式,那么它可以轻松地实现三元运算符。
fn main() {
let number = 107;
let normailize = if number > 100 {100}
else if number < 0 {0} else {number};
println!("{}", normailize); // 100
}
以上就是 Rust 的 if / else。
loop 循环
Rust 提供了 loop,不需要条件,表示无限循环。想要跳出的话,需要在循环内部使用 break。
fn main() {
let mut count = 0;
loop {
count += 1;
if count == 3 {
// countinue 后面加不加分号均可
continue;
}
println!("count = {}", count);
if count == 5 {
println!("ok, that's enough");
break;
}
}
/*
count = 1
count = 2
count = 4
count = 5
ok, that's enough
*/
}
最后 loop 循环有一个比较强大的功能,就是在使用 break 跳出循环的时候,break 后面的值会作为整个 loop 循环的返回值。
fn main() {
let mut count = 0;
let result = loop {
count += 1;
if count == 3 {
continue;
}
if count == 5 {
break 1234567;
}
};
println!("result = {}", result);
/*
result = 1234567
*/
}
这个特性还是很有意思的。
然后 loop 循环还支持打标签,可以更方便地跳出循环。
fn main() {
let mut count = 0;
// break 和 continue 针对的都是当前所在的循环
// 加上标签的话,即可作用指定的循环
let word = 'outer: loop {
println!("进入外层循环");
if count == 1 {
// 这里的 break 等价于 break 'outer
println!("跳出外层循环");
break "嘿嘿,结束了";
}
'inner: loop {
println!("进入内层循环");
count += 1;
// 这里如果只写 continue
// 那么等价于 continue 'inner
continue 'outer;
};
};
/*
进入外层循环
进入内层循环
进入外层循环
跳出外层循环
*/
println!("{}", word);
/*
嘿嘿,结束了
*/
}
注意一下标签,和生命周期一样,必须以一个单引号开头。
for 循环
while 循环和其它语言类似,这里不赘述了,直接来看 for 循环。for 循环遍历的一般都是迭代器,而创建迭代器最简单的办法就是使用区间标记,比如 a..b,会生成从 a 到 b(不包含 b)、步长为 1 的一系列值。
fn main() {
let mut sum = 0;
for i in 1..101 {
sum += i;
}
println!("{}", sum); // 5050
sum = 0;
// 如果是 ..=,那么表示包含结尾
for i in 1..=100 {
sum += i;
}
println!("{}", sum); // 5050
}
然后再来说一说迭代器,for 循环在遍历集合的时候,会自动调用集合的某个方法,将其转换为迭代器,然后再遍历,这一点和 Python 是比较相似的。那么都有哪些方法,调用之后可以得到集合的迭代器呢?
首先是 iter 方法,在遍历的时候会得到元素的引用,这样集合在遍历结束之后仍可以使用。
fn main() {
let names = vec![
"satori".to_string(),
"koishi".to_string(),
"marisa".to_string(),
];
// names 是分配在堆上的,如果遍历的是 names,那么遍历结束之后 names 就不能再用了
// 因为在遍历的时候,所有权就已经发生转移了,所以我们需要遍历 names.iter()
// 而 names.iter() 获取的是 names 的引用,那么在遍历的时候,拿到的也是每个元素的引用
for name in names.iter() {
println!("{}", name);
}
/*
satori
koishi
marisa
*/
println!("{:?}", names);
/*
["satori", "koishi", "marisa"]
*/
}
循环结束之后,依旧可以使用 names。
然后是 into_iter 方法,此方法会转移所有权,它和遍历 names 是等价的。
我们看到在遍历 names 的时候,会隐式地调用 names.into_iter()。如果后续不再使用 names,那么可以调用此方法,让 names 将自身的所有权交出去。当然啦,我们也可以直接遍历 names,两者是等价的。
最后是 iter_mut 方法,它和 iter 是类似的,只不过拿到的是可变引用。
fn main() {
let mut numbers = vec![1, 2, 3];
// numbers.iter() 获取的是 numbers 的引用(不可变引用)
// 然后遍历得到的也是每个元素的引用(同样是不可变引用)
// numbers.iter_mut() 获取的是 numbers 的可变引用
// 然后遍历得到的也是每个元素的可变引用
// 既然拿到的是可变引用,那么 numbers 必须要声明为 mut
for number in numbers.iter_mut() {
// 这里的 number 就是 &mut i32
// 修改引用指向的值
*number *= 2;
}
// 可以看到 numbers 变了
println!("{:?}", numbers); // [2, 4, 6]
}
以上就是创建迭代器的几种方式,最后再补充一点,迭代器还可以调用一个 enumerate 方法,能够将索引也一块返回。
fn main() {
let mut names = vec![
"satori".to_string(),
"koishi".to_string(),
"marisa".to_string(),
];
for (index, name) in names.iter_mut().enumerate() {
name.push_str(&format!(", 我是索引 {}", index));
}
println!("{:#?}", names);
/*
[
"satori, 我是索引 0",
"koishi, 我是索引 1",
"marisa, 我是索引 2",
]
*/
}
调用 enumerate 方法之后,会将遍历出来的值封装成一个元组,其中第一个元素是索引。
match 匹配
Rust 通过 match 关键字来提供模式匹配,和 C 语言的 switch 用法类似。会执行第一个匹配上的分支,并且所有可能的值必须都要覆盖。
fn main() {
let number = 20;
match number {
// 匹配单个值
1 => println!("number = 1"),
// 匹配多个值
2 | 5 | 6 | 7 | 10 => {
println!("number in [2, 5, 6, 7, 10]")
},
// 匹配一个区间范围
11..=19 => println!("11 <= number <= 19"),
// match 要求分支必须覆盖所有可能出现的情况
// 但明显数字是无穷的,于是我们可以使用下划线代表默认分支
_ => println!("other number")
}
/*
other number
*/
let flag = true;
match flag {
true => println!("flag is true"),
false => println!("flag is false"),
// true 和 false 已经包含了所有可能出现的情况
// 因此下面的默认分支是多余的,但可以有
_ => println!("unreachable")
}
/*
flag is true
*/
}
对于数值和布尔值,我们更多用的是 if。然后 match 也可以处理更加复杂的结构,比如元组:
fn main() {
let t = (1, 2, 3);
match t {
(0, y, z) => {
println!("第一个元素为 0,第二个元素为 {}\
,第三个元素为 {}", y, z);
},
// 使用 .. 可以忽略部分选项,但 .. 只能出现一次
// (x, ..) 只关心第一个元素
// (.., x) 只关心最后一个元素
// (x, .., y) 只关心第一个和最后一个元素
// (x, .., y, z) 只关心第一个和最后两个元素
// (..) 所有元素都不关心,此时效果等价于默认分支
(1, ..) => {
println!("第一个元素为 1,其它元素不关心")
},
(..) => {
println!("所有元素都不关心")
},
_ => {
// 由于 (..) 分支的存在,默认分支永远不可能执行
println!("默认分支")
}
}
/*
第一个元素为 1,其它元素不关心
*/
}
然后是枚举:
fn main() {
enum Color {
RGB(u32, u32, u32),
HSV(u32, u32, u32),
HSL(u32, u32, u32),
}
let color = Color::RGB(122, 45, 203);
match color {
Color::RGB(r, g, b) => {
println!("r = {}, g = {}, b = {}", r, g, b);
},
Color::HSV(h, s, v) => {
println!("h = {}, s = {}, v = {}", h, s, v);
},
Color::HSL(h, s, l) => {
println!("h = {}, s = {}, l = {}", h, s, l);
}
}
/*
r = 122, g = 45, b = 203
*/
}
接下来是结构体:
fn main() {
struct Point {
x: (u32, u32),
y: u32
}
let p = Point{x: (1, 2), y: 5};
// 之前说过,可以使用下面这种方式解构
// let Point { x, y } = p
// 对于使用 match 来说,也是如此
match p {
Point { x, y } => {
println!("p.x = {:?}, p.y = {}", x, y);
}
// 如果不关心某些成员的话,那么也可以使用 ..
// 比如 Point {x, ..},表示你不关心 y
}
/*
p.x = (1, 2), p.y = 5
*/
// let Point { x, y } = p 等价于 let Point { x: x, y: y } = p
// 表示新创建的变量 x、y,并赋上对应成员的值,只是在变量名和结构体成员名相同的时候,可以简写
// 在 match 表达式中也是如此,比如我们将 p.x 赋值给变量 a
match p {
Point { x: a, y } => {
println!("p.x = {:?}, p.y = {}", a, y);
/*
p.x = (1, 2), p.y = 5
*/
}
}
}
最后来看一下,如何对引用进行解构。首先要注意的是:解引用和解构是两个完全不同的概念。解引用使用的是 *,解构使用的是 &。
fn main() {
let mut num = 123;
// 获取一个 i32 的引用
let refer = &mut num;
// refer 是一个引用,可以通过 *refer 解引用
// 并且在打印的时候,refer 和 *refer 是等价的
println!("refer = {}, *refer = {}", refer, *refer);
/*
refer = 123, *refer = 123
*/
// 也可以修改引用指向的值
// refer 引用的是 num,那么要想修改的话
// num 必须可变,refer 也必须是 num 的可变引用
*refer = 1234;
println!("num = {}", num);
/*
num = 1234
*/
// 字符串也是同理
let mut name = "komeiji".to_string();
let refer = &mut name;
// 修改字符串,将首字母大写
*refer = "Komeiji".to_string();
println!("{}", name); // Komeiji
}
以上便是解引用,再来看看引用的解构。
fn main() {
let num = 123;
let refer = #
match refer {
// 如果用 &val 这个模式去匹配 refer,相当于做了这样的比较
// 因为 refer 是 &i32,而模式是 &val,那么相当于将 refer 引用的值拷贝给了 val
&val => {
println!("refer 引用的值 = {}", val)
} // 如果 refer 是可变引用,那么这里的模式就应该是 &mut val
};
/*
refer 引用的值 = 123
*/
// 如果不想使用 &,那么就要在匹配的时候解引用
match *refer {
val => {
println!("refer 引用的值 = {}", val)
}
};
/*
refer 引用的值 = 123
*/
}
补充:如果对一个引用进行解引用,那么引用指向的值必须是可 Copy 的。
对引用进行解引用的时候,会将值拷贝一份,于是值必须是可 Copy 的。如果不可 Copy,那么只能转移所有权,因为 Rust 默认不会拷贝堆上数据,但这会导致原有的变量不可用,因此 Rust 要求值必须是可 Copy 的。当然,如果不涉及解引用,只是普通的变量赋值,那么是会转移所有权的。
最后我们创建引用的时候,除了可以使用 & 之外,还可以使用 ref 关键字。
fn main() {
let num = 123;
// let refer = # 可以写成如下
let ref refer = num;
println!("{} {} {}", refer, *refer, num);
/*
123 123 123
*/
// 引用和具体的值在打印上是没有区别的,但从结构上来说,两者却有很大区别
// 比如我们可以对 refer 解引用,但不能对 num 解引用
// 创建可变引用
let mut num = 345;
{
let ref mut refer = num;
*refer = *refer + 1;
println!("{} {}", refer, *refer);
/*
346 346
*/
}
println!("{}", num); // 346
// 然后模式匹配也可以使用 ref
let num = 567;
match num {
// 此时我们应该把 ref refer 看成是一个整体
// 所以 ref refer 整体是一个 i32
// 那么 refer 是啥呢?显然是 &i32
ref refer => println!("{} {}", refer, *refer),
}
/*
567 567
*/
let mut num = 678;
match num {
// 显然 refer 就是 &mut i32
ref mut refer => {
*refer = 789;
}
}
println!("{}", num); // 789
}
以上就是 match 匹配,但是在引用这一块,需要多体会一下。
另外在使用 match 的时候,还可以搭配卫语句,用于过滤分支,举个例子:
fn match_tuple(t: (i32, i32)) {
match t {
// (x, y) 已经包含了所有的情况
// 但我们又给它加了一个限制条件,就是两个元素必须相等
(x, y) if x == y => {
println!("t[0] == t[1]")
},
(x, y) if x > y => {
println!("t[0] > t[1]")
},
// 此时就不需要卫语句了,该分支的 x 一定小于 y
// 并且这里加上卫语句反而会报错,因为加上之后
// Rust 无法判断分支是否覆盖了所有的情况
// 所以必须有 (x, y) 或者默认分支进行兜底
(x, y) => {
println!("t[0] < t[1]")
},
}
}
fn main() {
match_tuple((1, 2));
match_tuple((1, 1));
match_tuple((3, 1));
/*
t[0] < t[1]
t[0] == t[1]
t[0] > t[1]
*/
}
总的来说,卫语句用不用都是可以的,我们完全可以写成 (x, y),匹配上之后在分支里面做判断。最后 match 还有一个绑定的概念,看个例子:
fn main() {
let num = 520;
match num {
// 该分支一定可以匹配上,匹配之后会将 num 赋值给 n
n => {
if n == 520 {
println!("{} 代表 ❥(^_-)", n)
} else {
println!("意义不明的数字")
}
}
}
/*
520 代表 ❥(^_-)
*/
// 我们可以将 520 这个分支单独拿出来
match num {
// 匹配完之后,会自动将 520 绑定在 n 上面
n @ 520 => println!("{} 代表 ❥(^_-)", n),
n => println!("意义不明的数字")
}
/*
520 代表 ❥(^_-)
*/
// 当然啦,我们还可以使用卫语句
match num {
n if n == 520 => println!("{} 代表 ❥(^_-)", n),
n => println!("意义不明的数字")
}
/*
520 代表 ❥(^_-)
*/
}
这几个功能彼此之间都是很相似的,用哪个都可以。
if let
在一些简单的场景下,使用match 其实并不优雅,举个例子。
fn main() {
let num = Some(777);
match num {
Some(n) => println!("{}", n),
// 因为 match 要覆盖所有情况,所以这一行必须要有
// 但如果我们不关心默认情况的话,那么就有点多余了
_ => ()
}
/*
777
*/
// 所以当我们只关心一种情况,其它情况忽略的话,那么使用 if let 会更加简洁
// 可能有人觉得写成 if Some(i) == num 可不可以,答案是不行,因为 i 没有定义
// 而 if let Some(i) = num,是判断 Some(i) 能否匹配上 num
// 如果可以,就将 Some 里面的 777 赋值给变量 i
if let Some(i) = num {
println!("{}", i);
}
/*
777
*/
// 当然 if let 也支持 else if let 和 else
let score = 78;
if let x @ 90..=100 = score {
println!("你的分数 {} 属于 A 级", x)
} else if let x @ 80..=89 = score {
println!("你的分数 {} 属于 B 级", x)
} else if let 60..=79 = score {
println!("你的分数 {} 属于 C 级", score)
}
/*
你的分数 78 属于 C 级
*/
// 显然对于当前这种情况就不适合用 if let 了,此时应该使用 match 或者普通的 if 语句
// 总之:match 一般用来处理枚举,如果不是枚举,那么用普通的 if else 就好
// 如果只关注枚举的一种情况,那么使用 if let
}
注意:if let 也可以搭配 else if 语句。
小结
以上我们就回顾了一下 Rust 的基础知识,包括原生类型、自定义类型、变量绑定、类型系统、类型转换、流程控制。下一篇文章我们来回顾 Rust 的函数和泛型。
如果觉得文章对您有所帮助,可以请囊中羞涩的作者喝杯柠檬水,万分感谢,愿每一个来到这里的人都生活愉快,幸福美满。
微信赞赏
支付宝赞赏