Rust 语法学习

一般 Struct

定义

  • 和 C 语言一样,是一种自定义的数据类型
  • 和 C 语言一样,为相关联的值命名,打包成有意义的组合

建立

  • 使用 struct 关键字,并为整个 struct 命名。
  • 在花括号内,为所有字段(Filed)定义名称和类型
1
2
3
4
5
6
struct User {
username:String,
email:String,
sign_in_count:u64,
active:bool,
}

实例化

  • 想要使用 struct, 需要创建 struct 的实例;
  1. 为每个字段指定具体值
  2. 无需按声明的顺序指定
1
2
3
4
5
6
let user1 = User {
email: String::from("acb@123.com"),
username: String::from("Nikky"),
active: true,
sign_in_count: 556,
};

访问与赋值

  • 使用点标记法
1
2
3
4
5
6
7
8
9
let mut user1 = User { //声明为可变
email: String::from("acb@123.com"),
username: String::from("Nikky"),
active: true,
sign_in_count: 556,
};

username1 = use1.username; //访问
user1.email = String::from("anotheremail@example.com"); //赋值

一旦 struct 的实例是可变的,那么实例中所有的字段都是可变的。

作为函数返回值

1
2
3
4
5
6
7
8
fn build_user(email:String,username:String) -> User {
User{
email:email,
username:username,
active:true,
sign_in_count:1,
} //记住没‘;’,有;就是语句了
}

字段初始化简写

  • 当字段名与字段值对应变量名相同时,就可以使用字段初始化简写的方式:

改造上一个例子:

1
2
3
4
5
6
7
8
fn build_user(email:String,username:String) -> User {
User{
email,
username,
active:true,
sign_in_count:1,
} //记住没‘;’,有;就是语句了
}

struct 更新语法

  • 当你想基于某个 struct 实例来创建一个新实例的时候,可以使用 struct 更新语法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let user2 = User {
email:String::from("another@123.com"),
username:String::from("123"),
active:user1.active,
sign_in_count:user1.sign_in_count,
};

就可以简写为:

let user2 = User {
email:String::from("another@123.com"),
username:String::from("123"),
..user1
};

Tuple struct

定义

  • 可定义类似 tuple 的 struct,叫做 tuple struct.
  1. Tuple struct 整体有名字,其中的元素没有名字
  2. 适用于:想给整个 Tuple 起名,并让它不同于其他 Tuple, 而且又不需要给每个元素起名。

建立

  • 使用 struct 关键字,后面是名字,以及里面元素的类型。

访问与修改

  • 和 Tuple 一样的点标记法。
1
2
3
4
5
6
7
8
struct Color(i32,i32,i32);
struct Point(i32,i32,i32);

let mut black = Color(0,0,0);
let origin = Point(0,0,0);

let black1 = black.1; //访问
black.1 = 1; //修改

Unit-Like Struct

  • 可以定义没有任何字段的 struct,叫做 Unit-Like struct.
  • 适用于需要在某个类型上实现某个 trait,但是在里面又没有想要存储的数据。

tarit 可以理解为接口。

struct 所有权

对于这样的例子:

1
2
3
4
5
6
struct User {
username:String,
email:String,
sign_in_count:u64,
active:bool,
}
  • 这里的字段使用了 String 而不是 &str,表明:
  1. 该 struct 实例拥有其所有的数据。
  2. 只要 struct 实例是有效的,那么里面的字段数据也是有效的。
  • struct 里面可以存放引用,但需要使用生命周期(后续内容)

这样做的目的是保证只要 struct 实例有效,里面的所有数据都是有效的。

struct 方法

  • Rust 没有 -> 引用符号
  • Rust 会自动引用或解引用,所以无脑 . 即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#[derive(Debug)]
struct Rectangle {
width: u32,
length: u32,
};

iml Rectangle {
fn area(&self) -> u32 { //这里可借用,可获得所有权,可可变借用
self.width * self.length
}
}

fn main() {
let rect = Rectangle {
width: 30,
length: 50,
};

println!("{}",rect.area());

println!("{:#?}",rect);
}

