菜鸡前端的Rust学习笔记(五)—使用结构体来结构化数据

1,486 阅读13分钟

5. 使用Structs来结构化相关数据

写在前面

结构体数据类似面向对象中的对象,将多个有意义的字段且相关的数据聚合在一起。本章将对比元组和结构体,我们将示范,如何定义和使用结构体,将定义一系列有关联的函数(特别是其中一些相关联的函数,称为方法),来说明其与一个结构体类型之间的关系。另外,结构体和枚举值可用于创建在我们代码中一个新的类型,这样便于在Rust编译过程中进行类型检验。

5.1 定义和使用结构体的例子

5.1.1 定义和简单使用

对比元组:相比较元组,元组的每一项的类型确实可以不同,但是你并不能很直观的明白每一项类型对应数据背后的意义,而结构可以让你给每一个数据命名,这样你就可以很明白每一个数据它实际表示的意义。

定义方式:使用struct关键字,之后跟上对应的类型的名称,之后通过{}来规定结构体中拥有参数的类型,其实和typescript中的interface定义有点像,只不过是把interface 改成了struct

使用姿势:创建对应结构体的实例,其方法是需要指出具体每个参数的值。创建实例的过程:(1)声明结构体的名称(2)在大括号中通过key: value键值对的方式来定义已经存在与struct声明中的key并填充符合其类型的值(3)我们创建结构体后我们只关心这个名称的值想存在这个结构体中,而不关心其存储的顺序

参数使用:如果我们需要从结构体中获取相关的值,我们可以使用.语法,改变解构体的参数主要需要有两步:(1)设置对应的结构体为mut类型(2)使用.语法来将对应的结构体重新赋值

注意点:如果我们需要改变一个解构体中的某一项,那么他一定是整个结构体是可变的,Rust不允许我们只让其中几项是可变的,而整个结构体是不可变的,这个和JS有很大的不一样

// 结构体的定义
struct User{
    active: bool,
    username: String,
    age: u32,
    email: String
}
​
fn main() {
    // 但是比较奇怪的是
    // 这里构造的User这个对象,但是里面的attr竟然编辑器里面
    // 没有代码提示
    // 结构体的使用
    let mut user = User {
        active: true,
        username: String::from("Tom"),
        email: String::from("tom@163.com"),
        age: 18
    };
    
    user.email = String::from("loveJerry@163.com");
​
    // 获取解构体中具体的值
    println!("user email -> {}", user.email) // user email -> loveJerry@163.com
}
​

5.1.2 利用函数便捷创建结构体实例

当我们一个结构体中有很多相同字段的值(指的是赋值)使用字段初始化的构造函数的方式,会更加方便。这个语法其实目前和JS中的对象参数的省略写法是相同的。

Rust也支持我们使用类似一个构造函数的方式来创建一个结构体的实例。

(1)这种场景适合我们需要批量创建一些实例,但是其中有很多值其实是默认或者让可以复用的场景,这种场景下是一种更加便捷的方式。

(2)当参数名和需要传入解构体中的参数的名是相同的时候,可以email: email的步骤,直接在结构体中赋值通过email即可

具体的代码如下:

struct User{
    active: bool,
    username: String,
    age: u32,
    email: String,
    sign_in_count: u32
}
​
fn main() {
    let jerry = build_user(String::from("jerry"), 18, String::from("jerry@163.com"));
    println!("jerry name -> {}, jerry email -> {}", jerry.username, jerry.email);
}
​
fn build_user(name: String, age: u32, email: String) -> User {
    User {
        // 使用参数赋值的简写方式
        username,
        email,
        age,
        active: true,
        sign_in_count: 1
    }
}

通过上述的方式,当我们创建一个User解构,我们只需要传入nameageemail字段即可

5.1.3 使用Struct Update语法来从别的结构体创建新的结构体

这个语法其实和JS中的扩展运算符的语法是相同的,这里使用的语法是..(这里是两个点哈~JS里面是3个点),但是他这里写在前面的key他的优先级会更高。

