5 使用结构体组织相关数据

struct 是一个自定义的数据类型,允许你命名和包装多个相关的值,从而形成一个更有意义的组合

5.1 定义并实例化结构体

同元组一样,结构体中每个成员的数据类型可以是不同,但是结构体命名更加的清楚和明确,数据的名字和类型称为字段

结构体的定义:

struct User {

username :String,

email:String,

sign_in_count:u64,

active:bool,

}

结构体的实例化:

let user1 = User {

email: String::from("someone@example.com"),

username: String::from("someusername123"),

active:true,

sign_in_count:1,

}

改变User 实例email字段值,注意;整个结构体必须是可变的

let mut user1 = User {

email: String::from("someone@example.com"),

username: String::from("someusername123"),

active:true,

sign_in_count:1,

};

user1.email=String::from("anotheremail@example.com");

在函数中使用结构体

fn build_user (email:String,username:String) -> User{

User{

username :String,

email:String,

sign_in_count:true,

active:1,

}

}

变量与字段同命时字段初始化简写语法

fn build_user (email:String,username:String) -> User{

User{

username,

email,

sign_in_count:true,

active:1,

}

}

结构体更新

let user2 = User{

active:user1.active,

username:user1.username,

sign_in_count:user1.sign_in_count,

email:String::from("anotherone@example.com")

}//不使用更新语法

let user2 = User {

email:String::from("anotherone@example.com"),

..user1

}//使用更新语法..user1

注意,解构体更新语法类似 = 的赋值,因为它移动了数据,这个概念我们在前面变量与数据的交换方式:移动和克隆中讲过,知识点有模糊的朋友看前面啊

元组结构体

元组结构体没有具体的字段名,只有字段的类型

定义元组结构体,如下结构体尽管字段类型相同,但是结构体类型业不同

struct Color (i32,i32,i32);

struct Point(i32,i32,i32);

let black = Color(0,0,0);

let origin = Point(0,0,0);

类单元结构体

类单元结构体中没有任何字段,类似于(),即“元组类型”一节 中提到的unit类型

类单元结构体常常在你想要某个类型上实现trait但不需要在类型中存储数据的时候发挥作用

struct AlwaysEqual;

let subject = AlwaysEqual;

结构体数据的所有权

我们现在使用了自身拥有所有权的String类型而不是&str字符串slice类型

5.2 一个使用结构体的示例程序

我们来写一个计算长方形面积的小程序

fn main() {

let width1 = 30;

let heigth1 = 50;

println!("The area of the retangle is {} square pixels.",

area(width1,heigth1)

);

}

fn area (width:u32,heigth:u32) -> u32{

width*heigth

}

长方形的长和宽是有关系的,我们来重构一下,我们将两个变量放入了一个元组

fn main() {

let rect1 = (30,50);

println!("The area if rectangle is {} square pixels",

area(rect1)

);

}

fn area (dimensions:(u32,u32)) -> u32{

dimensions.0*dimensions.1

}

虽然使用元组可以将元素关联起来,但是我们仍然无法判断它们到底代表什么,我接下来用结构体重构,赋予他们更多意义

struct Rectangle {

width:u32,

height:u32,

}

fn main() {

let rect1 = Rectangle{width:30,height:50};

println!("The area if rectangle is {} square pixels",

area(&rect1)

);

}

fn area (rectangle:&Rectangle) -> u32{

rectangle.width*rectangle.height

}

参数形式的不同,处理其的函数形式也要跟着变化

通过派生trait来增加其他功能;增加属性来派生 Debug trait,并使用调试格式打印 Rectangle 实例

#[derive(Debug)]

struct Rectangle {

width:u32,

height:u32,

}

fn main() {

let rect1 = Rectangle{width:30,height:50};

println!("The rect1 is: {:?}",rect1);//Rectangle { width: 30, height: 50 }

}

另一种使用debug格式打印数值的方法是使用dbg!宏。dbg!宏接收一个表达式的所有权,打印出你代码中dbg!宏调用的文件或行号,以及该表达式的结果值,并返回该值所有权.调用dbg!宏会打印到标准错误控制台流(stderr)和(stdout)

