014、枚举与模式匹配

        枚举类型,通常也被简称为枚举,它允许我们列举所有可能的值来定义一个类型。在本篇文章中,我们首先会定义并使用一个枚举,以向你展示枚举是如何连同数据来一起编码信息的。

        接着,我们会讨论一个特别有用的枚举:Option,它常常被用来描述某些可能不存在的值。随后,我们将学会如何在 match 表达式中使用模式匹配,并根据不同的枚举值来执行不同的代码。

        最后,我们还会介绍另外一种常用的结构 if let,它可以在某些场景下简化我们处理枚举的代码。你可以找到许多拥有枚举特性的语言,但它们提供的具体功能却不尽相同。

        如果一定要比较的话,Rust中的枚举更类似于F#、OCaml和Haskell这类函数式编程语言中的代数数据类型(algebraic data type)。

1.定义枚举

        现在,让我们来尝试处理一个实际的编码问题,并接着讨论在这种情形下,为什么使用枚举要比结构体更加合适。

        假设我们需要对IP地址进行处理,那么目前有两种被广泛使用的IP地址标准:IPv4和IPv6。因为我们只需要处理这两种情形,所以可以将所有可能的值枚举出来,这也正是枚举名字的由来。

        另外,一个IP地址要么是IPv4的,要么是IPv6的,没有办法同时满足两种标准。这个特性使得IP地址非常适合使用枚举结构来进行描述,因为枚举的值也只能是变体中的一个成员。

        无论是IPv4还是IPv6,它们都属于基础的IP地址协议,所以当我们需要在代码中处理IP地址时,应该将它们视作同一种类型。我们可以通过定义枚举IpAddrKind来表达这样的概念,声明该枚举需要列举出所有可能的IP地址种类—V4和V6,这也就是所谓的枚举变体(variant):

enum IpAddrKind {
    V4,
    V6,
}

        现在,IpAddrKind 就是一个可以在代码中随处使用的自定义数据类型了。 

2. 枚举值

        我们可以像下面的代码一样分别使用 IpAddrKind 中的两个变体来创建实例: 

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

        需要注意的是,枚举的变体全都位于其标识符的命名空间中,并使用两个冒号来将标识符和变体分隔开来。由于 IpAddrKind::V4 IpAddrKind::V6 拥有相同的类型 IpAddrKind,所以我们可以定义一个接收 IpAddrKind 类型参数的函数来统一处理它们: 

fn route(ip_type: IpAddrKind) { }

        现在,我们可以使用任意一个变体来调用这个函数了:

route(IpAddrKind::V4);
route(IpAddrKind::V6);

        除此之外,使用枚举还有很多优势。让我们继续考察这个IP地址类型,到目前为止,我们只能知道IP地址的种类,却还没有办法去存储实际的IP地址数据。考虑到我们刚刚学习了结构体,所以你也许会像示例6-1所示的那样去解决这个问题。 

// 示例6-1:使用struct来存储IP地址的数据和IpAddrKind变体

❶enum IpAddrKind {
    V4,
    V6,
}

❷struct IpAddr {
 ❸ kind: IpAddrKind,
 ❹ address: String,
}

❺let home = IpAddr {
    kind: IpAddrKind::V4,
    address: String::from("127.0.0.1"),
};

❻let loopback = IpAddr {
    kind: IpAddrKind::V6,
    address: String::from("::1"),
};

        上面的代码定义了拥有两个字段的结构体IpAddr❷:一个IpAddrKind类型(也就是我们之前定义的枚举❶)的字段kind❸,以及一个String类型的字段address❹。另外,我们还分别创建了两个不同的结构体实例。

        第一个实例,home❺,使用了IpAddrKind::V4作为字段kind的值,并存储了关联的地址数据127.0.0.1。第二个实例,loopback❻,存储了IpAddrKind的另外一个变体V6作为kind的值,并存储了关联的地址::1

        新结构体组合了kindaddress的值,现在,变体就和具体数据关联起来了。实际上,枚举允许我们直接将其关联的数据嵌入枚举变体内。我们可以使用枚举来更简捷地表达出上述概念,而不用将枚举集成至结构体中。

        在新的IpAddr枚举定义中,V4V6两个变体都被关联上了一个String值: 

