结构体使用结构体来组织关联数据

golang里有同样的类型, 学到这里感觉有点舒服了

struct, 是一个自定义的数据类型, 其中可包含若干个值, 从而形成适合你业务的组合, 比如go的struct(结构体)

定义和实例化

结构体的每一个部分可以是不同的类型, 跟元组不同的是, 结构体需要对每一个数据进行命名, 这是为了定义这个值, 也是为了给这个值声明意义. 因为有了这些名字, 使得结构体比元组更加灵活, 不需要依赖顺序来方式实例中的某一个值, 而是通过其名字.

​struct​​{}​​字段(field)​
    struct User {
name: String, // 用户名
email: String, // 邮箱
age: u64, // 年龄
active: bool, // 活跃状态
} // 结构体 User, 代表用户信息


​{}​​key: value​
    let user1 = User {
name: String::from("user1"),
age: 23,
active: false,
email: String::from("user1@outlook.com")
};


我们也可以在实例化后修改字段的值, 此时这个实例应当是可变的

    let mut user1 = User {
name: String::from("user1"),
age: 23,
active: false,
email: String::from("user1@outlook.com")
}; // mut 可变
user1.age = 24 // 通过 .key 的方式来找到值


也可以将实例作为表达式的返回值

    fn build_user(name: String, email: String) -> User{
User{
name: name,
email: email,
active: false,
age: 18
}
} // 返回结构体 User 的实例


注意, 当变量或者参数名和类型与结构体的字段完全一致时, 可以使用简略的写法

    fn build_user(name: String, email: String) -> User{
let active = false;
User{
name, // name: name, User的字段name与参数name一致
email, // email: email, User的字段email与参数email一致
active, // active: active, User的字段active与变量active一致
age: 18
}
}


我们也可以借用已经存在的实例的某些字段创建新的实例

    let user1 = User {
name: String::from("user1"),
age: 23,
active: false,
email: String::from("user1@outlook.com")
}; // user1
let user2 = User {
name: user1.name, // 借用user1的字段
age: user1.age, // 借用user1的字段
email: String::from("user2@outlook.com"),
active: true
};


如果剩下的字段值都使用老的实例的值, 还可以使用简略的写法

    let user1 = User {
name: String::from("user1"),
age: 23,
active: false,
email: String::from("user1@outlook.com")
}; // user1
let user2 = User {
email: String::from("user2@outlook.com"),
..user1 // name/age/active 都使用user1
};


元组结构体

有时我们想给某个元组定义一个名字, 让这个元组结构可以复用并且与普通元组分开, 此时你可以使用 元组结构体,

元组结构体不同于普通的结构体, 他没有具体的每个字段的名字, 只有字段的类型, 但是整个元组结构体拥有一个名字

    struct Color(i32, i32, i32);  // 元组结构体定义
struct Point(i32, i32, i32); // 同上

let black = Color(0, 0, 0); // 赋值
let origin = Point(0, 0, 0);


上面的 Color 和 Point 虽然都是 i32, 长度为3 的元组, 但是因为不是一个结构体所以无法混用

同时因为没有字段的名字, 想要访问其中某一个值, 可以通过索引来获取

结构体的生命周期

​String​​&str​​生命周期​
    struct User {
name: &str, // 用户名
email: String, // 邮箱
age: u64, // 年龄
active: bool, // 活跃状态
} // 结构体 User, 代表用户信息
error[E0106]: missing lifetime specifier
--> src/main.rs:3:15
|
3 | name: &str, // 用户名
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
2 | struct User<'lifetime> {
3 | name: &'lifetime str, // 用户名
|

error: aborting due to previous error

For more information about this error, try `rustc --explain E0106`.
error: could not compile `t_struct`.


等到之后, 我们会讲到怎样解决这个问题

使用结构体编写示例代码

我们使用之前学的知识, 编写一段代码, 他的功能是求出长方形的面积

fn main() {
let width1 = 30;
let height1 = 50;
println!("area={}", area(width1, height1))
}

fn area(width: u32, height: u32) -> u32{
width * height
}


