欢迎光临散文网 会员登陆 & 注册

第 45 讲:枚举(一):枚举的基本机制

2021-07-02 23:12 作者:SunnieShine  | 我要投稿

前面我们把基本的结构的使用方式、继承关系的理论给大家介绍了一遍。说实话,也没有多少内容,主要是前面类的内容学完了,这边的结构都是照着用就可以,只是部分细节有点不一样而已。

今天我们继续介绍结构。今天我们要介绍的是枚举类型(Enumeration Type)。

因为内容非常多,所以我们分若干部分介绍。今天先讲解枚举的基本语法、用法和它的底层逻辑。

Part 1 什么是枚举类型?

思考一个问题。我们把全班的人的基本数据存储到一个表里,其中有一个信息名字叫做性别(Gender)。我们前面使用的是一个 bool 类型的数据代指是不是男生。如果是男生,这个数值就是 true;否则用的是 false 表示的。不过这样有一点小小的问题是,这样数据不够直观:因为数据本身直接用 truefalse,从这两个单词上看,我们是看不出这个人是男生还是女生的。如果我们 C# 里有一种机制,可以专门用一个“可以直接看出来数据是什么数值的”的类型来表达的话,岂不是美哉。

是的,C# 确实为了这一点提供了一种机制,叫做枚举类型。所谓的“枚举”,就是将一个数据的所有取值情况全部列出来的一种逻辑。

Part 2 语法

比如前面举例里说到的“性别”。咱们这里暂时按照“男”和“女”来表达的话,那么就只有两种情况。那么,我们可以通过这样的代码表达出来:

当我们看到 Male 的时候,我们自然而然就可以知道这个人性别是男生;反之看到 Female 就可以反应过来这个人是女生。这样的话,我们就可以直接给数据的数值“取名”,来达到“数值有意义”的效果。

它的语法很简单:

一个访问修饰符(可以不写,和结构、类和接口的声明里的那个访问修饰符一样,可有可无);然后是 enum 关键字;然后是枚举的类型名称;然后是一对大括号,里面写的是所有可能的数值。

稍微注意一下最后这个分号。分号是可以不写的(而且一般也是不写的,因为写不写都没有区别),和其它数据类型的声明长一样;但是不同于其它的数据类型,枚举类型可以在大括号最后加一个分号,但类、结构和接口的声明的最外层大括号最后是不能加分号的。这一点只有枚举类型可以。

其中,枚举的数值是需要写成“名字 = 数值”的一对赋值关系的。当然,右侧的这个赋值关系可以省略,默认是从 0 开始计算。比如说,这里 MaleFemale 都不写赋值部分的话,那么:

代码就变成了这样。这样也是 OK 的,它依旧和前面的 0 和 1 等价:从第一个枚举数值开始,默认从 0 开始计算。第一个元素的数值是 0、第二个数值是 1,以此类推。如果中间给出了某个元素的数值,那么后面没有给出赋值关系的这些数值都是依赖于前一个枚举数值的值,然后加 1 得到。

这样的话,AG 的数值分别是 0、1、100、101、102、29 和 30。总之就是看前面一个的数值,在这个基础上增加 1 得到。第一个元素默认是 0(当然,如果你写了数值,那么就是这个数值)。

那么,怎么使用呢?

Part 3 枚举类型的使用

假设 Person 类型长这样:

啰嗦一下。我们观察一下属性的书写格式,显然可以知道的是 public 后面跟的这个就是类型名称,而类型名后跟的才是属性的名字。那么我把属性名和类型名称取名成一样的,会不会造成歧义?肯定不会,对吧。因为先类型名后属性名,那么这样的关系一旦出现的话,C# 就能识别和分辨出来这样取名是哪个信息对应哪个部分。所以 public Gender Gender { get ... } 里的第一个 Gender 表示 Gender 类型,而第二个 Gender 实际上指的是配套 _gender 字段的 Gender 属性名字了。

我们再假设我们已经得到了一个列表叫做 classmates,存储的是这些人的数据信息。

