本系列文章已升级、转移至我的自建站点中,本章原文为:rust语法进阶
一、结构体
上一章介绍了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中,很多源码的结构体都是通过这种方式定义的。
二、枚举
如果你学过其它语言,那么上面的结构体你应该并不陌生,甚至对于这里要说的枚举,也会有一定熟悉的感觉。
枚举最主要的作用是:限制输入选项。
比如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类型"),
IP::Ipv6 => println!("传入的是IPv6类型"),
};
同时注意不同分支之间用,
隔开,如果存在{}
,那么,
分隔符是可以省略的,完整代码如下:
fn test_ip(t:IP) {
match t {
IP::Ipv4 => {
println!("传入的是IPv4类型");
} //在使用{}情况下,可以省略分隔符
IP::Ipv6 =>{
println!("传入的是IPv6类型");
}
};
}
那么上面这段代码的整体逻辑就是:
- 匹配这个函数传入的
IP
类型的参数t
- 如果它是
ipv4
类型,那我就打印println!("传入的是IPv4类型");
- 如果是
Ipv6
类型,那我就打印println!("传入的是IPv6类型")
。
除此之外,就没有第三种类型了:
fn main() {
test_ip(IP::Ipv4);
}
比如像上面这样,当调用这个函数,并传入ipv4
类型,那就会执行ipv4
分支后的代码,并打印出"传入的是IPv4类型"
这个字符串。
这便是枚举最基本、同时也是最重要的用法。
如果是其它语言,枚举类型介绍到这里也就差不多了,但rust
中的枚举却并不仅仅如此,它还能更加强大!
在rust
中,这种强大的枚举类型随处可见,如果你没能搞懂枚举,那你基本也就学不会rust
。
下面我们就开始介绍rust
中的枚举到底有何强大之处。
还是以前面的IP地址为例,它一般用来处理ip地址,但不能仅仅只知道个类型,你还需要具体的地址信息呀,比如ipv4中的192.168.0.1
。
而rust
中的枚举就提供了一种强大的特性:允许枚举类型携带任意类型与数量的数据
enum IP{
Ipv4(u32,u32,u32,u32),
Ipv6(String)
}
就像上面这样,我让ipv4
类型携带4
个数据,均为u32
类型(对应IP地址的4
个数字),然后再让ipv6
类型的携带1个数据,为String
类型的。
注意这里的参数是通过
()
,分别填入想要携带的数据类型,实际上就是前面章节提到的复合类型:元组。
这时,如果我们想要使用它,就必须在其后填入我们携带的数据才行:
fn main() {
//等价于:
//let m=IP::Ipv4(192,168,0,1);
//test_ip(m);
test_ip(IP::Ipv4(192,168,0,1));
//等价于:
//let m1=IP::Ipv6(String::from("fdb8:27c5:221:5400:2c95:a7fc:5d57:a375"));
//test_ip(m1);
test_ip(IP::Ipv6(String::from("fdb8:27c5:221:5400:2c95:a7fc:5d57:a375")));
}
就像上面这样,因为合在一起写可能嵌套太多,新手看起来比较困难,所以我在注释中将其分开写了的。
与以前的区别仅仅在于,这时我们必须在其后面跟()
,然后在里面填入我们想要让它携带的数据就行了。
ipv4
看起来应该比较容易,就是填入4个数字就行了,而ipv6
由于携带的String
类型,所以需要用Strng
中的from
方法来构造,所以看起来会比较繁琐。
既然有填入数据,那就必然需要取出数据,而取出数据,同样需要使用match
。
这也是为什么前面我说过不能使用
if
直接比较枚举类型的原因,因为if
本身无法拿到其携带的数据。
代码如下:
fn test_ip(t:IP) {
match t {
IP::Ipv4(a,b,c,d) => {
println!("传入的是IPv4类型:{}.{}.{}.{}",a,b,c,d);
},
IP::Ipv6(s)=>{
println!("传入的是IPv6类型:{}",s);
}
};
}
取出的方式也很简单,就是像上面代码所示的那样,在类型后面添加一个()
,里面填写任意参数名称用来取出其携带的数据即可。
同时由于前面声明枚举类型的时候,我们就已经写好了其携带的参数类型,所以这里填写变量名后无需写参数的类型注解,rust的编译器可以自行为我们推断出来它的类型。
但每次都写match还是有点过于繁琐了,比如很多时候我就只想知道它是否等于某个类型。
那么就可以使用语法糖if let
了:
这和match
其实是类似的,你可以这样理解它的工作流程:
if
就是“如果”,let
就是“让”,结合起来的意思就是“如果让IP::Ipv4(a,b,c,d)
等于了t”,那么就执行后面的语句。
通过在if
后面添加let
的方式,就能实现简化代码、同时也能取出枚举所携带的数据。
包括else if
同样可以后面添加let
的方式继续分支判断。
只不过大多数情况下,我们仅在需要判断枚举是否等于某个类型时才会使用if let
语法。
所以如果你确实需要为枚举所有可能的分支进行处理,那么最好还是使用match
。
至于分支太多的情况,你可以使用_
进行默认处理:
比如上图中我就试图匹配一个自然数,但自然数是无穷的,不可能挨个枚举。
那么此时你就可以只处理你想要处理的分支,其它剩余的分支则使用_
进行默认处理。
虽然这里使用的是自然数,但对于上面的枚举类型也是同样适用的。
三、方法
方法,本质来说其实与上一章提到的函数是一个东西,它也是一个函数,但不同之处就在于,它是绑定到一个结构体上的。
注意:如果你学过C++、java、python等语言,应该知道它们里面都有类的概念,而Rust
中并没有类的概念,但可以通过这里所说的方法,来实现其它语言中类的特性。
比如以本章第一小节介绍结构体所展示的代码:
struct Student{
name:String,
age:u32,
sex:char,
}
如果我想要输出它,我就不得不每次都要像下面这样去挨个取值:
println!("{} {} {}", s.name, s.age,s.sex);
学习了前面的函数之后,我们自然就可以通过定义一个函数来简化这种行为:
fn main(){
let s=Student{
name:String::from("yushi-"),
age:100,
sex:'男'
};
println!("{}",stu_to_str(s));
}
fn stu_to_str(s:Student) -> String {
return format!("{}-{}-{}", s.name, s.age, s.sex);
}
这里通过定义一个函数stu_to_str
,将传入的值,转化为一个字符串,并返回。
前面我们已经用过了println!
,只要后面带有!
就代表这是一个宏,这里的format!
同样也是一个宏,其使用方法与println
一样。
只不过其作用是将内容格式化为一个字符串并返回,而前者是将格式化好的字符串打印到控制台上。
后面讲到宏的时候,我们会再来探究它的实现原理。
总之就是,通过这种方式,我们就实现了简化输出的步骤,如果想要将这个类作为字符串进行输出,那就只需要调用这个函数,并传递进去这个类型的变量即可。
注意,对于同一个变量,不能调用两次这个函数,否则必然会报错:
这里涉及到了Rust中一个核心概念:所有权
不过这个概念稍微有点复杂,将留到后面章节、结合String
进行讲解,现在我们还是先将注意力拉回这里的方法。
从这里我们可以注意到,这个stu_to_str
只有唯一一个参数,那就是Student
类型,换句话来说就是,这个函数是和这个结构体绑定了的。
因为除却这个结构体外,你无法使用其它结构体来调用这个函数。
为了简化这一过程,rust就出现了方法这一个概念,其目的就是,既然这个方法只能用于这个结构体,那就直接显式的将其绑定起来好了!
struct Student{
name:String,
age:u32,
sex:char,
}
impl Student {
fn to_str(self) -> String {
return format!("{}-{}-{}", self.name, self.age, self.sex);
}
}
想要为某个结构体显式的绑定方法,需要用到impl
(implementation
,实现)关键字,后面紧跟你想要绑定的结构体,然后在后面的大括号里面,写上你想为它绑定的函数即可。
比如这里的impl
后面的关键字就是Student
,意思就是为它绑定(实现)方法。
其内部的方法,与原本的函数几乎是完全一致的。唯一不同的就是将原本的参数s:Student
改为了self
。
这同样是rust
中的关键字,其意思是“自己”,本质上是这种写法的简写:self: Self
。
前面这个小写的self
是参数变量名,而后面的Self
才是它的类型,这里的大写Self
,代指前面impl
关键字后面的结构体,所以在这里它就等价于Student
。
这样绑定了之后,这个self
参数就指代了任何调用这个函数的变量本身,调用方法如下:
fn main(){
let s=Student{
name:String::from("www.kucoding.com"),
age:100,
sex:'男'
};
println!("{}",s.to_str());
}
也就是通过.
的方式,就可以调用绑定到它自己本身的方法了:s.to_str()
但明明这个函数有一个参数啊,这里没有填入参数怎么还能调用呢?
因为这个.
运算符号,会将前面的这个s
变量,默认作为第一个参数传入,这样绑定之后,你就会发现,调用属于它自己的函数就变得非常方便了!
甚至vscode
还能直接给我们做出提示信息:
但绑定的方法并非必须含有self
参数,比如像下面这样:
impl Student {
fn new(name:String, age:u32,sex:char) -> Student {
return Student {
name: name,
age: age,
sex: sex
};
}
}
这里写了一个new
函数,参数就是三个数据,然后通过这三个数据返回一个Student
类型,就没有包含self
参数。
注意,如果你要包含
self
参数,那就必须是在第一个,其它参数都只能写在它之后。
它的作用就是构造一个Student
类型的变量,一旦没有包含self
,那么其使用方法就变得不一样了:
fn main(){
let s=Student::new(String::from("yushi-"), 100, '男');
println!("{}",s.to_str());
}
想要使用这种不带self
参数的函数,就必须用::
符号进行调用。
看到这里,现在你是不是就有些理解String
中的from
函数了?其本质上作用与我们这里的new
函数一样,就是用一些参数来构造本类型的。
这是最常见的用法,事实上
String
中也有一个new
函数,用来构造一个空字符串,这个后面再提。
官方称它为关联函数(因为方法的第一参数必须是self
)
这和C++中的静态函数类似,而方法,则可以类比为C++类中的成员函数。
事实上C++中的类,其成员函数实现方式也是用的这一套逻辑(有一个隐藏的参数作为第一个参数,我们称其为this
指针),只不过C++
没有像rust
这样为我们暴露出来而已。
四、简单总结
本章主要讲解了rust中的结构体、枚举与方法。
首先是结构体,用struct
关键字来标识,其作用就在于将相关联的一系列数据组织在一起,方便使用,需要注意的是其内部的每个字段都需要为其标注类型。
然后是枚举,用关键字enum
定义,其主要作用在于限制可选项,一般用于函数的参数,在后面对rust的介绍中,还会看到其会被作为函数的返回值而大量使用。
与其它语言不同的地方是,rust中的枚举是可以携带数据的,该数据为“元组”,这意味着它可以携带任意个数、任意类型的数据。
最后还有方法,其主要作用是简化代码逻辑,让结构体绑定一个方法可以方便我们使用,最重要的就是对self
关键字的理解。
如果不带参数self
,它就有另一个名称:关联函数,最常见的用途就是生成一个本类型的变量实例。
本系列文章已升级、转移至我的自建站点中,本章原文为:rust语法进阶