struct 关联函数

  • 可以在 impl 块里定义不把 self 作为第一个参数的函数,它们叫关联函数(不是方法)
  • 例如 String::from()

结合引入时的猜数游戏例子,有没有豁然开朗?

1
2
3
4
5
6
7
8
9
10
11
12
13

iml Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
length: size
}
}
}

fn main() {
let s = Rectangle::square(20);
}

枚举

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum IpAddrKind {
v4,
v6,
} //定义

fn main() {
let four = IpAddrKind::v4; //创建
let six = IpAddrKind::v6; //创建


route(four); //传参
route(six); //传参
route(IpAddrKind::v6); //传参
}


fn route(ip_addr:IpAddrKind) {
...
}

将数据附加到枚举的变体中

  • 附加的数据类型可以是任意
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum IpAddrKind {
v4(u8, u8, u8, u8),
v6(String),
}

enum Message {
Quit,
Move {
x:i32,
y:i32,
}, //匿名结构体
Write(String),
ChangeColor(i32, i32, i32),
}

fn main() {
let home = IpAddrKind::V4(127, 0, 0, 1);
let loopback = IpAddrKind::V6(String::from("123"));
}

枚举方法

和 struct 一样。

Option 枚举

  • 标准库中的定义:
1
2
3
4
enum Option<T> {
Some(T),
None,
}
  • 它包含在 Prelude (预导入模块) 中。可直接使用:
1
2
3
Option<T>
Some(T)
None
1
2
3
4
5
6
fn main() {
let some_number = Some(5); //自动推断类型
let some_string = Some("A String"); //自动推断类型

let absent_number: Option<i32> = None; //None 无法帮助推断类型,需要自己手动给出类型。
}

Option 主要是表示别的语言中的 Null, 这种表示避免了很多了 Null 值的错误使用带来的 panic.

match

  • 强大的控制流运算符

  • 普通使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}

fn value_in_cents(coin:Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

fn main() {}

match 的返回值为匹配到分支的返回值。

  • 匹配的分支可以绑定到被匹配对象的部分值
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
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
}

enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}

fn value_in_cents(coin:Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Penny!");
1 //返回值为1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
}
}
}

fn main() {
let c = Coin::Quarter(UsState::Alaska);
println("{}",value_in_cents(c));
}
  • Rust match 匹配必须穷举所有值。
  • 可以用 _ 通配符来替代其余没列出的值。

if let

  • 简单的控制流语句
  • 处理只关心一种匹配而忽略其他匹配的情况。
  • 更少的代码,更少的缩进,更少的模板代码。
  • 放弃了穷举的可能。
  • 可搭配 else.
1
2
3
4
5
if let Some(3) = v { //这里只有一个等号
println("Three!")
} else {
println!("others!")
}

Rust 代码组织

代码组织主要包括

  • 哪些细节可以暴露,哪些细节是私有的。
  • 作用域内哪些名称有效

模块系统:

  • Package(包):Cargo 的特性,让你构建、测试、共享 crate.
  • Crate(单元包):一个模块树,它可产生一个 Library 或可执行文件.
  • Module(模块):use: 让你控制代码的组织、作用域、私有路径.
  • Path(路径):为 struct、function 或 module 等项命名的方式.

Crate的类型

  • binary
  • library

Crate Roof

  • 是源代码文件
  • Rust 编译器从这里开始,组成你的 Crate 的根 Module

一个 Package

  • 包含 1 个 Cargo.toml, 它描述了如何构建这些 Crates.
  • 只能包含 0 - 1 个 Library crate.
  • 可以包含任意数量的 binary crate.
  • 但必须至少包含一个 crate (library 或 binary).

Cargo 的惯例

  • src/main.rs:
    • binary crate 的 crate root
    • crate 名与 package 名相同
  • src/lib.rs:
    • package 包含一个 library crate
    • library crate 的 crate root
    • crate 名与 package 名相同
  • Cargo 把 crate root 文件交给 rustc 来构建 library 或 binary.
  • 一个 Package 可以同时包含 src/main.rs 和 src/lib.rs
    • 一个 binary crate, 一个 library crate.
    • 名称与 package 名相同
  • 一个 Package 可以有多个 binary crate.
    • 文件放在 src/bin 下。
    • 一个文件就是一个 binary crate.

