学习 Rust

Posted on May 6, 2021

起因

吃饱了撑的。

安装

我使用的是 ArchLinux,直接

1
yay -S rust-nightly cargo-nightly

我这里使用的是 nightly,这个包在 archlinuxcn 下,如果没有添加 archlinuxcn,直接装 rust 也是 ok 的。

其他安装方式可见官网 https://www.rust-lang.org/learn/get-started

Hello World

1
cargo new hello_world

值得注意的是,rust推荐项目名和项目文件夹使用小写下划线的形式,如果使用类似于 HelloWorld 的命名方式,则会抛出警告。

可使用宏 #[allow(non_snake_case)] 消除警告。

变量

rust 中使用 let 声明一个不可变变量,使用 let mut 声明一个可变变量,举个例子:

1
2
3
4
5
6
7
8
9
fn main() {
    let a = 1;
    a = 2; // 编译错误,无法对变量 a 进行二次赋值
    let mut b = 1;
    b = 1; // 编译通过
    let c = 1;
    let c = 2; // 编译通过
    let c = "3"; // 编译通过
}

所谓不可变指的是值不可变,相当于 C++/Java 中 const 或者 final 修饰的变量,但是又和传统的 const/final 不太一样,上述例子中的 6-7 行多次声明了变量 c(也就是多次创建了变量 c ),这种特性称为 “隐藏(shadowing)",当后续访问变量 c 时,得到的结果是字符串的 “3”,前面两个 c 被隐藏了。

如果声明为常量,那么使用 const 代替 let 进行修饰比较好,并且需要指定变量类型。

1
2
3
4
fn main() {
    const A = 1_000; // 编译错误
    const A: i32 = 100_000; // 编译通过
}

数据类型

一般类型

Rust 是静态类型的语言,在编译阶段所有的类型都是必须被求出的。

Rust 中整数以 i 开头,无符号整数以 u 开头。

下表截取自 https://kaisery.github.io/trpl-zh-cn/ch03-02-data-types.html

长度 有符号 无符号
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize
数字字面值 例子
Decimal (十进制) 98_222
Hex (十六进制) 0xff
Octal (八进制) 0o77
Binary (二进制) 0b1111_0000
Byte (单字节字符)(仅限于u8) b'A'

Rust 中自然也有 float 和 double 类型,分别是 f32 和 f64。

值得一提的是,Rust 中的 char 和别的语言中的 char 有些不太一样,Rust 中的 char 长度是 4 个字节,相当于 i32。

复合类型

Rust 有两个复合类型,元组(tuple)和数组(array)。

元组

元组长度固定,无法调整。

1
2
3
4
fn main() {
    let t = (1,2.2,"3");
    let tt: (i32, f64, &str) = (1,2.2,"3");
}

对于元组数据的访问

1
2
3
4
5
6
fn main() {
    let t = (1,2.2,"3");
    println!("{}", t.1) // 使用下标
    let (c, d, e) = t; // 对元组数据进行绑定。
    println!("{}", c);
}

数组

与元组不同,数组中的数据类型必须一致,与元组相同的是,数组也是固定长度的,一旦声明就无法改变

1
2
3
4
5
6
fn main() {
    let x = [1,2,3,4,5,6,7];
    let x: [&str; 3] = ["a", "b", "c"];
    let x = ["666", 3]; // ["666", "666", "666"]
    let x: [&str; 3] = ["666"; 3]; // 同上,显式声明类型。
}

函数

函数以 fn 起始进行声明,函数的参数需要显式的声明类型

1
fn func(i: i32, j: i32) { ... }

对于非空(void)返回的函数,也需要显式声明返回类型

1
fn func(i: i32) -> i32 { ... }

对于函数的返回值,需要先了解 表达式语句 的概念,简单来说就是表达式是有值的,语句是没有值的。

1
2
3
fn func() -> i32 {
    3
}