上面的代码能够完成我们的需求, 但是仔细想, 一个长方形, 长和宽应该是绑定的关系, 如何体现绑定关系呢? 我们将长和宽使用元组绑定到一起

fn main() {
let rect1 = (30, 50);
println!("area={}", area(rect1))
}

fn area(dimensions: (u32, u32)) -> u32{
dimensions.0 * dimensions.1
}


这样就增加了一些结构性. 但是问题出现了, 使用元组的方式, 我们没法知道哪一个是长, 哪一个是宽, 假如说我们需要根据长宽不同进行不同操作, 比如在屏幕中绘制, 那就可能让调用者产生疑问, 不知道参数的意义

于是我们使用结构体来进行代码的编写

fn main() {
let rectange1 = Rectangle{
width: 20,
height: 30
};
println!("area={}", area(&rectange1))
}

struct Rectangle {
width: u32,
height: u32
}
fn area(rectangle: &Rectangle) -> u32{
rectangle.height * rectangle.width
}


​area​

通过派生 trait 增加功能

​println!​
fn main() {
let rectange1 = Rectangle{
width: 20,
height: 30
};
println!("rec = {}", rectange1);
}
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
--> src/main.rs:6:26
|
6 | println!("rec = {}", rectange1);
| ^^^^^^^^^ `Rectangle` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: required by `std::fmt::Display::fmt`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `t_struct`.


​println!​​Display​​Display​​Display​

但是rust给了我们建议, 查看输出, 有一行

note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead


似乎是告诉我们应该这样输出, 于是我们将打印修改为

println!("rec = {:?}", rectange1);


运行后发现还是不行, 但是又给了一个提示

error[E0277]: `Rectangle` doesn't implement `std::fmt::Debug`
--> src/main.rs:6:28
|
6 | println!("rec = {:?}", rectange1);
| ^^^^^^^^^ `Rectangle` cannot be formatted using `{:?}`
|
= help: the trait `std::fmt::Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` or manually implement `std::fmt::Debug`
= note: required by `std::fmt::Debug::fmt`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
  = help: the trait `std::fmt::Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` or manually implement `std::fmt::Debug`


这里告诉我们rust确实有打印, 但是是Debug模式, 需要我们显式的开启, 我们将代码修改为

fn main() {
let rectange1 = Rectangle{
width: 20,
height: 30
};
println!("rec = {:?}", rectange1);
}

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32
}


运行

    Finished dev [unoptimized + debuginfo] target(s) in 0.40s
Running `target/debug/t_struct`
rec = Rectangle { width: 20, height: 30 }


可以打印出结构体数据了

​#[derive(Debug)]​​Rectangle​​Debug​​derive​

方法语法

当我们写python的时候, 总会使用到类/方法, 使用类生成对象, 调用对象的方法, 让这个方法与类有紧密的联系

​struct​​go​

定义方法

fn main() {
let rectange1 = Rectangle{
width: 20,
height: 30
};
println!("area={}", rectange1.area()) // 调用结构体定义的方法
}

struct Rectangle {
width: u32,
height: u32
}

impl Rectangle{ // impl 结构体名称
fn area(&self) -> u32 { // 定义方法 area
self.height * self.width
}
}


​impl​​area​​&self​​&​​&mut self​
​self​​self​
​实例.方法​​&self​
​C/C++​​->​​.​​slef/ &self/ &mut self​

带有更多参数的方法

很多时候调用方法时肯定需要传入更多参数, 这些参数与实例本身并无联系

fn main() {
let rectange1 = Rectangle{
width: 20,
height: 30
};
let rectange2 = Rectangle{
width: 30,
height: 50
};
println!("area={}", rectange1.area());
println!("r2 can_hold r1 = {}", rectange1.can_hold(&rectange2)) // 调用, 额外参数手动指定
}

struct Rectangle {
width: u32,
height: u32
}

impl Rectangle{
fn area(&self) -> u32 {
self.height * self.width
}
fn can_hold(&self, other: &Rectangle) -> bool { // 接受一个额外参数 other 类型是 &Rectangle
self.width > other.width && self.height > other.height
}
}


​self​​self​

关联函数

​impl​​slef​​self​
fn main() {
let sq = Rectangle::square(20); // 通过 :: 调用, 因为不依赖实例, 所以不需要通过实例去调用, 直接使用结构体
}