Crate 的作用

  • 将相关功能组合到一个作用域内,便于在项目间进行共享,这样做防止冲突
  • 例如 rand crate, 访问它的功能需要通过它的名字:rand.

定义 module 来控制作用域和私有性

  • 在一个 crate 内,将代码进行分组
  • 增加可读性,易于复用
  • 控制项目(item)私有性
  • 使用 mod 关键字建立 module.
  • module 可以嵌套.
1
2
3
4
5
6
7
8
9
10
11
12
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}

mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}

crate模块

路径

  • 为了在 Rust 的模块中找到某个条目,需要使用路径
  • 路径的两种形式:
    • 绝对路径:从 crate root 开始,使用 crate 名或字面值 crate.
    • 相对路径:从当前模块开始,使用 self,super 或当前模块的标识符。
  • 路径至少由一个标识符组成,标识符之间使用 ::
1
2
3
4
5
6
7
8
9
10
11
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}

pub fn eat_at_restaurant() {
crate::front_of_house::hosting::add_to_waitlist(); //绝对路径

front_of_house::hosting::add_to_waitlist(); //相对路径
}
但这样是无法通过编译的,这是由于 Rust 中条目默认为 private

私有边界

  • 同级模块可以互相调用私有条目
  • 子模块可以调用父模块私有条目
  • 父模块不能调用子模块私有条目

pub 关键字

  • 使用 pub 关键字可以将某些条目标记为公共的。

super 关键字

  • 类似于文件系统中 ..\
1
2
3
4
5
6
7
8
9
10
11
fn serve_order() {}

mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order(); //相对路径
crate::serve_order(); //绝对路径
}

fn cook_order() {}
}

pub struct

  • pub 放在 struct 前
    • struct 变成公共
    • 但 struct 中的字段仍然是私有的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mod back_of_house{
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}

impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}

pub fn eat_at_restaurant() {
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("Wheat"); //正确
meal.seasonal_fruit = String::from("apple"); //报错,权限不足
}

pub enum

  • pub 放在 enum 前:
    • enum 变成公共
    • enum 变体全部变成公共

use

可以使用 use 关键字将路径导入到作用域内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {

}
}
}

use crate::front_of_house::hosting; //绝对引入
use front_of_house::hosting; //相对引入

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}

as 关键字

as 关键字可以为引入路径指定本地的别名

1
2
3
4
5
6
7
8
use std::fmt::Result;
use std::io::Result as IoResult;

fn f1 -> Result {}

fn f2 -> IoResult {}

fn main() {}

pub use

  • 使用 use 将路径导入到作用域内后,该名称在此作用域内是私有的
  • pub use: 重导出
    • 该条目引入作用域。
    • 该条目可以被外部代码引入到它们的作用域。

使用外部包(package)

  1. Cargo.toml 添加依赖的包(package)
  2. use 将特定条目引入作用域

嵌套路径

  • 使用嵌套路径清理大量的 use 语句
  • 如果使用同一个包或模块下多个条目
  • 可使用嵌套路径在同一行内将上述条目进行引入
  • 形式:路径相同的部分::{路径差异的部分}
1
2
3
4
5
6
7
8
9
use std::{cmp::Ordering, io} 
相当于
use std::io;
use std::cmp::Ordering;

use std::io::{self, write};
相当于
use std::io;
use std::io::write;

通配符

  • 使用 * 可以把路径中所有的公共条目都引入到作用域。

use std::collections;
use std::collections::*;
有啥区别?
以创建 HashMap 为例:
let a = collections::HashMap 第一种
let a = HashMap 第二种

将模块内容移动到其他文件

  • 模块定义时,如果模块名后面是 ; 而不是代码块
    • Rust 会从与模块同名的文件中加载内容
    • 模块树的结构不会发生变化

