定义并介绍结构体

结构体和我们前面学习的元组类似,结构体中的每一项都可以是不同的数据类型。和元组不同的地方在于,我们需要给结构体的每一项命名。结构体较元组的优势是:我们声明和访问数据项的时候不必使用索引,可以直接使用名字。

声明结构体

我们直接看一个结构体的例子:

struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

结构体使用关键字 struct 开头,紧跟结构体的名字,之后就是大括号包裹的多条结构体数据项,每个数据项由名字和类型组成,我们把每个数据项称为字段。

结构体实例化

我们声明了一个结构体后,如何使用它呢?接下来创建一个结构体的实例:

let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};

可以看到,创建结构体实例 (结构体实例化) 是直接以结构体名字开头,之后就是大括号包裹的键值对。这些键值对顺序和声明结构体的顺序无关,换句话说,声明结构体就是定义一个通用的模版,结构体实例化就是给模版填充值。

结构体数据的存取

创建了结构体实例,那我们应该如何存取实例中的数据呢?比如我们要获取邮箱信息,可以 user1.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");

这个实例整个都是可变的,如果我只想修改 email 和 username 两个字段,而不想修改其它的字体,应该怎么办呢?

修改部分字段

要知道,rust 不允许我们只把部分字段标记为可变。那我们可不可以把这个结构体放在函数中,让函数返回一个新的实例呢?看例子:

fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}

在这个例子中,函数参数名字和结构体字段名字是相 的,如果有很多字段,一个一个地写名字和参数是很无聊的,不过,rust 为我们提供了简写的方式

结构体字段初始化简写

我们直接看例子吧:

fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}

email 和 username 的结构体字段名字和函数传入的参数变量的名字是相同的,我们可以只写一遍。

结构体更新

如果旧的结构体实例中的一部分值修改,使之变成一个新的实例,使用结构体更新语法会更加方便。如果在 user1 的基础上修改 email 和 username 而不改变其他的值,我们通常会这样写:

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

如果我们使用了结构体更新语法,创建新结构体就变成了这样:

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

利用 .. 语法达到了和前面案例相同的结果。

元组结构体

我们也可以定义看起来像元组的结构体,我们称它为元组结构体。元组结构体只定义字段的类型而不定义字段的名字,它主要用于给整个元组一个名字来区分不同的元组。直接看例子:

struct Color(i32, i32, i32);
struct Point(i32, i32, i32); let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

定义了黑色和原点,显然二者的类型不相同,虽然声明的字段类型相同,但是二者是使用不同的元组结构体实例化的。

空结构体

我们也可以定义空结构体,它不包含任何字段。详细内容后文再聊。

写个关于结构体的例子

为了学习什么时候用到结构体,我们来写一个计算长方形面积的程序。我们从使用简单变量写起,一直写到使用结构体为止。

编写项目

我们先来创建一个名叫 rectangles 的项目,然后写一个通过宽高计算面积的函数。

fn main() {
let width1 = 30;
let height1 = 50; println!(
"长方形的面积是 {}。",
area(width1, height1)
);
} fn area(width: u32, height: u32) -> u32 {
width * height
}

我们运行的结果是:

cargo run
Compiling rectangles v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.51s
Running `target/debug/rectangles`
长方形的面积是 1500。

我们调用 area 函数完成了对长方形面积的计算,但是描述长方形的宽高是分开的,我们能不能想个办法,把两个值变成一个值?很容易想到的办法就是元组,对,没错,是元组。

利用元组重构项目

我们直接看重构完成后的代码:

fn main() {
let rect1 = (30, 50); // 定义元组 println!(
"长方形的面积是 {}。",
area(rect1)
);
} fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}

现在只有一个值了,但是又有了一个新的问题:元组没有名字,计算面积还好,元组中的两个值混了也没事,如果是把这个长方形画出来,那就得记着 0 位置是宽,1 位置是高,别人调用我们的代码时,别人也得记着这个顺序。这是很不友好的,那应该怎么解决呢?

利用结构体重构项目

我看来看重构后的代码:

struct Rectangle { // 定义结构体
width: u32,
height: u32,
} fn main() {
let rect1 = Rectangle { width: 30, height: 50 }; // 结构体实例化 println!(
"长方形的面积是 {}。",
area(&rect1)
);
} fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}

现在只有一个参数了,而且参数也有了实际的含义了,似乎完成了我们的目标。但是 area 函数只能计算长方形的面积,我们希望这个函数尽可能地在 Rectangle 结构体内部,因为它不能处理其它的结构体。那我们应该如果做呢?我们可以把该函数转变成 Rectangle 结构体的方法。在此之前,我们先看一个调试程序的小技巧。