对于上面那个例子,3是上面函数的返回值,需要说明一下,如果上面例子中的 3 后面接了一个分号,那将编译失败,因为 3; 是一个语句,而不是一个表达式。

在 Rust 中,函数的最后一句代码为返回值,如果需要提前返回,可以使用 return 语句。

1
2
3
4
5
6
7
fn func(i: i32) -> i32 {
    if i == 1 {
        return 1;
    } else {
        return 2;
    }
}

控制流

对于简单 if else 就不表了,Rust 中,条件判断语句是 强 bool 的,也就是说 if condition 中必须为 bool 类型,他不像其他语言那样可以自动把非 bool 转为 bool。

Rust 中,if 语句块可以是一个表达式:

1
2
3
4
fn func(i: i32) -> i32 {
    let x = if i == 1 { return 1; } else { return i; };
    let x = return if i == 1 { 1 } else { i }; // 等效上面
}

循环

loop

loop 是一个无限循环,有意思的是 Rust 的 break 语句可以像 return 一样有返回值,引用一个官方的例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn main() {
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;
        }
    };
    println!("The result is {}", result);
}

while

1
2
3
4
5
6
7
fn func() {
    let mut i = 0;
    while i < 5 {
        println!("{}", i);
        i = i + 1
    }
}

for

引用官方例子

1
2
3
4
5
6
7
8
9
fn main() {
    let a = [10, 20, 30, 40, 50];
    for element in a.iter() {
        println!("the value is: {}", element);
    }
    for number in (1..4).rev() { // 反向
        println!("{}!", number);
    }
}

所有权

待续

https://kaisery.github.io/trpl-zh-cn/ch04-01-what-is-ownership.html

结构体

定义结构体

Rust 中的结构体与 C/C++ 中结构体类似,注意 字段 结尾是逗号而不是分号。

1
2
3
4
struct Stu {
    sid: u32,
    name: String,
}

声明结构体

1
2
3
4
let stu = Stu {
    sid: 1,
    name: String::from("X"),
};

从函数中获取结构体实例

1
2
3
4
5
6
fn build_stu(sid: u32) -> Stu {
    Stu {
        sid, // 也可写为 sid: sid,
        name: String::from("A"),
    }
}

使用结构体更新语法从其他实例创建实例

1
2
3
4
5
6
7
8
let s1 = Stu {
    sid: 1,
    name: String::from("s1"),
};
let s2 = Stu {
    sid: 2,
    ..s1
};

修改结构体内容

修改结构体内容需要将结构体声明为 mutable 的

1
2
3
4
5
let mut stu = Stu {
    sid: 1,
    name: String::from("X"),
};
stu.sid = 2;

元组结构体

元组结构体与结构体类似,但是没有字段名,只有类型

1
2
struct Stu(i32, String);
let stu = Stu(1, String::from("X"));

修改元组结构体只需要像修改元组一样去写就行了。

结构体方法

Rust 中定义方法(函数)是在一个 impl 包裹的块中实现的,impl 块可以有多个。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#[derive(Debug)]
struct Stu {
    sid: u32,
    name: String,
}
impl Stu {
    fn out(&self) {
        print!("Sid:{}, Name:{}", self.sid, self.name);
    }
    fn from(sid: u32, name: String) -> Stu {
        Stu{
            sid,
            name,
        }
    }
}
fn main() {
    let s3 = Stu::from(666, String::from("XX"));
    s3.out();
    dbg!(s3);
}

其中,没有 &self 作为参数的函数/方法称为关联函数,和其他语言中用 static 修饰的函数差不多。

枚举

一般枚举

与 C/C++ 中类似

1
2
3
4
enum Sex {
    female,
    male,
}

引用一个官方教程的例子,原代码:

1
2
3
4
5
6
7
8
enum IpAddrKind {
    V4,
    V6,
}
struct IpAddr {
    kind: IpAddrKind,
    address: String,
}