Rust 常用的集合

Vector

定义

Vec,叫做 vector.

  • 由标准库提供
  • 可存储多个值
  • 只能存储相同类型的数据
  • 值在内存中连续存放

创建

  • Vec::new 函数:let v : Vec<i32> = Vec::new();

如果下文可以推断出 Vector 类型,则无需在定义变量时给出类型。

  • 使用初始值创建 Vec<T>, 使用 vec! 宏:let v = vec![1, 2, 3];

更新

  • 向 Vector 添加元素,使用 push 方法:v.push(1);
  • 读取 Vector 元素:
    • 索引:v[2]
    • get 方法:v.get(2)

索引访问越界时会 panic
get访问越界时会返回 None

  • 遍历 Vector 元素:
1
2
3
4
let v = vec![100, 32, 57];
for i in &v {
println("{}", i);
}

使用 enum 来存储多种数据类型

笔者看来这算是一种 trick, 用来打破 Vector 只能存一种类型的问题。

String

注意事项

UTF-8 编码

创建

  • String::new() 函数
  • 使用初始值来创建 String.
    • to_string() 方法, 可用于实现了 Display trait 的类型,包括字符串字面值。
    • String::from() 函数,从字面值创建 String.

更新

  • push_str() 方法: 把一个字符串切片附加到 String.
  • push() 方法 : 把单个字符附加到 String.
  • 拼接字符串: +
  • 格式化字符串: format!:format!("{}-{}-{}", s1, s2, s3);

这种格式化和 println! 的格式化输出一模一样,区别在于 format 返回字符串。
format 不会像 + 那样取得所有权。

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let s1 = String::from("Hello,");
let s2 = String::from("World!");

let s3 = s1 + &s2;
// '+' 相当于调用了 fn add(self,s: &str) -> String 函数

println!("{}", s3); //有效
println!("{}", s2); //有效
println!("{}", s1); //失效

}

字节、标量值、字形簇.这是 Rust 看待三种字符串的方式。

切割

  • 使用 []一个范围 来创建字符串的切片(例子)
  • 必须沿着字符边界切割,否则会 panic.

HashMap

  • HashMap<K,V>
  • 键值对的形式存储数据,一个键值(key)对应一个值(value)

    创建

  • 创建空 HashMap: new 函数
  • 使用 insert 方法添加数据。
  • 在元素类型为 Tuple 的 Vector 上使用 Collect 方法,可以组建一个 HashMap:
    • 要求 Tuple 有两个值:一个作为 K,一个作为 V
    • Collect 方法可以把数据整合成很多种集合类型,包括 HashMap
    • 返回值需要显式指明类型(理由是 Collect 可以返回很多数据结构)
1
2
3
4
5
6
7
8
9
use std::collections::HashMap;

fn main() {
let teams = vec![String::from("Blue"), String::from("Yellow")];
let intial_scores = vec![10, 50];

let scores:HashMap<_,_> = teams.iter().zip(intial_scores.iter()).collect();
//先用 zip 生成 Tuple 的 Vector,之后用 collect 根据显式声明的类型将其转化为相应类型。
}

HashMap 和所有权

  • 对于实现了 Copy trait 的类型(例如 i32),值会被复制到 HashMap 中。
  • 对于拥有所有权的值(例如 String),值会被移动,所有权会转移给 HashMap。
1
2
3
4
5
6
7
8
9
10
11
use std::collections::HashMap;

fn main() {
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");

let mut map = HashMap::new();
map.insert(field_name, field_value);

println!("{}:{}", field_name, field_value); //错误,所有权已经被移动。
}

访问 HashMap 中的值

  • get 方法:
    • 参数:K
    • 返回值:Option<&V>
所以要通过 Match 来匹配值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::collections::HashMap;

fn main() {
let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

let team_name = String::from("Blue");
let score = scores.get(&team_name);

match score {
Some(s) -> println!("{}", s),
None -> println!("team not exist"),
};
}

遍历

for 循环

1
2
3
4
5
6
7
use std::collections::HashMap;