打印结构体

在我们调试程序的时候,经常想看一下结构体每个字段的值是什么,如果直接打印结构体会报错,比如:

struct Rectangle {
width: u32,
height: u32,
} fn main() {
let rect1 = Rectangle { width: 30, height: 50 }; println!("rect1 是: {}", rect1); // 报错: `Rectangle` cannot be formatted with the default formatter
}

rust 为我们提供了打印的方法,在结构体定义的上方加入 #[derive(Debug)] 声明,在打印的大括号中加入 :? 就可以了,看例子:

#[derive(Debug)]    // 这里加入声明
struct Rectangle {
width: u32,
height: u32,
} fn main() {
let rect1 = Rectangle { width: 30, height: 50 }; println!("rect1 是: {:?}", rect1); // 这里加入打印的格式
}

运行的结果是:

 cargo run
Compiling rectangles v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/rectangles`
rect1 是: Rectangle { width: 30, height: 50 }

这个结构体的输出很不美观,我们调整一下,让结构体可以结构化输出,只需要把 {:?} 改成 {:#?} 即可,然后输出就变成了:

cargo run
Compiling rectangles v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/rectangles`
rect1 是: Rectangle {
width: 30,
height: 50,
}

后文会详细介绍 derive 声明。

结构体方法

方法和函数类似,都是以关键字 fn 打头,后接方法名、参数和返回值,最后是方法体。方法和函数不同之处在于:方法定义在结构体的上下文中,方法的第一个参数是 self (self 代表结构体方法调用者的实例)。

定义方法

我们来修改 area 方法,把它变成 Rectangle 结构体的方法,如下:

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
} impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
} fn main() {
let rect1 = Rectangle { width: 30, height: 50 }; println!(
"长方形的面积是 {}。",
&rect1.area()
);
}

使用 impl 关键字和结构体名字 Rectangle 来定义 Rectangle 的上下文,然后把 area 函数放进去,就把函数的第一个参数修改成 self,在主函数中,可以让 rect1 实例直接调用 area 方法。

对于方法和第一个参数,我们使用 &self 来代替 rectangle: &Rectangle,因为在 impl Rectangle 声明的上下文中,rust 知道 self 代表的是 Rectangle,但是还需要在 self 前面加上 & 符号,意思是借用 Rectangle 的不可变实例。如果要借用 Rectangle 的可变实例,参数需要写成 &mut self。

使用方法较函数的优势在于:添加方法的时候可以直接使用结构体方法语法,而不必要在每个函数中重复写实例的类型。我们可以把结构体对应的所有方法都写在结构体的上下文中,当我们为别人提供库函数的时候不必要在很多地方寻找需要的函数。

多参方法

我们再写一个例子,第一个长方形能不能装下第二个长方形,如果能装下,就返回 true,否则返回 false,实例代码如下:

#[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!("rect1 装下 rect2? {}", rect1.can_hold(&rect2));
println!("rect1 装下 rect3? {}", rect1.can_hold(&rect3));
}

我们先来看运行结果:

rect1 装下 rect2? true
rect1 装下 rect3? false

我们在 Rectangle 上下文中定义第二个方法 can_hold,该方法借用另一个 Rectangle 实例作参数,我们需要告诉调用者参数的类型。

关联函数

在结构体上下文中也可以定义不含有 self 参数的函数,这种函数被称为关联函数。这里叫函数,不叫方法,因为这些函数不是使用结构体实例调用的,而是使用双冒号调用,比如之前使用的 String::from 就是一个关联函数。

关联函数常常被用于返回结构体实例的构造函数。例如,我们可以提供一个关联函数来生产 Rectangle 结构体的实例。在这里,我们假设宽度是相等的,我们就可以使用一个参数来代替两个参数了,如下:

impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height: size }
}
}

使用双冒号 :: 语法来调用关联函数,举个简单的例子 let sq = Rectangle::square(3);,即 结构体::关联函数

多个 impl 模块

每个结构体都允许使用多个 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
}
}

这种语法是有效的,后文学习中可能会使用到,知道有这种语法就好了。

欢迎阅读单鹏飞的学习笔记