简化:

1
2
3
4
enum IpAddr {
    V4(String),
    V6(String),
}

使得枚举值和数据类型绑定,可以少写结构体。

这个数据类型可以是基本数据类型,元组,结构体,甚至是枚举。

枚举中也是可以实现方法的,与结构体类似。

标准库的 Option 枚举

待续….

模式匹配 match

类似于其他语言的 switch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let i = 1;
match i {
    1 => println!("{}", 1),
    2 => println!("{}", 2),
    3 => println!("{}", 3),
    4 | 5 | 6 | 7 => (), // 组合匹配
    8 ..= 77 => (), // 范围匹配
    // 8 ... 77 => (), // deprecated.
    _ => println!("other cases"),
}

常见集合

vector

vector<T> 是一个动态数组。

声明 vector

1
2
let mut v = Vec::new();
let mut v: Vec<i32> = Vec::new();

在第一行中,没有显式的指定类型,但是在第一次插入(push)之后,Rust 就可以推断出内容的类型,不显式的指明类型也是可以的。

也可以使用自带的宏声明一个 vector

1
2
let mut v = vec![1,2,3,4];
v.push(666);

读取 vector 中的元素

1
2
3
4
let mut v = vec![1, 2, 3, 4];
let x = &v[1];
let c = v.get(1);
for i in &v { ... }

使用 [] 直接访问下标,如果越界,那么会 panic,如果使用 get 进行访问,可以看到 c 的类型是 Option<&i32>,也就是说,越界了会返回 None,而不会造成程序崩溃。

修改 vector 中的元素

1
2
3
4
let mut v = vec![1, 2, 3, 4];
v[0] = 777;
let mut x = &mut v[1];
*x = 666;

官方教程中有一个非常有趣的例子,使用枚举配合vector达到一个可以存储任意类型的动态数组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

String

Rust 中的字符串指的是 String 和 String 的 slice &str

1
2
3
let s = "str"; // &str
let s = s.to_string(); // String
let s = String::from("str"); // String

拼接字符串

由于一些奇怪的习惯,我觉得 Rust 的一些代码有些反直觉

1
2
3
4
5
6
let s1 = "s1".to_string();
let s2 = "s2".to_string();
let s3 = s1 + s2; // Error
let s3 = &s1 + &s2; // Error
let s3 = s1 + &s2; // Ok
println!("{}{}{}", &s1, &s2, &s3); // Error s1 所有权被移动后再次被借用

我只想拼接字符串 s3 = s1 + s2 怎么办呢,拼接结束后 s1s2s3 都要是可用的,那可以使用 format! 宏。

1
2
3
4
let s1 = "s1".to_string();
let s2 = "s2".to_string();
let s3 = format!("{}{}", s1, s2);
let s3 = format!("{}{}", &s1, &s2);

字符串索引

由于一些原因,Rust 不支持字符串索引,具体可以阅读 https://kaisery.github.io/trpl-zh-cn/ch08-02-strings.html

遍历字符串

你可以对字符串进行字符遍历(Rust 中的 char),或者单字节遍历(其他语言中的char,1个字节)。

1
2
3
let s1 = "s123456789".to_string();
for i in s1.bytes() { println!("{}", i); }
for i in s1.chars() { println!("{}", i); }

HashMap

创建 HashMap

和 vector 一样,在声明的时候可以显式声明其类型或者待插入的时候等编译器自动推断。

1
2
let mut hash: HashMap<i32, &String> = HashMap::new();
let mut hash: = HashMap::new();

插入 HashMap

如果在把一个 String 插入到 HashMap 中。而 HashMap 的声明是 HashMap<i32, String> 的话,那么这个 String 的所有权就被转到了 HashMap 上,也就是说,你插入的 String 你就不能再用了!(淦!)

1
2
3
4
let mut hash = HashMap::new();
let zero = "0".to_string();
hash.insert(0, zero);
dbg!(zero); // Error