fn main() {
for (k, v) in &scores {
println!("{}: {}", k, v);
}
}

更新

  • 由于 HashMap 要求 K 唯一,所以在 HashMap 已经存在一对<K,V>的情景下,直接再插入<K,M>会替换原有的<K,V>
  • 一般情况下,我们需要保证只有 K 不对呀任何值的情况下,才插入 V。
    • entry 方法: 检查指定的 K 是否对应一个 V
      • 参数为 K
      • 返回 enum Entry: 代表值是否存在。
1
2
3
4
5
6
7
8
9
10
11
use std::collections::HashMap;

fn main() {
let mut scores = HashMap::new();

scores.insert(String::from("Blue"),10);

let e = scores.entry(String::from("Yellow"));
e.or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
}

or_insert 就是枚举中的方法。
如果 K 存在,返回到对应 V 的一个可变引用。
如果 K 不存在,将方法参数作为 K 的新值插进去,返回到这个值的可变引用。

  • 当然,我们也可以基于已有的 V 来更新 V
1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::collections::HashMap;

fn main() {
let text = "hello world wonderful world";

ley mut map = HashMap::new();

for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}

println("{:#?}", map);
}

Hash 函数

  • 通过指定不同的 hasher 来切换到另一个哈希函数。
  • 默认函数为安全性较高的哈希函数,可抵抗 Dos 攻击。

Rust 错误处理

解释说明

  • Rust 的可靠性:错误处理

    • 大部分情况下:在编译时提示错误,并处理。
  • 错误的分类:

    • 可恢复:例如文件未找到,可再次尝试
    • 不可恢复:bug,例如访问的索引超出范围
  • Rust 没有类似异常的机制

    • 可恢复错误:Result<T,E>
    • 不可恢复:panic!

不可恢复错误

  • 当 panic! 宏执行:
    1. 程序会打印一个错误信息
    2. 展开(unwind)、清理调用栈(Stack)
    3. 退出程序

何为展开调用栈?
是 Rust 沿着调用栈往回走,清理每个遇到的函数中的数据。

何为终止调用栈?
是 Rust 不进行清理,立刻停止程序。
内存需要 OS 进行清理。

  • 想让二进制文件更小,把设置从”展开”改为”终止”,在 Cargo.toml 中 profile 部分设置

abort

可恢复错误

Result 枚举
  • 主要依靠 Result 枚举:
1
2
3
4
enum Result<T,E> {
Ok(T),
Err(E),
}
  • T: 操作成功情况下,Ok 变体里返回的数据的类型
  • E:操作失败的情况下,Err 变体里返回的数据的类型

  • 处理 Result 的方式依靠match表达式

1
2
3
4
5
6
7
8
9
10
fn main() {
let f = File::open("hello.txt"); //返回 Result

let f = match f { //枚举 Result
Ok(file) -> file,
Err(error) -> {
painc!("Error opening file {:?}", error);
}
};
}
  • 处理 Result 的方式也可以依靠unwarp表达式
    • 如果 Result 结果是 Ok,返回 Ok 里面的值
    • 如果 Result 结果是 Err,调用 panic!宏
上述例子简写为
1
2
3
fn main() {
let f = File::open("hello.txt").unwarp();
}
传播错误
1、传统形式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");

let mut f = match f {
Ok(file) -> file,
Err(e) -> return Err(e),
};

let mut s = String::new();

match f.read_to_string(&mut s) {
Ok(_) -> Ok(s),
Err(e) -> Err(e),
} //记得不要分号,加了就成为了语句而不是表达式
}
2. 以上也可以用 `?` 语法糖完成(`?`放在Result后面),作用是:
  • 如果 Result 是 Ok: Ok 中的值就是表达式的结果,然后继续执行程序。
  • 如果 Result 是 Err:Err 就是整个函数的返回值,就像使用了 Return。
1
2
3
4
5
6
7
8
9
10
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}

?语法糖会自动将错误类型转换为函数返回值需要的错误类型,实质上调用了 from 函数

3. 更进一步的链式调用
1
2
3
4
5
6
7
8
9
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}