enum IpAddr {
    V4(String),
    V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));

        我们直接将数据附加到了枚举的每个变体中,这样便不需要额外地使用结构体。另外一个使用枚举代替结构体的优势在于:每个变体可以拥有不同类型和数量的关联数据。

        还是以IP地址为例,IPv4地址总是由40~255之间的整数部分组成。假如我们希望使用4u8值来代表V4地址,并依然使用String值来代表V6地址,那么结构体就无法轻易实现这一目的了,而枚举则可以轻松地处理此类情形: 

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

        目前,我们已经为存储IPv4地址及IPv6地址的数据结构给出了好几种不同的方案。但实际上,由于存储和编码IP地址的工作实在太常见了,因此标准库为我们内置了一套可以开箱即用的定义!

        让我们来看一看标准库是如何设计IpAddr的。它采用了和我们自定义一样的枚举和变体定义,但将两个变体中的地址数据各自组装到了两个独立的结构体中: 

struct Ipv4Addr {
    // --略--
}

struct Ipv6Addr {
    // --略--
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}

        在这段代码中,你可以在枚举的变体中嵌入任意类型的数据,无论是字符串、数值,还是结构体,甚至可以嵌入另外一个枚举!

        另外,标准库中的类型通常不会比我们设想的实现要复杂多少。需要注意的是,虽然标准库中包含了一份IpAddr的定义,但由于我们没有把它引入当前的作用域,所以可以无冲突地继续创建和使用自己定义的版本。

        我们会在后面深入讨论作用域引入。继续来看示例6-2中另外一个关于枚举的例子,它的变体中内嵌了各式各样的数据类型。 

// 示例6-2:枚举Message的变体拥有不同数量和类型的内嵌数据

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

        这个枚举拥有4个内嵌了不同类型数据的变体:

Quit没有任何关联数据。

Move包含了一个匿名结构体。

Write包含了一个String

ChangeColor包含了3i32值。

        定义示例6-2中的枚举有些类似于定义多个不同类型的结构体。但枚举除了不会使用struct关键字,还将变体们组合到了同一个Message类型中。下面代码中的结构体可以存储与这些变体完全一样的数据: 

        两种实现方式之间的差别在于,假如我们使用了不同的结构体,那么每个结构体都会拥有自己的类型,我们无法轻易定义一个能够统一处理这些类型数据的函数,而我们定义在示例6-2中的Message枚举则不同,因为它是单独的一个类型。

        枚举和结构体还有一点相似的地方在于:正如我们可以使用impl关键字定义结构体的方法一样,我们同样可以定义枚举的方法。下面的代码在Message枚举中实现了一个名为call的方法: 

impl Message {
    fn call(&self) {
     ❶ // 方法体可以在这里定义
    }
}

❷let m = Message::Write(String::from("hello"));
m.call();

        方法定义中的代码同样可以使用self来获得调用此方法的实例。在这个例子中,我们创建了一个变量 m❷,并为其赋予了值Message::Write(String::from("hello")),而该值也就是执行m.call()指令时传入call方法❶的self

        让我们再来看一看标准库中提供的另外一个非常常见且实用的枚举:Option

3. Option枚举及其在空值处理方面的优势

        在前文中,我们看到了IpAddr枚举是如何利用Rust的类型系统来将更多的信息,而不仅仅是数据,编码到程序中去的。而本节则会针对性地研究一个定义于标准库中的枚举:Option

        由于这里的Option类型描述了一种值可能不存在的情形,所以它被非常广泛地应用在各种地方。将这一概念使用类型系统描述出来意味着,编译器可以自动检查我们是否妥善地处理了所有应该被处理的情况。

        使用这一功能可以避免某些在其他语言中极其常见的错误。在设计编程语言时往往会规划出各式各样的功能,但思考应当避免设计哪些功能也是一门非常重要的功课。Rust并没有像许多其他语言一样支持空值。空值(Null)本身是一个值,但它的含义却是没有值。

        在设计有空值的语言中,一个变量往往处于这两种状态:空值或非空值。Tony Hoare,空值的发明者,曾经在2009年的一次演讲Null References: The Billion Dollar Mistake中提到: 