Rust 入门 (五)的更多相关文章

  1. openresty 前端开发入门五之Mysql篇

    openresty 前端开发入门五之Mysql篇 这章主要演示怎么通过lua连接mysql,并根据用户输入的name从mysql获取数据,并返回给用户 操作mysql主要用到了lua-resty-my ...

  2. Rust入门篇 (1)

    Rust入门篇 声明: 本文是在参考 The Rust Programming Language 和 Rust官方教程 中文版 写的. 个人学习用 再PS. 目录这东东果然是必须的... 找个时间生成 ...

  3. Thinkphp入门 五 —模型 (49)

    原文:Thinkphp入门 五 -模型 (49) [数据库操作model模型] model  模型  数据库操作 tp框架主要设计模式:MVC C:controller   控制器   shop/Li ...

  4. DevExpress XtraReports 入门五 创建交叉表报表

    原文:DevExpress XtraReports 入门五 创建交叉表报表 本文只是为了帮助初次接触或是需要DevExpress XtraReports报表的人群使用的,为了帮助更多的人不会像我这样浪 ...

  5. 脑残式网络编程入门(五):每天都在用的Ping命令,它到底是什么?

    本文引用了公众号纯洁的微笑作者奎哥的技术文章,感谢原作者的分享. 1.前言   老于网络编程熟手来说,在测试和部署网络通信应用(比如IM聊天.实时音视频等)时,如果发现网络连接超时,第一时间想到的就是 ...

  6. C#基础入门 五

    C#基础入门 五 递归 递归调用:一个方法直接或间接地调用了它本身,就称为方法的递归调用. 递归方法:在方法体内调用该方法本身. 递归示例 public long Fib(int n) { if(n= ...

  7. Python爬虫入门五之URLError异常处理

    大家好,本节在这里主要说的是URLError还有HTTPError,以及对它们的一些处理. 1.URLError 首先解释下URLError可能产生的原因: 网络无连接,即本机无法上网 连接不到特定的 ...

  8. Python爬虫教程——入门五之URLError异常处理

    大家好,本节在这里主要说的是URLError还有HTTPError,以及对它们的一些处理. 1.URLError 首先解释下URLError可能产生的原因: 网络无连接,即本机无法上网 连接不到特定的 ...

  9. 转 Python爬虫入门五之URLError异常处理

    静觅 » Python爬虫入门五之URLError异常处理 1.URLError 首先解释下URLError可能产生的原因: 网络无连接,即本机无法上网 连接不到特定的服务器 服务器不存在 在代码中, ...

随机推荐

  1. 谁说搞Java的不能玩机器学习?

    简介 机器学习在全球范围内越来越受欢迎和使用. 它已经彻底改变了某些应用程序的构建方式,并且可能会继续成为我们日常生活中一个巨大的(并且正在增加的)部分. 没有什么包装且机器学习并不简单. 它对许多人 ...

  2. ABAP中时间戳的处理

    UTC(UTC, Universal Time Coordinated,通用协调时)时间戳,分为长时间戳和段时间戳,其中长时间戳餐开始的系统的数据元素TIMESTAMPL,类型为DEC(21,7):而 ...

  3. 转:redis-cli 命令总结

    redis-cli常用命令,原文地址:https://maoxian.de/2015/08/1342.html Redis提供了丰富的命令(command)对数据库和各种数据类型进行操作,这些comm ...

  4. Spark安装与学习

    http://www.cnblogs.com/jerrylead/archive/2012/08/13/2636115.html

  5. 从壹开始【NetCore3.0】 46 ║ 授权认证:自定义返回格式

    前言 哈喽大家好,马上就要年末了,距离新的一年,只有50天了,春节是75天. 在这个时节内,天气逐渐变凉,但是大家的心肯定很热吧,因为发生了两件大事: 1.双十一买买买,在这个让人激动又纠结的一天,大 ...

  6. C++STL整理

    STL整理 vector #include<bits/stdc++.h> #define go(i,a,b) for(int i=a;i<b;i++) using namespace ...

  7. 『题解』洛谷P1063 能量项链

    原文地址 Problem Portal Portal1:Luogu Portal2:LibreOJ Portal3:Vijos Description 在\(Mars\)星球上,每个\(Mars\)人 ...

  8. [转载]2.5 UiPath循环活动Do While的介绍和使用

    一.Do While的介绍 先执行循环体, 再判断条件是否满足, 如果满足, 则再次执行循环体, 直到判断条件不满足, 则跳出循环. 二.Do While在UiPath中的使用 1. 打开设计器,在设 ...

  9. Python 基础 装饰器

    今天把学过的装饰器的知识进行回顾一下,说到装饰器,第一反应就是这个东西呢就是用来装逼的,为啥这样说呢,是应为没有这个东西照样可以干活,大部分工作都是可以做的,不管咋样还是把学过的装逼器梳理一下吧. 一 ...

  10. Python基础之JSON

    作用 对Python对象进行序列化,便于存储和传输 Python对象与JSON字符串相互转换 Python对象转JSON字符串 import json data = [ { 'a' : 1, 'b' ...