struct Rectangle {
width: u32,
height: u32
}

impl Rectangle {
fn square(size: u32) -> Rectangle { // 不依赖实例本身
Rectangle { width: size, height: size }
}
}


​::​

多个impl块

每个结构体都允许有多个impl块, 比如


struct Rectangle {
width: u32,
height: u32
}

impl Rectangle {
fn square(size: u32) -> Rectangle { // 不依赖实例本身
Rectangle { width: size, height: size }
}
}

impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool { // 接受一个额外参数 other 类型是 &Rectangle
self.width > other.width && self.height > other.height
}
}


impl Rectangle{
fn area(&self) -> u32 {
self.height * self.width
}
}


这样是可以正常使用的, 但是一般不建议这样做, 因为没有意义, 可能在特殊的需求下有用, 我们之后会说

枚举与模式匹配

枚举(enumerations/ enums)开发者应该都很熟悉, 枚举让你可以通过列举可能的 成员(variants) 来定义一个类型

定义枚举

通过一个场景来理解枚举, 我们知道, IP地址目前主要有两种, IPv4 和 IPv6, 这两个都属于IP, 假设我们的程序有可能会且只会处理这两种IP, 那么我们可以将两个归属为一起, 当代码在处理IP时将其当做一样的来处理, 我们可以使用 枚举 来做.

​IpAddrKind​​v4​​v6​
enum IpAddrKind {
// 枚举名
v4, // ipv4
v6, // ipv6
}


​IpAddrKind​

枚举值

使用定义的枚举

    let four = IpAddrKind::v4;
let six = IpAddrKind::v6;


​::​​IpAddrKind​
enum IpAddrKind {
// 枚举名
v4, // ipv4
v6, // ipv6
}

fn a(ip: IpAddrKind){
}

fn main() {
let four = IpAddrKind::v4;
let six = IpAddrKind::v6;
a(four);
a(six)
}


如果我们想要将IP地址和IP类型形成关联关系, 我们可能优先想到使用结构体

enum IpAddrKind {
// 枚举名
v4, // ipv4
v6, // ipv6
}

struct IpAddr { // ip地址结构体
address: String, // ip
kind: IpAddrKind // 类型
}

fn main() {
let address1 = IpAddr{
kind: IpAddrKind::v4,
address: String::from("123.234.111.222")
};
let address2 = IpAddr{
kind: IpAddrKind::v6,
address: String::from("::1")
};
}


​IpAddrKind​​v4​​v6​​IpAddr​​address​​value​
enum IpAddr {
// 枚举名
v4(String), // ipv4, String类型
v6(String), // ipv6, String类型
}

fn main() {
let address1 = IpAddr::v4(String::from("127.0.0.1"));
let address2 = IpAddr::v6(String::from("::1"));
}


因为IP地址实在是太常见了, 很多时候我们都会用到, 所以Rust内置了数据结构专门存放IP地址, ​​IpAddr in std::net - Rust (rust-lang.org)​​, 内部是这样定义的

struct Ipv4Addr {
// --snip--
}

struct Ipv6Addr {
// --snip--
}

enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}


​value​
​IpAddr​

下面我们再看一个新的枚举

enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}


​Message​​Quit​​Move​​Write​​String​​ChangeColor​

如果我们单纯使用结构体也可以达到效果

struct QuitMessage; // 类单元结构体
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体


问题是, 这样的话就没有将这几个Message形成关联关系, 如果使用枚举, 因为枚举本身是一种类型, 就能将这些Message以成员的方式合到一起

​impl​
impl Message {
fn call(&self) {
// 方法call
}
}

let m = Message::Write(String::from("hello"));
m.call(); // 调用


具体的调用, self方式, 可以参照上方的结构体impl

option枚举

​Option​
​空值​​空值(Null)​

Tony Hoare,null 的发明者,在他 2009 年的演讲 “Null References: The Billion Dollar Mistake” 中曾经说到:

I call it my billion-dollar mistake. At that time, I was designing the first comprehensive type system for references in an object-oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