如果插入之后还想再使用字符串,那可以插入引用

1
2
3
4
let mut hash = HashMap::new();
let zero = "0".to_string();
hash.insert(0, &zero);
dbg!(zero); // OK

但是插入的引用属于被借用了,就不可以对这个 zero 变量进行修改。

查询

1
2
3
4
5
6
7
let mut hash = HashMap::new();
let zero = "0".to_string();
hash.insert(0, zero);
dbg!(hash[&0]); // Error
dbg!(&hash[&0]); // Ok
println!("{}", hash[&0]); // Ok
println!("{}", &hash[&0]); // Ok

修改

1
2
3
let mut hash = HashMap::new();
hash.insert("0".to_string(), "0".to_string());
*hash.get_mut("0").unwrap() = "666".to_string();

泛型

函数中的泛型

1
fn func<T>(arg: T) -> T { ... }

结构体中的泛型

1
2
3
4
5
struct Stu<T, R> {
    sid: T,
    name: R,
}
let stu = Stu { sid: 1, name: "666".to_string(), };

结构体方法的泛型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct Stu<T, R> {
    sid: T,
    name: R,
}
impl Stu<i32, String> {
    fn out(&self) {
        println!("sid:{}, name:{}", self.sid, self.name);
    }
}
impl<T, R> Stu<T, R> {
    fn out(&self) {
        println!("default method");
    }
}

let stu = Stu { sid: 1, name: "666".to_string(), };

上例中,针对 sid 类型为 i32 并且 name 类型为 String 进行了 “特化”,如果 sidname 是其他类型的话,就无法执行 out 函数

枚举中的泛型

与结构体差不多

trait

trait 和 interface 差不多

1
2
3
trait Out {
    fn disp();
}

来个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
fn main() {
    let stu = Stu { sid: 1, name: "stu".to_string() };
    let tea = Tea { tid: 2, name: "tea".to_string() };
    stu.disp();
    tea.disp();
}

trait Out {
    fn disp(&self);
}

struct Stu {
    sid: i32,
    name: String,
}

impl Out for Stu {
    fn disp(&self) {
        println!("STU---sid:{} name:{}", self.sid, self.name);
    }
}

struct Tea {
    tid: i32,
    name: String,
}

impl Out for Tea {
    fn disp(&self) {
        println!("TEA---tid:{} name:{}", self.tid, self.name);
    }
}

当然了, trait 也可以有自己的默认行为

1
2
3
4
5
trait Out {
    fn disp() {
        println!("{}", 666);
    }
}

trait 作为参数

主要是可以将实现了某 trait 的结构体作为参数,传入函数中,相当于一个约束的作用

1
2
3
4
5
// 接上面的代码,略
// ------------------------------------- //
fn global_out(x: impl Out) {
    x.disp();
}

意思就是 x 是一个参数,一个实现(impl)了 Out 这个 trait 的一个变量。

trait bound

trait bound 就是对 trait 做出限制,上面的 trait 作为参数,是 trait bound 的一个语法子集

1
2
3
fn global_out<T: Out>(x: T) {
    x.disp();
}

这段代码和上面那段是等效的,T: Out 指明了 T 的约束,如果需要多个约束呢?

1
2
3
fn global_out<T: Out + OOO>(x: T) {
    x.disp();
}

+ 连接即可。

简化 trait bound

1
2
3
4
fn global_out<T, R>(x: T) 
where T: Out + OOO,
      R: Out + XXX
{ ... }

trait 返回值

1
2
3
4
5
6
7
fn ret(i: bool) -> Out {
    if i {
        Stu { sid: 1, name: "stu".to_string() };
    } else {
        Tea { tid: 2, name: "tea".to_string() };
    }
}

这样是不行的,因为函数返回 trait 需要确定的返回类型,虽然 StuTea 都实现了 Out 。但是他们的类型并不一致。

生命周期