这是一个价值数十亿美金的错误设计。当时,我正在为一门面向对象语言中的引用设计一套全面的类型系统。我的目标是,通过编译器自动检查来确保所有关于引用的操作都是百分之百安全的。但是我却没有抵挡住引入一个空引用概念的诱惑,仅仅是因为这样会比较容易去实现这套系统。这导致了无数的错误、漏洞和系统崩溃,并在之后的40多年中造成了价值数10亿美金的损失。

        空值的问题在于,当你尝试像使用非空值那样使用空值时,就会触发某种程度上的错误。因为空或非空的属性被广泛散布在程序中,所以你很难避免引起类似的问题。

        但是不管怎么说,空值本身所尝试表达的概念仍然是有意义的:它代表了因为某种原因而变为无效或缺失的值。引发这些问题的关键并不是概念本身,而是那些具体的实现措施。

        因此,Rust中虽然没有空值,但却提供了一个拥有类似概念的枚举,我们可以用它来标识一个值无效或缺失。这个枚举就是Option<T>,它在标准库中被定义为如下所示的样子: 

enum Option<T> {
    Some(T),
    None,
}

        由于Option<T>枚举非常常见且很有用,所以它也被包含在了预导入模块中,这意味着我们不需要显式地将它引入作用域。

        另外,它的变体也是这样的:我们可以在不加Option::前缀的情况下直接使用SomeNone。但Option<T>枚举依然只是一个普通的枚举类型,Some(T)None也依然只是Option<T>类型的变体。

        这里的语法<T>是一个我们还没有学到的Rust功能。它是一个泛型参数,我们将会在后文讨论关于泛型的更多细节。现在,你只需要知道<T>意味着Option枚举中的Some变体可以包含任意类型的数据即可。下面是一些使用Option值包含数值类型和字符串类型的示例: 

let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;

        假如我们使用了None而不是Some变体来进行赋值,那么我们需要明确地告知Rust这个Option<T>的具体类型。这是因为单独的None变体值与持有数据的Some变体不一样,编译器无法根据这些信息来正确推导出值的完整类型。

        当我们有了一个Some值时,我们就可以确定值是存在的,并且被Some所持有。而当我们有了一个None值时,我们就知道当前并不存在一个有效的值。这看上去与空值没有什么差别,那为什么Option<T>的设计就比空值好呢?

        简单来讲,因为Option<T>T(这里的T可以是任意类型)是不同的类型,所以编译器不会允许我们像使用普通值一样去直接使用Option<T>的值。例如,下面的代码在尝试将i8Option<i8>相加时无法通过编译: 

let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;

        运行这段代码,我们可以看到类似下面的错误提示信息:

error[E0277]: the trait bound `i8: std::ops::Add<std::option::Option<i8>>` is
not satisfied
 -->
  |
5 |     let sum = x + y;
  |                 ^ no implementation for `i8 + std::option::Option<i8>`
  |

        哇!这段错误提示信息实际上指出了Rust无法理解i8Option<T>相加的行为,因为它们拥有不同的类型。当我们在Rust中拥有一个i8类型的值时,编译器就可以确保我们所持有的值是有效的。我们可以充满信心地去使用它而无须在使用前进行空值检查。

        而只有当我们持有的类型是Option<i8>(或者任何可能用到的值)时,我们才必须要考虑值不存在的情况,同时编译器会迫使我们在使用值之前正确地做出处理操作。

        换句话说,为了使用Option<T>中可能存在的T,我们必须要将它转换为T。一般而言,这能帮助我们避免使用空值时最常见的一个问题:假设某个值存在,实际上却为空。 

        在编写代码的过程中,不必再去考虑一个值是否为空可以极大地增强我们对自己代码的信心。为了持有一个可能为空的值,我们总是需要将它显式地放入对应类型的Option<T>值中。

        当我们随后使用这个值的时候,也必须显式地处理它可能为空的情况。无论在什么地方,只要一个值的类型不是Option<T>的,我们就可以安全地假设这个值不是非空的。

        这是Rust为了限制空值泛滥以增加Rust代码安全性而做出的一个有意为之的设计决策。那么,当你持有了一个Option<T>类型的Some变体时,你应该怎样将其中的T值取出来使用呢?

        Option<T>枚举针对不同的使用场景提供了大量的实用方法,你可以在官方文档中找到具体的使用说明。熟练掌握Option<T>的这些方法将为你的Rust之旅提供巨大的帮助。

        总的来说,为了使用一个Option<T>值,你必须要编写处理每个变体的代码。某些代码只会在持有Some(T)值时运行,它们可以使用变体中存储的T

        而另外一些代码则只会在持有None值时运行,这些代码将没有可用的T值。match表达式就是这么一个可以用来处理枚举的控制流结构:它允许我们基于枚举拥有的变体来决定运行的代码分支,并允许代码通过匹配值来获取变体内的数据。

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/293717.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