请注意第 6 行代码。classmate 后我们直接跟上的是 .Gender。我们假设 Person 类型包含这个实例属性,因此我们可以直接通过这样的语法得到信息;接着,我们使用 classmate.Gender 表达式,得到的是一个 Gender 类型的结果,那么我们要使用 == 来比较这个结果到底和哪个枚举的数值相等。比较方式也很简单:== Gender.Male。右侧用 类型名字.枚举数值名 的方式取得结果,然后用等号比较数值是不是一样的。

如果一致,那么我们就可以认为 classmate 的性别是男了,那么我们就给 boysCount 增加 1。一轮 foreach 循环下来,我们就把整个数组全部看完了,这样就可以达到统计男女生分别有多少个人的效果。

Part 4 那么,枚举数值的值到底有什么用?

好像,我们直接用 == 比较数值是不是一样,按名字去比较好像跟这个设置的 0、1、2 这些数据没有关系啊。实际上不是的。它其实是和枚举底层实现是有关系的。我们这里需要把枚举看作是一种“可以自己命名数值信息的整数类型”。它本质上是一种整数类型,只是它长相非常不像是整数类型(什么 intshort 什么的)。确实不太像,但因为它最终会通过 = 来赋值,那么最终我们这里使用的 == 比较其实是看的这个整数数值是不是相同。如果直接上手比较 Gender.Male 这个字符串是不是一样的话,肯定比比整数要慢一点。所以 C# 把枚举类型比较结果用一个整数表示出来,我们称为它的特征值(Eigenvalue)。这个枚举数值整体,我们称为一个枚举字段(Enumeration Field)或者叫一个枚举成员(Enumeration Member)。

说清楚这个名字后,我们来说一下枚举的继承机制。枚举是不能自定义继承关系的,你只能直接使用,它也必须表示成一个整数。但是整数的类型也很多,目前在 C# 里有如下这些数据类型是整数的类型:

  • sbyte:带符号字节型

  • byte:无符号字节型

  • short:短整数型

  • ushort:无符号短整数型

  • int:整数型

  • uint:无符号整数型

  • long:长整数型

  • ulong:无符号长整数型

这些数据类型都可作为枚举类型的数值的类型。我们可以规定枚举类型到底用的是哪个整数类型作为类型的数值表达,语法是通过类似继承关系相同的语法追加到类型声明最后:

如果这个枚举用到的字段很少,我们可以使用 byte,因为它完全用不到那么多的情况。虽然我们经常省略不写这个枚举的“继承关系”,但是我们依旧需要了解,其实这个数值类型是可以自己控制的。

Part 5 枚举类型和整数类型的互相转化

枚举和整数在底层里可以说是基本一样,但为了约束用户使用 C# 更为规范,很多语法处理起来还是不一样的。比如转换的关系。虽然说的是两者底层是基本一样的,但我们仍然不能直接把整数赋值给枚举,反之亦然。

这是因为我们赋值的整数很有可能超出赋值范围。比如说我 Gender 此时的整数表示范围只可能是 0 和 1,但我可能手残赋值过去一个 2:

反之亦然。可能你会问,我不管拿什么整数的数据类型接收,貌似都可以合理。比如 int i = gender;gender 完全不可能超出 int 范围,我这么赋值貌似没有任何问题吧。

这是一种 C# 的语法约束。它为了规范化你的使用,这两种类型是不能直接转化的,因为它们表现出来的类型机制不同。那么怎么转换呢?强制转换就行了。使用强制转换机制来告知编译器,我这么做是我预期的行为。

假如说 Gender 还是基于 int 类型的话:

这样的赋值就没有问题了。但是……思考一点。如果我们尝试改变这段代码里 int 变量接收,而改用 short 的话呢?你这个时候就需要两次强制转换了:

是的,你必须要两次强制转换。这是为什么呢?因为 Gender 类型是基于 int 的,所以 (int) 强转只是让 Gender 能够用 int 类型来表现出来而已(这样转换没有问题);然后,int 转换到 short 才是告诉编译器,我这是预期行为,所以再次使用 (short) 强转来改变原本结果的类型。

Part 6 枚举类型的继承机制

前文说到,枚举类型只能定义特征值的赋值类型,而不能改变它的继承关系。那么它原始的继承关系是如何的呢?