#[derive(Debug)]

struct Rectangle {

width: u32,

height: u32,

}

fn main() {

let scale = 2;

let rect1 = Rectangle {

width: dbg!(30 * scale),

height: 50,

};

dbg!(&rect1);

}

除了Debug trait,Rust还为我们提供了很多可以通过derive属性来使用的trait,他们可以为我们的自定义类型增加实用的行为

5.3 方法语法

area函数其实非常特殊,只能用来计算长方形的面积,让我们来重构代码,将area函数协调进Rectangle类型定义阿area方法中

方法与函数类似,使用fn关键字声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。不过方法和函数是不同的,方法在结构体上下文中被定义,第一个参数总是self,它代表调用该方法的结构体实例

定义方法

让我们把之前的area函数改为定义于Rectangle结构体上的area方法

#[derive(Debug)]

struct Rectangle {

width: u32,

heigth: u32,

}

impl Rectangle {// 我们开始了一个impl块,后面接块名称(Rectangle),因此块中所有内容都与Rectangle类型相关

fn area(&self) -> u32 {//定义了一个方法,参数是&self(也可是self或者可变借用),代替了rectangle: &Rectangle

self.width*self.heigth

}

}

fn main() {

let rect1 = Rectangle {width:30,heigth:50};

println!(

"The area of the rectangle is {} square pixels.",

rect1.area()

);

}

通过使用self作为第一参数来让方法获取实例的所有权是非常少见的,这种做法一般是用方法将self转换成别的示例的时候,这个时候我们需要防止调用者在转换后仍然使用原始实例

使用方法的好处:我们可以将某个类型实例能做的事情都放入impl 块,而不是让将来的用户在我们的库中到处寻找Rectangle的功能

我们在某个类型的块中定义方法,这个方法名可以和类型中的字段相同,如下:

#[derive(Debug)]

struct Rectangle {

width: u32,

heigth: u32,

}

impl Rectangle {

fn width(&self) -> bool {

self.width>0

}

}

fn main() {

let rect1 = Rectangle {

width:30,

heigth:50,

};

if rect1.width() {

println!("The retangle has a nonezero with; it is {}",rect1.width);

}

}

运算符去哪里了

Rust 有一个叫 自动引用和解引用(automatic referencing and dereferencing)的功能

工作原理:当使用 object.something() 调用方法时,Rust 会自动为 object 添加 &、&mut 或 * 以便使 object 与方法签名匹配

p1.distance(&p2);

(&p1).distance(&p2);

带有多个参数的方法

#[derive(Debug)]

struct Rectangle {

width: u32,

height: u32,

}

impl Rectangle {

fn area(&self)-> u32 {

self.width*self.height

}

fn can_hold(&self,other:&Rectangle)->bool {

self.width>other.width && self.height>other.height

}

}

fn main() {

let rect1 = Rectangle { width: 30, height: 50 };

let rect2 = Rectangle { width: 10, height: 40 };

let rect3 = Rectangle { width: 60, height: 45 };

println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));

println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));

}

关联函数

在impl块中定义的函数称为关联函数,因为它们与impl后面的类型相关。String::from函数就是在String类型上定义的函数

关联函数经常被用作返回一个结构体新实例的构造函数

impl Rectangle {

fn square(size: u32) -> Rectangle {

Rectangle { width: size, height: size }

}

}

多个impl块

impl Rectangle {

fn area(&self) -> u32 {

self.width * self.height

}

}

impl Rectangle {

fn can_hold(&self, other: &Rectangle) -> bool {

self.width > other.width && self.height > other.height

}

}

虽然这里没必要编写多个impl语法,但这是有效的语法

总结:结构体让我们可以创建出在我们的领域中有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名它们,这样可以使得代码更加清晰。在 impl 块中,我们可以定义与我们的类型相关联的函数,而方法是一种相关联的函数,让我们指定结构体的实例所具有的行为

但结构体并不是创建自定义类型的唯一方法,下一章,我们将会了解枚举的功能

查看原文