第三章 rust语法进阶

本系列文章已升级、转移至我的自建站点中,本章原文为: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,
}

随后就是一对大括号,而大括号中的内容就是我们这个结构体的各种属性。

比如这里我们想要保存一个学生的信息,就有姓名、年龄、性别等等,也就是这里的nameagesex

由于这种声明类型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地址,目前来说最常用的就Ipv4IPv6两种,你不可能再凭空创建一种。

再比如,一周是周一到周日的七天,你也不能凭空再创造一个周八。

rust中的枚举相比于其它语言又非常的不同,下面我们来看一看rust中的枚举是如何使用的:

enum IP{
    Ipv4,
    Ipv6
}
fn main() {
    let mut m:IP; //声明一个IP类型的变量
    m=IP::Ipv4; //正确
    m=IP::Ipv6; //正确
    //m=11; //错误 
}

可以看到枚举的定义方式与上面提到的结构体非常相似,只是将struct关键字改为了enumenumeration的缩写),然后后面跟着这个枚举类型的名称。

为了更好的体现出rust中枚举的特性,这里以IP为例,IP地址只有两种类型,所以我就在枚举中只写了两个类型:ipv4ipv6

这个名字是随便写的,不过一般会为其取一些有意义的名字,如果选用一周七天作为一个枚举类型,那其中的就可以写七种类型,分别为周一到周七。

接着,我在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
    }

注意每条分支由三部分组成:

  1. 分支类型
  2. =>符号(作为分支类型与所属代码的分隔符)
  3. {}中的代码(如果匹配这个分支,就会执行里面的代码)

注意{}并不是必须的,如果该分支内部只有一条语句,那么{}可以省略:

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类型");
        }
    };
}

那么上面这段代码的整体逻辑就是:

  1. 匹配这个函数传入的IP类型的参数t
  2. 如果它是ipv4类型,那我就打印println!("传入的是IPv4类型");
  3. 如果是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);
        }
    };
}

取出的方式也很简单,就是像上面代码所示的那样,在类型后面添加一个(),里面填写任意参数名称用来取出其携带的数据即可。

image-20231212133915620

同时由于前面声明枚举类型的时候,我们就已经写好了其携带的参数类型,所以这里填写变量名后无需写参数的类型注解,rust的编译器可以自行为我们推断出来它的类型。

但每次都写match还是有点过于繁琐了,比如很多时候我就只想知道它是否等于某个类型。

那么就可以使用语法糖if let了:

image-20240219092021720

这和match其实是类似的,你可以这样理解它的工作流程:

if就是“如果”,let就是“让”,结合起来的意思就是“如果让IP::Ipv4(a,b,c,d)等于了t”,那么就执行后面的语句。

通过在if后面添加let的方式,就能实现简化代码、同时也能取出枚举所携带的数据。

包括else if同样可以后面添加let的方式继续分支判断。

只不过大多数情况下,我们仅在需要判断枚举是否等于某个类型时才会使用if let语法。

所以如果你确实需要为枚举所有可能的分支进行处理,那么最好还是使用match

至于分支太多的情况,你可以使用_进行默认处理:

image-20240219092743446

比如上图中我就试图匹配一个自然数,但自然数是无穷的,不可能挨个枚举。

那么此时你就可以只处理你想要处理的分支,其它剩余的分支则使用_进行默认处理。

虽然这里使用的是自然数,但对于上面的枚举类型也是同样适用的。

三、方法

方法,本质来说其实与上一章提到的函数是一个东西,它也是一个函数,但不同之处就在于,它是绑定到一个结构体上的。

注意:如果你学过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一样。

只不过其作用是将内容格式化为一个字符串并返回,而前者是将格式化好的字符串打印到控制台上。

后面讲到宏的时候,我们会再来探究它的实现原理。

总之就是,通过这种方式,我们就实现了简化输出的步骤,如果想要将这个类作为字符串进行输出,那就只需要调用这个函数,并传递进去这个类型的变量即可。

注意,对于同一个变量,不能调用两次这个函数,否则必然会报错:

image-20231212134155707

这里涉及到了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);
    }
}

想要为某个结构体显式的绑定方法,需要用到implimplementation,实现)关键字,后面紧跟你想要绑定的结构体,然后在后面的大括号里面,写上你想为它绑定的函数即可。

比如这里的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还能直接给我们做出提示信息:

image-20231212134548441

但绑定的方法并非必须含有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语法进阶

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

余识-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值