楔子
关于 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 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 会抛出警告,我们可以在没有使用的变量前面加上下划线,来消除警告。
可变变量
变量默认都是不可变的,我们可以在声明的时候加上 mut 关键字让其可变。
#[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::<[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 的时候还要指定泛型的具体类型 impl Fromfor Number { // 在调用 Number::from(xxx) 的时候 // 就会自动执行这里的 from 方法 // 因为实现的是 From ,那么 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 Fromfor Number { fn from(item: u16) -> Self { Number { val: item } } } fn main() { println!("{:?}", Number::from(666)); /* Number { val: 666 } */ // 由于不同的类型都可以实现 From trait // 那么在调用 666u16.into() 的时候,编译器就不知道转成哪种类型 // 因此这里需要显式地进行类型声明 let n: Number = 666u16.into(); println!("{:?}", n); // Number { val: 666 } }
另外里面的 666u16 写成 666 也是可以的。因为调用了 into 方法,Rust 会根据上下文将其推断为 u16。
但如果我们指定了类型,并且类型不是 u16,比如 666u8,那么就不行了。因为 Number 没有实现 From
然后除了 From 和 Into 之外,还有 TryFrom 和 TryInto,它们用于易出错的类型转换,返回值是 Result 类型。我们看一下 TryFrom 的定义:
trait TryFrom
type Error; fn try_from(value: T) -> Result; }
如果简化一下,那么就是这个样子,我们需要实现 try_from 方法,并且要给某个类型起一个别名叫 Error。
// TryFrom 和 TryInto 需要先导入 use std::TryFrom; use std::TryInto; #[derive(Debug)] struct IsAdult { age: u8 } impl TryFromfor IsAdult { type Error = &'static str; fn try_from(item: u8) -> Result { 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 = 20.try_into(); let p4: Result = 15.try_into(); println!("{:?}", p3); println!("{:?}", p4); /* Ok(IsAdult { age: 20 }) Err("未成年") */ }
最后再来介绍一个叫 ToString 的 trait,只要实现了这个 trait,那么便可以调用 to_string 方法转成字符串。因为不管什么类型的对象,我们都希望能将它打印出来。
use std::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 */ }最后来看一下,如何对引用进行解构。首先要注意的是:解引用和解构是两个完全不同的概念。解引用使用的是 *,解构使用的是 &。
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 */ }
最后我们创建引用的时候,除了可以使用 & 之外,还可以使用 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 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 的函数和泛型。
编辑:黄飞
评论
查看更多