一、结构体
上一章介绍了Rust中的一些基本语法,本章继续深入学习Rust中的一些高级特性。
首先要介绍的就是结构体,它的作用与上一章介绍的元组类似,可以用来存放一系列有关联、不同类型的数据。
如果你了解C/C++,那基本与C/C++
中的结构体一致,或者与python
中的字典类似。
使用方法如下:
struct Student{
name:String,
age:u32,
sex:char,
}
fn main() {
let s=Student{
name:String::from("yushi-"),
age:100,
sex:'男'
};
println!("{} {} {}", s.name, s.age,s.sex);
}
首先是声明一个结构体,需要用struct
关键字,后面紧跟结构体的名称,这里我给它取名为Student
,因为要用它来保存学生的信息。
struct Student{
name:String,
age:u32,
sex:char,
}
随后就是一对大括号,而大括号中的内容就是我们这个结构体的各种属性。
比如这里我们想要保存一个学生的信息,就有姓名、年龄、性别等等,也就是这里的name
、age
、sex
。
由于这种声明类型rust
的编译器无法为我们自动推断各个属性的类型,所以需要在其后面手动添加类型注解。
比如上面的:String
、:u32
以及:char
,分别代表字符串类型、无符号整数类型、以及字符类型。
而各个属性之间用,
分隔,一般我们习惯于将各个属性分行写,更好看。
这里出现了一个字符串类型String
,前面没有提到过。
之所以没有提到,一是因为它并不是rust中的基本类型,二是因为Rust
中的字符串类型非常复杂与强大,需要后面专门花一章对它介绍才能将其讲清楚,所以这里就暂时先用着,只需要知道它是保存字符串的就行了。
比如"hello world"
这种,用双引号包裹起来的就是字符串,但在Rust
中又有一点不一样,我们后面再讨论。
声明好了这个结构体后,我们就可以像普通的类型那样,用它来定义一个变量:
let s=Student{
name:String::from("yushi-"),
age:100,
sex:'男'
};
与声明这个结构体时的写法类似,只是去除了前面的struct
关键字,然后把大括号中的各个属性值后面的类型注解,更换为你要给它们赋的值就好了!
注意,各个内部属性的赋值顺序是可以任意调换的,比如可以先给age字段赋值,然后再给nama字段赋值,但必须全部赋值。
执行完上面的代码后,s
变量就是一个我们自己的Student
类型的变量了。
注意String
类型中name
属性的赋值方式,需要调用String
里面的from
方法将一个字符串转换为String
。
这就是Rust
中比较独特的方式,虽然看起来比较繁琐,但到后面你明白了它的原理,应该就能理解它这样做的理由了。
如果要访问这个变量中的各个字段,也非常的简单,只需要用.
即可。
println!("{} {} {}", s.name, s.age,s.sex);
比如这里我就用.
的方式访问它内部的三个字段,并进行了输出。
当然了,它默认同样是不可修改的,如果你想要对它进行修改,请使用let mut
。
上面内容是结构体中最常见的用法,但事实上结构体还有其它形式,在一些开源代码中可能会经常看到。
比如你完全可以声明一个空的结构体:
struct S;
虽然此时它内部没有任何属性、看起来没什么用处。
但当你学习了本章后面的“方法”后,就会发现它在某些时候还是有点用处的。
比如某些工具函数,本身并不需要什么属性,但为了方便调用,就可以将其绑定到指定的空结构体上。
除此之外,如果你的结构体字段并不需要“名字”,那么其实你可以像写元组那样写结构体的,就像下面这样:
struct S(u32);
fn main() {
let t = S(10);
println!("{}", t.0);
}
其使用方法和前面提到的元组非常像。
这种用法在一些开源库中用的尤其多,比如在windows
这个crate中,很多源码的结构体都是通过这种方式定义的。
因为像HANDLE
这样的自定义类型实际上就是一个指针的重定义别名,同时又需要为其添加一些方法,所以这样定义非常合适这样的场景。
二、枚举基础
如果你学过其它语言,那么上面的结构体你应该并不陌生,甚至对于这里要说的枚举,也会有一定熟悉的感觉。
枚举最主要的作用是:限制输入选项。
比如IP地址,目前来说最常用的就Ipv4
与IPv6
两种,你不可能再凭空创建一种。
再比如,一周是周一到周日的七天,你也不能凭空再创造一个周八。
但rust
中的枚举相比于其它语言又非常的不同,下面我们来看一看rust
中的枚举是如何使用的:
enum IP{
Ipv4,
Ipv6
}
fn main() {
let mut m:IP; //声明一个IP类型的变量
m=IP::Ipv4; //正确
m=IP::Ipv6; //正确
//m=11; //错误
}
可以看到枚举的定义方式与上面提到的结构体非常相似,只是将struct
关键字改为了enum
(enumeration
的缩写),后面跟着这个枚举类型的名称。
为了更好的体现出rust中枚举的特性,这里以IP为例,IP地址只有两种类型,所以我就在枚举中只写了两个类型:ipv4
与ipv6
。
这个名字是随便写的,不过一般会为其取一些有意义的名字,如果选用一周七天作为一个枚举类型,那其中的就可以写七种类型,分别为周一到周七。
接着,我在main
函数中,声明了一个IP
类型的枚举变量。
然后最重要的一部分就出现了,这个变量将只能等于IP
这个枚举内部所写的类型,如果你等于其它类型的,比如数字11
,就会出错。
取出其内部的枚举变量通过::
的方式,比如这里的IP::ipv4
,就是取出其中的ipv4
。
这有什么用?如上面所说,它的作用就是将一个变量限制到一个范围,便于我们比较、选择。
比如上面提到的IP地址,如果是IPv4地址类型,我就得用点分十进制的方式写ip地址,比如192.168.0.1
。
而如果是ipv6
,那就必须得用十六进制的方式书写地址,比如:fdb8:27c5:221:5400:2c95:a7fc:5d57:a375
。
这两者是很不一样的,并且也不会出现第三种类型,这在函数传参时非常的有用
fn test_ip(t:IP) {
}
如上面这个函数一样,只要我将参数的类型改为IP
类型,那这个时候,外面将只能传递进来两种类型,要么是IPv4
,要么是IPv6
,不会有第三种情况。
如果想要判断它具体是哪种类型,就必须用到rust中的match
语句(意为匹配的意思)
注意,如果你学过其它语言,比如C/C++等,可能潜意识里会用if
来进行判断,但这样直接判断是错误的,后面我会提及。
匹配代码如下:
match t {
IP::Ipv4 => {
println!("传入的是IPv4类型");
}
IP::Ipv6 =>{
println!("传入的是IPv6类型");
}
};
也就是使用match
这个关键字。后面紧跟你要匹配的类型变量,这里就是函数传入的参数t
。
然后在后面的大括号中,就要分别为其所有可能的类型书写分支,比如这里IP
只有两个类型,所以大括号中只有两个分支:
IP::Ipv4 => {
//分支ipv4
}, // 不同分支用‘,’分隔
IP::Ipv6 =>{
//分支ipv6
}
注意每条分支由三部分组成:
- 分支类型
=>
符号(作为分支类型与所属代码的分隔符){}
中的代码(如果匹配这个分支,就会执行里面的代码)
注意{}
并不是必须的,如果该分支内部只有一条语句,那么{}
可以省略:
match t {
IP::Ipv4 => println!("传入的是IPv4类型"),