注意点

  1. 除了没有使用到的字段,其他字段都是从jerry借用到Dog的,其等同于=
  2. 因为字段是借用的,所以在使用这种更新语法后,调用jerry.email(所有这些已经借用出去的字段),rust在编译的时候就会报错
struct User{
    active: bool,
    username: String,
    age: u32,
    email: String,
    sign_in_count: u32
}
​
fn main() {
    let jerry = build_user(String::from("jerry"), 18, String::from("jerry@163.com"));
​
    let dog = User {
        // 这里username写在前面,所以username用的是Dog
        // 其他的都是借用jerry的
        username: String::from("Dog"),
        ..jerry
    };
    // 这里会报错,如果打开注释的话
    // 因为这里除了username,其他字段都从jerry -> dog是个move的过程
    // println!("jerry name -> {}, jerry email -> {}", jerry.username, jerry.email);
​
    // dog name -> Dog, dog email -> jerry@163.com
    println!("dog name -> {}, dog email -> {}", dog.username, dog.email);
}
​
fn build_user(username: String, age: u32, email: String) -> User {
    User {
        username,
        email,
        age,
        active: true,
        sign_in_count: 1
    }
}

5.1.4 没有命名的元组解构来创建多种类型

定义:通过元组定义的结构体类型,其称为元组结构。

作用:元组结构由于没有对每个字段进行命名,其一般用来对整个元组类型一个命名,来区分不同的元组类型,通过整个结构体维度,而不是字段维度

定义方法:通过struct关键字、结构体名称(跟在struct关键词后)、元组定义三部分类定义。

使用姿势

【实例化】使用结构名 + ()传参的方式实例化

【获取值】通过.语法,和元组取数一样

【解构方法】需要使用Struct来进行解构,直接看例子把,我感觉我用语言形容不出来

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
​
fn main() {
    let black = Color(0, 0, 0);
    let white = Color(255, 255, 255);
    
    // 需要通过这种方式解构
    let Color(black_r, black_g, black_b) = black;
    
    let origin = Point(0, 0, 0);
    // 也可以使用.关键字来调用对应元组中的值
    let x = origin.0;
    
    println!("r -> {}, g -> {}, b -> {}", black_r, black_g, black_b); // r -> 0, g -> 0, b -> 0
}

5.1.5 没有任何字段的单位结构体

定义:其实讲的就是,一个空的结构体。(我们其实有时候只需要一个类型位的标志,但是其实你说他有值和没值有没有关系,也是也没有,我们知道知道这个类似枚举的类型是这么个东西就行, 我们也不需要去实现他。)

定义方法:使用struct关键字

// 通过;来代表定义结束
struct AlwaysEqual;
​
fn main (){
    let subject = AlwaysEqual;    
}

注意点

Q1: 这里可能有一个问题,我们定义对象中的字段都是String类型,我们用&str为什么不行?

A1: 我们想要结构体的所有者是结构体本身,所以不使用引用类型,因为这样所有用到结构体中的数据,其只要被使用,结构体就不会被GC

Q2: 如果我们一定要存储的是一个引用类型呢?

A2: 那么我们需要给对应的引用类型,声明生命周期,这部分我们现在还没学到。

5.2 使用引用类型的一个例子

目标:实现一个长方形面积计算的功能

5.2.1 使用函数分开传参

我们第一种最直接容易想到的,就是将widthheight作为两个参数传入,这种比较简单,不过多赘述了

fn main() {
    let width = 30;
    let height = 40;
​
    let areaRlt = area(&width, &height);
​
    println!("area -> {}", areaRlt)
}
​
fn area(x: &u32, y: &u32) -> u32 {
    x * y
}
​

5.2.2 使用元组传参

我们将widthheight通过元组封装,然后通过元组解构得到不同的数据来计算面积。

缺点:我们并不清楚,哪个参数是长,哪个参数十款,虽然我们将两个参数整合成了一个参数,但是其参数的意义并不清晰。