figma导入psd实战笔记

最近发现figma特别好用 并且插件生态特别庞大 如 将设计图转成vue react react-native 项目 flutter 项目 最重要的是 可以集成vscode 插件使用 使用蓝湖久了 感觉蓝湖 有写繁琐 同事扩展功能有限 Figma: The Collaborative Interface Design ToolFigma is the leading collabo…

上帝视角俯视工厂设计模式

引言 本篇聊聊设计模式中的简单工厂、工厂方法、抽象工厂设计模式&#xff0c;争取在看完这篇后不会再傻傻分不清以及能够应用在实际项目中 背景 以一个咱们都熟悉的场景举个例子&#xff0c;我们平时都会戴口罩&#xff0c;用来过滤一些普通病毒&#xff0c;大致的设计如下…

电脑记事本怎么打开?电脑记事本打开方法

在日常工作中&#xff0c;许多上班族都习惯于使用电脑记事本记录重要事项、灵感想法或临时任务。电脑记事本轻便、简洁&#xff0c;能够为我们提供便捷的记事体验。那么电脑记事本怎么打开呢&#xff1f;电脑记事本打开方法是什么呢&#xff1f;在Windows电脑上&#xff0c;我们…

手把手教你用Python打造一个语音合成系统

目录 引言 一、了解语音合成技术 1.1 什么是语音合成技术 1.2 语音合成技术的分类 二、准备所需工具和库 2.1 Python编程语言 2.2 TensorFlow深度学习框架 2.3 WaveNet模型 三、搭建语音合成系统 3.1 数据准备 3.2 数据预处理 3.3 构建WaveNet模型 3.4 训练WaveNe…

京东年度数据报告-2023全年度净水器十大热门品牌销量榜单

近年来&#xff0c;随着科技的不断发展和应用&#xff0c;净水器的技术得到持续创新和提高&#xff0c;产品品质和使用效果不断优化&#xff0c;这也进一步提升了净水器的市场竞争力&#xff0c;2023年&#xff0c;净水器市场的销售成绩呈现增长。 根据鲸参谋平台的数据显示&a…

大语言模型占显存的计算和优化

可以优化的地方&#xff1a; per_device_train_batch_size&#xff08;相当于batch size&#xff0c;越小显存占的越小&#xff09; gradient_accumulation_steps&#xff08;per_device_train_batch_size*gradient_accumulation_steps计算梯度的数据数&#xff09; gradien…

【CSS】设置0.5px的边框宽度

直接写border: 0.5px solid red; 这样在移动端可能会出现问题&#xff0c;下面说下解决办法&#xff1a; 直接上代码&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-C…

1.4 day4 IO进程线程

使用两个子进程进行文件拷贝&#xff0c;父进程进行资源回收 #include <myhead.h> int main(int argc, const char *argv[]) {//创建一个文件描述符并以只读的方式打开int fd-1;if((fdopen("./test.bmp",O_RDONLY))-1){perror("open error");return…

2023年度全球重大关基安全事件 TOP 10 | FreeBuf 年度盘点

2023年&#xff0c;针对关键信息基础设施的网络攻击已经演变成为了一个全球性的问题&#xff0c;无论是中、美、俄等国际大国&#xff0c;还是诸多小国/地区&#xff0c;无论是经济发达还是落后&#xff0c;都无法保证绝对免疫关键基础设施的攻击。为了保障国家安全和社会稳定&…

Python Selenium如何下载网页中的图片到本地?(Base64编码的图片下载)