所有的枚举类型统统从一个叫 Enum 的抽象类进行派生的。这一点和结构从 ValueType 抽象类派生可以说是非常相似,都是从抽象类派生下来的类型。那么,为什么要有这样的派生机制呢?这是因为枚举类型有别的类型做不到的事情,就是表达数据语义化。那么,对于数值本身来说,显然就会有非常多的额外处理机制,比如说取这个枚举的名字啊,获取这个特征值是不是在枚举表达的范围里之类的。那么,Enum 这个抽象类就提供了这些操作。

比如这样就达到了一种多态赋值的过程。不过请注意的是,左侧的 Enum 是引用类型(抽象类嘛那当然是引用类型了),右侧的是值类型(枚举都基于整数了那还不是值类型?),所以这种赋值会造成隐式的装箱行为。当然,装箱也无伤大雅,执行起来也没问题,只是效率略低一点。

马上我们来说一下 Enum 类型的基本用法。

Part 7 枚举类型和字符串之间的转化

前面我们说到,枚举类型是用整数数值(它的特征值)来比较的。那么枚举类型可否表示成字符串形式呢?可以。它的转换和之前我们整数和字符串互相转换方式完全一样,不过类型名字稍微换一下。

我们使用完全和 object 这些类型一样的 ToString 方法来获取字符串。最后我们可以得到的结果是 "Male"。请注意,虽然我们写成 Gender.Male,但结果并不会包含“Gender.”这一部分。

这个是转字符串。反过来呢?反过来的话,语法有些超纲,我们需要使用一个叫做 typeof 的表达式来表示。这一点有些麻烦。

满天飞的小括号。首先,我们需要学习的是 Enum.Parse 这个静态方法。这个方法后需要跟两个参数,第一个参数是需要你使用 typeof 这个稍微超纲一点的语法来表达是什么类型。因为 Enum.Parse 最终还是 Enum 类型,但是我们的枚举类型是我们自己定义的,所以我们自己定义的枚举类型和 int 这样的类型还有所不同。intshort 好歹是单个的个体的类型,这样 Parse 无需指定类型名字就可以直接转换,而且非常方便;但是问题就出在 Enum 本身机制上。

总之,我们是无法从抽象类型 Enum 上知道我们自己的枚举类型到底是如何的。因此,我们必须要制定 Parse 方法到底转换成什么类型才可以。但是,要指明类型是什么,我们目前学到的语法还做不到这一点,所以用到了这里的超纲语法 typeof。这个表达式的写法是 typeof(类型)。我们直接在小括号里写上这个类型名字,这样就可以指示类型的基本信息了。这样,我们就可以把这个东西传过去,Enum.Parse 就知道我们要转成什么类型的数据了。那么,第二个参数自然就是我们需要的字符串了。

另外,C# 还没有这么智能,智能到参数用的这个 typeof 就能暗示返回值的类型,所以返回值 Enum.Parse 方法是 object 来表示的。这就体现出了 object 的好处了。如果没有 C# 面向对象的这一些继承机制,就不可能存在 object 这样的顶级数据类型。如果没有顶级数据类型的话,我们就无法通过语法实现这里 Enum.Parse 返回值的类型确定。正是因为 object 类型是任何数据类型都可以直接赋值过去的机制,所以这样就可以表达所有想要表示的结果,这就是 object 带来的好处。

那么,得到 obj 变量后,我们显然不能直接用。所以我们要向精确类型上进行转换。首先是 objEnum 类型上转,表示它实际上是一个枚举类型的结果;然后再次转为 Gender 这个精确的枚举类型。前面的 objEnum 类型上转是因为,我们先要暗示 obj 实际上是一个枚举类型,然后才可以往下继续转换为 Gender

实际上你写成 Gender result = (Gender)obj; 也没多大问题。但是这一点和前文描述的 (short)(int)gender 双重类型转换语法就不统一了,可能会造成理解上的困惑。初学为了了解类型的基本转换规则,我们建议养成好习惯,先走 Enum 上转一下,然后再继续往下转。

这里的 (Gender)(Enum)obj 实际上是和 (short)(int)gender 的转换机制是不一样的。但是这一点很难马上就描述清楚,所以我干脆就不在这里说了。到时候你们直接看视频吧。


第 45 讲:枚举(一):枚举的基本机制的评论 (共 条)

分享到微博请遵守国家法律