第 45 讲:枚举(一):枚举的基本机制
前面我们把基本的结构的使用方式、继承关系的理论给大家介绍了一遍。说实话,也没有多少内容,主要是前面类的内容学完了,这边的结构都是照着用就可以,只是部分细节有点不一样而已。
今天我们继续介绍结构。今天我们要介绍的是枚举类型(Enumeration Type)。
因为内容非常多,所以我们分若干部分介绍。今天先讲解枚举的基本语法、用法和它的底层逻辑。
Part 1 什么是枚举类型?
思考一个问题。我们把全班的人的基本数据存储到一个表里,其中有一个信息名字叫做性别(Gender)。我们前面使用的是一个 bool
类型的数据代指是不是男生。如果是男生,这个数值就是 true
;否则用的是 false
表示的。不过这样有一点小小的问题是,这样数据不够直观:因为数据本身直接用 true
和 false
,从这两个单词上看,我们是看不出这个人是男生还是女生的。如果我们 C# 里有一种机制,可以专门用一个“可以直接看出来数据是什么数值的”的类型来表达的话,岂不是美哉。
是的,C# 确实为了这一点提供了一种机制,叫做枚举类型。所谓的“枚举”,就是将一个数据的所有取值情况全部列出来的一种逻辑。
Part 2 语法
比如前面举例里说到的“性别”。咱们这里暂时按照“男”和“女”来表达的话,那么就只有两种情况。那么,我们可以通过这样的代码表达出来:
当我们看到 Male
的时候,我们自然而然就可以知道这个人性别是男生;反之看到 Female
就可以反应过来这个人是女生。这样的话,我们就可以直接给数据的数值“取名”,来达到“数值有意义”的效果。
它的语法很简单:
一个访问修饰符(可以不写,和结构、类和接口的声明里的那个访问修饰符一样,可有可无);然后是 enum
关键字;然后是枚举的类型名称;然后是一对大括号,里面写的是所有可能的数值。
其中,枚举的数值是需要写成“名字 = 数值”的一对赋值关系的。当然,右侧的这个赋值关系可以省略,默认是从 0 开始计算。比如说,这里 Male
和 Female
都不写赋值部分的话,那么:
代码就变成了这样。这样也是 OK 的,它依旧和前面的 0 和 1 等价:从第一个枚举数值开始,默认从 0 开始计算。第一个元素的数值是 0、第二个数值是 1,以此类推。如果中间给出了某个元素的数值,那么后面没有给出赋值关系的这些数值都是依赖于前一个枚举数值的值,然后加 1 得到。
这样的话,A
到 G
那么,怎么使用呢?
Part 3 枚举类型的使用
假设 Person
类型长这样:
public
后面跟的这个就是类型名称,而类型名后跟的才是属性的名字。那么我把属性名和类型名称取名成一样的,会不会造成歧义?肯定不会,对吧。因为先类型名后属性名,那么这样的关系一旦出现的话,C# 就能识别和分辨出来这样取名是哪个信息对应哪个部分。所以public Gender Gender { get ... }
里的第一个Gender
表示Gender
类型,而第二个Gender
实际上指的是配套_gender
字段的Gender
属性名字了。
我们再假设我们已经得到了一个列表叫做 classmates
,存储的是这些人的数据信息。
classmate
后我们直接跟上的是 .Gender
。我们假设 Person
类型包含这个实例属性,因此我们可以直接通过这样的语法得到信息;接着,我们使用 classmate.Gender
表达式,得到的是一个 Gender
类型的结果,那么我们要使用 ==
来比较这个结果到底和哪个枚举的数值相等。比较方式也很简单:== Gender.Male
。右侧用 类型名字.枚举数值名
的方式取得结果,然后用等号比较数值是不是一样的。
如果一致,那么我们就可以认为 classmate
的性别是男了,那么我们就给 boysCount
增加 1。一轮 foreach
循环下来,我们就把整个数组全部看完了,这样就可以达到统计男女生分别有多少个人的效果。
Part 4 那么,枚举数值的值到底有什么用?
好像,我们直接用 ==
比较数值是不是一样,按名字去比较好像跟这个设置的 0、1、2 这些数据没有关系啊。实际上不是的。它其实是和枚举底层实现是有关系的。我们这里需要把枚举看作是一种“可以自己命名数值信息的整数类型”。它本质上是一种整数类型,只是它长相非常不像是整数类型(什么 int
啊 short
什么的)。确实不太像,但因为它最终会通过 =
来赋值,那么最终我们这里使用的 ==
比较其实是看的这个整数数值是不是相同。如果直接上手比较 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
范围,我这么赋值貌似没有任何问题吧。
假如说 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
这样的类型还有所不同。int
、short
好歹是单个的个体的类型,这样 Parse
无需指定类型名字就可以直接转换,而且非常方便;但是问题就出在 Enum
本身机制上。
总之,我们是无法从抽象类型 Enum
上知道我们自己的枚举类型到底是如何的。因此,我们必须要制定 Parse
typeof
。这个表达式的写法是 typeof(类型)
。我们直接在小括号里写上这个类型名字,这样就可以指示类型的基本信息了。这样,我们就可以把这个东西传过去,Enum.Parse
就知道我们要转成什么类型的数据了。那么,第二个参数自然就是我们需要的字符串了。
另外,C# 还没有这么智能,智能到参数用的这个 typeof
就能暗示返回值的类型,所以返回值 Enum.Parse
方法是 object
来表示的。这就体现出了 object
的好处了。如果没有 C# 面向对象的这一些继承机制,就不可能存在 object
这样的顶级数据类型。如果没有顶级数据类型的话,我们就无法通过语法实现这里 Enum.Parse
返回值的类型确定。正是因为 object
类型是任何数据类型都可以直接赋值过去的机制,所以这样就可以表达所有想要表示的结果,这就是 object
带来的好处。
那么,得到 obj
变量后,我们显然不能直接用。所以我们要向精确类型上进行转换。首先是 obj
往 Enum
类型上转,表示它实际上是一个枚举类型的结果;然后再次转为 Gender
这个精确的枚举类型。前面的 obj
往 Enum
类型上转是因为,我们先要暗示 obj
实际上是一个枚举类型,然后才可以往下继续转换为 Gender
。
实际上你写成
Gender result = (Gender)obj;
也没多大问题。但是这一点和前文描述的(short)(int)gender
双重类型转换语法就不统一了,可能会造成理解上的困惑。初学为了了解类型的基本转换规则,我们建议养成好习惯,先走Enum
上转一下,然后再继续往下转。这里的
(Gender)(Enum)obj
实际上是和(short)(int)gender
的转换机制是不一样的。但是这一点很难马上就描述清楚,所以我干脆就不在这里说了。到时候你们直接看视频吧。