前言&#xff1a; 在网页上&#xff0c;图片有时会以Base64编码的形式嵌入在HTML中&#xff0c;而不是作为单独的文件提供。这种方式的优点是可以减少HTTP请求的数量&#xff0c;因为图片数据直接包含在HTML中&#xff0c;不需要额外的请求来获取图片文件。这对于小图片…

java大数据hadoop2.92安装伪分布式文件系统

Apache Hadoop 3.3.6 – Hadoop: Setting up a Single Node Cluster. 1、解压缩到某个路径 /usr/local/hadoop 2、修改配置文件 /usr/local/hadoop/etc/hadoop/hadoop-env.sh export JAVA_HOME/usr/local/javajdk 3、修改配置文件 /usr/local/hadoop/etc/hadoop/core-sit…

Linux-进程间通信_管道

项目场景&#xff1a; 须熟知文件管理和进程方面的基础知识 通过Xshell和VScode 相互进行远程开发&#xff0c;学习进程间通信的其中一种方式——管道。 问题描述 依照我们曾经所学的知识&#xff0c;我们仅仅只能在单个进程中进行数据的交互&#xff0c;但是在实际应用中&a…

geemap学习笔记041:Landsat Collection2系列数据去云算法总结

前言 去云算法是进行数据处理中所要进行一步重要操作&#xff0c;Sentinal-2数据中已经提供了去云算法&#xff0c;但是Landsat Collection2系列数据中并没有提供去云算法&#xff0c;下面就以Landsat 8 Collection2为例进行介绍。 1 导入库并显示地图 import ee import gee…

二进制安装包安装Prometheus插件安装(mysql_exporter)

简介 mysql_exporter是用来收集MysQL或者Mariadb数据库相关指标的&#xff0c;mysql_exporter需要连接到数据库并有相关权限。既可以用二进制安装部署&#xff0c;也可以通过容器形式部署&#xff0c;但为了数据收集的准确性&#xff0c;推荐二进制安装。 一&#xff0c;下载安…

【CSS】浅学一下filter

目录 1、基本概念 2、用法 3、应用案例 更加智能的阴影效果&#xff1a; 元素、网页置灰 元素强调、高亮 毛玻璃效果 调整网页sepia 褐色值可以实现护眼效果 1、基本概念 CSS filter 属性将模糊或颜色偏移等图形效果&#xff08;对比度、亮度、饱和度、模糊等等&#…

MySQL基础篇(四)事务

一、事务简介 事务是一组操作的集合&#xff0c;它是一个不可分割的工作单位&#xff0c;事务会把所有的操作作为一个整体一期向系统提交或撤销操作请求&#xff0c;即这些操作要么同时成功&#xff0c;要么同时失败。 注意&#xff1a; 默认 MySQL 的事务是 自动提交 的&#…

使用Docker方式安装Artifactory

1、安装前环境准备 首先要关闭防火墙&#xff0c;关闭Selinux&#xff0c;准备好安装好的docker。以下安装版本&#xff1a;7.19.10 ##关闭防火墙&#xff0c;并设置开机自关闭 systemctl stop firewalld.service systemctl disable firewalld.service ##查看防火墙状态 sy…

Spark集群搭建

Spark集群结构 图 名词解释 Driver 该进程调用 Spark 程序的 main 方法&#xff0c;并且启动 SparkContextCluster Manager 该进程负责和外部集群工具打交道&#xff0c;申请或释放集群资源Worker 该进程是一个守护进程&#xff0c;负责启动和管理 ExecutorExecutor 该进程是一…

Linux-故障排查

实验要求 samba仅允许192.168.1.0/24、192.168.10/24进行访问 开一台虚拟机 快照恢复到未联网安装 关闭防火墙 安全linux 编辑ens33网卡 vim /etc/sysconfig/network-scripts/ifcfg-ens33 将ens33网卡复制一份命名为ens37 cp /etc/sysconfig/network-scripts/ifcfg-ens33 /etc…

深度学习课程实验三训练和测试卷积神经网络

一、 实验目的 1、学会搭建、训练和测试卷积神经网络&#xff0c;并掌握其应用。 2、掌握使用numpy实现卷积(CONV)和池化(POOL)层&#xff0c;包括正向春传播和反向传播。 二、 实验步骤 Convolutional Neural Networks: Step by Step 1、导入所需要的安装包 2、构建卷积神经…
最新文章