fn main() {
    let width = 30;
    let height = 40;
​
    let areaRlt2 = areaTuples((&width, &height));
    println!("area2 -> {}", areaRlt2);
}
​
​
fn areaTuples(dimesion: (&u32, &u32)) -> u32 {
    let (width, height) = dimesion;
    
    width * height
}

5.2.3 重构参数通过结构体,增加参数的意义

由于元组对数据没有特殊的定义,这里我们采用结构体的数据结构来标记数据,代替元组。

我们将长和宽分别标记为widthheight,分别定义为u32类型,通过将widthheight作为Dimension的参数传入

在area函数中接受两个参数,然后通过.语法得到对应的值,这里我们故意使用&Dimension来对应使用创建的Dimension的引用(虽然这里看起来没那么必要,但是如果我们这个参数被很多地方共享的话,如果没有这个&引用,后面的代码会被借用折磨),在函数中我们使用.width.height来取值,相较于元组,肯定是更具有于一的

struct Dimension {
    width: u32,
    height: u32
}
​
fn main() {
    let width = 30;
    let height = 40;
​
    let areaRlt3 = area(&Dimension{
        width,
        height,
    });
    println!("area3 -> {}", areaRlt3)
}
fn area(prop: &Dimension) -> u32 {
    let width = prop.width;
    let height = prop.height;
​
    width * height
}

5.2.4 使用衍生特性增加函数的功能性

这里向我们介绍了衍生特性,derive的用法。

原文中举的例子是,如果我们想要通过println!这个宏打印结构体,目前是不行的。目前会报错

error[E0277]: `Dimension` doesn't implement `std::fmt::Display`
=help: the trait `std::fmt::Display` is not implemented for `Dimension`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with
-Z macro-backtrace for more info)

其意思就是,没有实现Display这个方法,对于系统自带的绝大多数类型来说都实现了这个方法,但是对于结构体来说,其实没有实现Display方法因此展示不出来。然后看后面的help,他其实给出了几个解决办法:使用{:?}/{:#?}来代替Display函数作为输出,这个时候函数会执行Debug特性

但是当我们加上对应的{:?}之后,又出现报错:

help: the trait `Debug` is not implemented for `Dimension`
note: add `#[derive(Debug)]` to `Dimension` or manually `impl Debug for Dimension`

告诉我们,我们目前没有实现Debug,Rust目前功能函数中我们增加#[devrive(Debug)]就可以引入Debug特性

所以最终我们要做的事情就是:(1)增加#[derive(Debug)](2)在打印的时候使用{:?}/{:#?}代替{}(其中{:#?}输出的特性会带有格式)

#[derive(Debug)]
struct Dimension {
    width: u32,
    height: u32
}
​
fn main() {
    let demension = Dimension{
        width,
        height,
    };
    println!("debug -> {:?}", demension)
}
​

在这里,目前官网还告诉我们了一个debug的办法,使用宏dbg!,他也可以直接打出Debug对应的值,但是这个注意的是,dbg!是一个会返回所有者的方法,所以我们可以直接套在表达式内,不会应用我们想要的函数本身的所有者,所有的这类衍生功能都放在了附录中,如果有需要可以进入附录C

5.3 方法语法

方法和函数类似,他们都是通过fn关键词来进行定义的,他也能接受参数和返回值,也可以在任意位置调用。方法不同于函数的地方在于,他们需要定义在结构体的上下文中,另外第一个参数必须是self代表这个实例的引用。

5.3.1 定义方法

定义方法的步骤

  1. 使用impl关键字来将struct的参数定义和对应的实现关联(这里impl和struct后面对应的结构名应该要一样才能关联起来)。
  2. 通过fn关键字来定义方法
  3. 方法的第一个参数为&self来接受实例上下文(必须)
  4. 如果我们想要去改变self中的值,可以使用&mut self
  5. rust并不会自己去实现settergetter方法需要我们手动去写,来实现变量的私有化
#[derive(Debug)]
struct Dimension {
    width: u32,
    height: u32
}
​
impl Dimension {
    fn width(&self) -> bool {
        self.width > 0
    }
​
    fn height(&self) -> bool {
        self.height > 0
    }
​
    fn area(&self) -> u32 {
        let mut area: u32 = 0;
​
        if self.width() && self.height() {
            area = self.width * self.height;
        }
​
        area
    }
}
​
fn main() {
    let width = 30;
    let height = 40;
​
    let demension = Dimension{
        width,
        height,
    };
    println!("area -> {}", demension.area())
}
​

5.3.2 在何处使用->运算符

C/C++语言中,使用.来调用对象的方法,使用->来调用引用对象指针的方法,所以你需要区分到底使用的对象是真的对象,还是对象的引用。

在Rust中我们没有类似->运算符的语法,我们使用.语法即可object.someting()&object.something()就可以,在Rust中&self, &mut selfself调用方法的姿势都是一样的,这样看起来清楚不少,但是我们最好也能清晰的知道这几个东西的区别

5.3.3 更多参数的方法

之前说过了,在结构体的方法中,其实除了self参数外还可以传多个其他参数,这里官网实现了一个can_hold方法来描述,是否一个矩阵可以包括另外一个矩阵,其实就是我们外部调用时传入的参数,对应到实际impl实现内部,其实就是参数索引+1的关系(因为第一个参数是self

#[derive(Debug)]
struct Dimension {
    width: u32,
    height: u32
}
​
impl Dimension {
    fn width(&self) -> bool {
        self.width > 0
    }
​
    fn height(&self) -> bool {
        self.height > 0
    }
​
    fn area(&self) -> u32 {
        let mut area: u32 = 0;
​
        if self.width() && self.height() {
            area = self.width * self.height;
        }
​
        area
    }
​
    fn can_hold(&self, dimension: &Dimension) -> bool {
        &self.width > &dimension.width && &self.height > &dimension.height
    }
}
​
fn main() {
    let rect1 = Dimension {
        width: 30,
        height: 50
    };
​
    let rect2 = Dimension {
        width: 20,
        height: 15
    };
​
    let rect3 = Dimension {
        width: 25,
        height: 10
    };
​
    println!("Can rect 1 hold rect2 ? {}", rect1.can_hold(&rect2)); // Can rect 1 hold rect2 ? true
    println!("Can rect 2 hold rect3 ? {}", rect2.can_hold(&rect3)); // Can rect 2 hold rect3 ? false
}
​

5.3.4 结构体中的函数

这里其实原文应该翻译成相关函数,其实讲的就是我们之前的所有方法都是有&self作为第一个参数的,这种是作为结构体方法(即需要实例化后才能调用的),但是类似JS中对象的静态方法的这种,就是不需要实例化即可调用的函数如何定义呢?

  • impl中定义关联函数时,第一个参数不传self
  • 在调用时,使用::语法
  • 一般用来作为构造函数给出
#[derive(Debug)]
struct Dimension {
    width: u32,
    height: u32
}
​
impl Dimension {
    fn width(&self) -> bool {
        self.width > 0
    }
​
    fn height(&self) -> bool {
        self.height > 0
    }
​
    fn area(&self) -> u32 {
        let mut area: u32 = 0;
​
        if self.width() && self.height() {
            area = self.width * self.height;
        }
​
        area
    }
​
    fn can_hold(&self, dimension: &Dimension) -> bool {
        &self.width > &dimension.width && &self.height > &dimension.height
    }
​
    fn square(width: u32) -> Dimension {
        Dimension {
            width,
            height: width
        }
    }
}
​
fn main() {
    // 通过::语法创建除了一个正方形,之后实例调用area方法计算面积
    println!("square area -> {}", Dimension::square(3).area())
}
​

5.3.5 多个impl实现

如果对同一个结构体,我们定义了多个impl会怎么办,其实也不会怎么样,会将多个impl中的内容合并,但是如果存在定义了同名的结构体方法,会报错提示你多个impl中对应的方法被重复定义了