我称之为我十亿美元的错误。当时,我在为一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过我未能抵抗住引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数十亿美元的苦痛和伤害。

空值的问题主要在于, 当你想像使用非空值一样使用空值, 就会出现某种形式上的错误, 因为空和非空无处不在, 所以很容易出现这种问题

​Option​
enum Option<T> {
Some(T),
None,
}


​Option​​Option​​Some(T)​​None​​
​Option::Some​
let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;


​None​​Option​​None​
​Option​​Option​
fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;
}


会报错

error[E0277]: cannot add `std::option::Option<i8>` to `i8`
--> src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + std::option::Option<i8>`
|
= help: the trait `std::ops::Add<std::option::Option<i8>>` is not implemented for `i8`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `t_enum`.


​i8​​Option​​i8​​Option​​None​
​Option​​T​
​Option
​Option​​T​

match控制流运算符

​Golang​​switch​​Python​​else if​​Rust​​match​​模式​
enum Coin{  // 枚举
Penny,
Nickel,
Dime,
Quarter,
}

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


​match​
​value_in_cents​​match​​match​​coin​​match​
​{}​​match​​Coin::Penny => 1​​Coin::Penny​​=>​
​match​​match​

匹配到的代码如果很短, 通常不使用大括号, 如果有多行代码则需要使用, 例如

fn value_in_cents(coin: Coin) -> u8 {
match coin { // match
Coin::Penny => {
println!("Penny"); // 打印
1 // 返回
},
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}


绑定值的模式

​match​
#[derive(Debug)]  // debug
enum UsState {
Alabama,
Alaska,
}

enum Coin {
Penny,
Nickel,
Dime(u8),
Quarter(UsState),
}


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


​match​​Quarter​
#[derive(Debug)]  // debug
enum UsState {
Alabama,
Alaska,
}

enum Coin {
Penny,
Nickel,
Dime(u8),
Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime(v) => {
println!("{}", v);
8
},
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
},
}
}

fn main(){
let d = Coin::Dime(20);
let q = Coin::Quarter(UsState::Alaska);
let dp = value_in_cents(d);
let qp = value_in_cents(q);
}


​cargo run​
​()​
​Option
​match​​Option
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}

fn main(){
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}


​Optione​​None​​match​​x​​None​

匹配是穷尽的

​match​
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}

fn main(){
let five = Some(5);
let six = plus_one(five);
}
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:2:11
|
2 | match x {
| ^ pattern `None` not covered
|
= help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms

warning: unused variable: `six`
--> src/main.rs:9:9
|
9 | let six = plus_one(five);
| ^^^ help: consider prefixing with an underscore: `_six`
|
= note: `#[warn(unused_variables)]` on by default

error: aborting due to previous error

For more information about this error, try `rustc --explain E0004`.
error: could not compile `t_enum`.


​None​
​_​

其他语言类似的分支处理, 通常会有一个default, 如果都匹配不上, 则会进入default分支, 通常default分支是写在最后的

fn main() {
let some_u8_value = 0;
match some_u8_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => {
println!("kkkkk")
},
}
}


​_​
​some_u8_value​​1/3/5/7​​_​
​_​​_​
​if let​

如果有需求, 如果值为3则进行操作, 其他则不处理

fn main(){
let some_u8_value = Some(0);
match some_u8_value {
Some(3) => println!("three"),
_ => (),
}
}


​match​
​if let​
fn main(){
let some_u8_value = Some(3);
if let Some(3) = some_u8_value {
println!("three");
}
}


​if let​​=​​Some(3)​
{
println!("three");
}


当模式匹配后进行表达式的运行, 不匹配则不运行, 例如

fn main(){
let some_u8_value = Some(1);
if let Some(3) = some_u8_value { // 不运行
println!("three");
}
}


​match​
​if let​​else​​_​
fn main(){
let some_u8_value = Some(1);
if let Some(3) = some_u8_value {
println!("three");
}else{
println!("other") // 打印
}
}


​if let​
fn main() {
let some_u8_value = Some(4);
if let Some(3) = some_u8_value {
println!("three");
}
if let Some(4) = some_u8_value {
println!("four")
} else {
println!("other")
}
}


​match​