日常生活中,经常会遇到一组组的数据,它们像兄弟姐妹一样,彼此结伴出现。比较常见的如:东、南、西、北男、女等。这样的数据在软件领域,我们通常会用一种被称为枚举的类型专门表示。今天我们就来聊聊枚举的使用。

Swift中,最简单的定义枚举的方式如下:

enum Gender {
    case male
    case female
    case other
}

这里我们通过enum关键字定义了名为Gender的枚举,用来约束性别的几种可能,这些可能性通过case来逐条声明。其实这三条case也可以合并成一行书写,像下面这样,也是可以的:

enum Gender {
    case male , female , other
}

定义完枚举类型后,我们来演示下怎么使用它。想象定义一个类,用来描述人类,它包含两个基本字段:agegender,分别代表年龄和性别。其中age的类型显而易见,而gender的类型,就用我们前面定义的枚举即可,请看下面的代码:

class People {
    var age:Int
    var gender:Gender
    
    init(age:Int, gender:Gender){
        self.age = age
        self.gender = gender
    }
}

let aGirl = People(age:8, gender:.female)

在上面代码的最后一行,我们定义了一位8岁的女性,所以她是为小女孩,下面我们尝试在People类里让她开口说话。定义一个sayHello方法,它可以判断自己的性别,然后给出不同的反馈。这里涉及到的知识点,就是对枚举类型的判断。具体请看下面的代码:

    func sayHello()->String{
        switch gender{
        case .male:
            return "I am a boy."
        case .female:
            return "I am a girl."
        case .other:
            return "Hello."
        }
    }

当上面的方法被定义到People中后,我们就可以这样测试一下效果:

let aGirl = People(age:10, gender:.female)
print(aGirl.sayHello())

这里面要强调一点,枚举一旦参与switch语句判断,它的每一项都要接受比较。比如上面的场景,如果我想说,.other的情况我不关心,那么如果我删除了它,而使代码成为这样的话:

    func sayHello()->String{
        switch gender{
        case .male:
            return "I am a boy"
        case .female:
            return "I am a girl"
    }

很遗憾,我们收到一个Switch must be exhaustive编译错误,提醒我们要判断完整。如果你实在需要省略部分case的选项的话,可以引入default,来匹配你不关心的选项,比如这样:

    func sayHello()->String{
        switch gender{
        case .female:
            return "I am a girl."
        default:
            return "Hello."
        }
    }

枚举除了和switch语句走得比较近之外,还经常参与迭代,也就是和for循环关系同样紧密。如果想用for循环遍历枚举的每一项的话,我们需要声明该枚举支持迭代,声明的方式非常简单,需要在类名后用:跟随CaseIterable,代码如下:

enum Gender : CaseIterable{
    case male , female , other
}

我们在之前介绍过,如果用类与类的继承关系,会用到:跟随它要继承的父类。而在上面的用法中,并不是在继承父类,属于比较特殊的情况,这个我们以后再详细介绍。话说回来,只要我们追加了魔法般的CaseIterable之后,我们就可以很方便的对枚举进行迭代了。

for gender in Gender.allCases{
    print(gender)
}

输出的结果为:

male
female
other

这里不要疑惑,虽然打印到控制台的内容是字符串。但是本质上for循环体内的gender对应的还是Gender类型,这跟People定义的gender变量类型是完全相同的。如果你想验证一下的话,可以用下面的代码:

for gender in Gender.allCases{
    print(type(of:gender))
}

print(type(of: aGirl.gender))

所以type函数可以帮我们确定一个参数的类型。那么上面神奇的Gender.allCases,咱们也可以观察下。

print(type(of: Gender.allCases))

你将得到结果Array<Gender>,所以它就是个Array,你可以像把玩其他数组一样把玩Gender.allCases,比如这样print(Gender.allCases.count)可以打印该枚举的大小。

其实有了switch,有了for,我们就能对枚举做很多事了。但是某些情况下,可能还是有点力不从心。比如我们尝试构造一个星期的枚举。

enum Week{
    case monday , tuesday , wednesday , thursday , friday , saturday , sunday
}

如果我想判断周末是不是到了,诚然可以去跟.saturday.sunday分别比较一下,也不是不行。但是如果能直接判断一个index>5,那不是很容易么?这就牵扯到枚举类型原始值的概念。在这里,我希望一个星期的每一天,都能对应一个整型数字,所以需要这样写:

enum Week : Int{ // <--注意这里
    case monday , tuesday , wednesday , thursday , friday , saturday , sunday
}

这样我们的每个枚举就拥有了原始值(rawValue),以.monday为例,可以这么访问print(Week.monday.rawValue),略显尴尬的是,看到的结果是0。这也可以理解,计算机世界里,牵扯到计数一般都是从0开始走的。所以我们需要手动给monday分配1,也非常简单,就像这样:

enum Week : Int{
    case monday = 1 , tuesday , wednesday , thursday , friday , saturday , sunday
}

如果你想事无巨细地把枚举里的每个元素都分配一个原始值当然是可以的,但其实swift已经很聪明的帮你把枚举中的后续元素以1为起点,逐个的修正好了。想验证的话,我们可以通过前面的for循环,同时别忘了,要在枚举命名处的:后面追加CaseIterable。最后的代码如下:

enum Week : Int , CaseIterable{
    case monday = 1 , tuesday , wednesday , thursday , friday , saturday , sunday
}

for week in Week.allCases{
    print(week.rawValue)
}

有了原始值的支持,我们获取一个具体的枚举元素就可以用另外一个方式了。比如:

print(Week(rawValue: 7) == Week.sunday)

返回的就是true,足以证明通过原始值拿到的枚举与直接点出来的枚举是一回事。你可能会有疑问,没有原始值的枚举明明也可以工作的不错,那么原始值的加入到底能带来什么呢?想象一下,你的业务是跟一个关系型数据库(SQL)打交道的,最终存储在数据库里的数据都是符合数据库标准的,如果你尝试把Week.sunday存进去的话,你甚至不知道该给这个数据列定义什么类型。而有原始值的支持,你就可以在数据库支持的类型与Swift的枚举类型中做优雅的转换工作了。

还有一点需要注意的是,在上面的例子中,我把原始值的类型定义为Int,只是觉得它正好满足星期几的这种需求。如果你的业务有其他场景,完全可以用其他类型表示原始值。比如下面这个例子:

enum ASCIIControlCharacter: Character {
    case tab = "\t"
    case lineFeed = "\n"
    case carriageReturn = "\r"
}

在前面的例子中,我们都在用枚举表示某种定数,比如从星期一到星期天,不外乎7种情况。但其实swift的枚举设计得更为强大,可以存储变幻多端的数据,请看下面的代码:

enum ServerResponse {
    case ok(String)
    case failure(Int , String)
}

let success = ServerResponse.ok("hello world")
let failure = ServerResponse.failure(502,"Bad Gateway")

switch failure {
case let .ok(context):
    print("Response is \(context)")
case let .failure(stateCode , message):
    print("Failure... \(stateCode)  \(message)")
}

这里的描述的场景是服务器HTTP返回的数据。如果成功的话,就是ok,并提供服务器返回的内容;如果失败的话,要提供一个状态码,以及错误的详细信息。你可以通过替换上面代码中的switch failureswitch success观察不同的输出效果。

上面的演示只是枚举能力的冰山一角,更多高阶的功能就等待你自己去探索发觉了。


参考资料:

往期回顾: