枚举是一种对象类型,其实例表示不同的预定义值,可以将其看作已知可能的一个列表。Swift通过枚举来表示彼此可替代的一组常量。枚举声明中包含了若干case语句。每个case都是一个选择名。一个枚举实例只表示一个选择,即其中的一个case。
比如,在我开发的Albumen应用中,相同视图控制器的不同实例可以列出4种不同的音乐库内容:专辑、播放列表、播客、有声书。视图控制器的行为对于每一种音乐库内容来说存在一些差别。因此,在实例化视图控制器时,我需要一个四路switch进行设置,表示该视图控制器会显示哪一种内容。这就像枚举一样!
下面是该枚举的基本声明;称为Filter,因为每个case都表示过滤音乐库内容的不同方式:
enum Filter { case Albums case Playlists case Podcasts case Books}
该枚举并没有初始化器。你可以为枚举编写初始化器,稍后将会介绍;不过它提供了默认的初始化模式,你可以在大多数时候使用该模式:使用枚举名,后跟点符号以及一个case。比如,如下代码展示了如何创建表示Albums case的Filter实例:
let type = Filter.Albums
作为一种简写,如果类型提前就知道了,那就可以省略枚举的名字,不过前面还是要有一个点。比如:
let type : Filter = .Albums
不能在其他地方使用.Albums,因为Swift不知道它属于哪个枚举。在上述代码中,变量被显式声明为Filter,因此Swift知道.Albums的含义。类似的情况出现在将枚举实例作为实参传递给函数调用时:
func filterExpecter(type:Filter) {}filterExpecter(.Albums)
第2行创建了一个Filter实例并传递给函数,无须使用枚举的名字。这是因为Swift从函数声明中已经知道这里需要一个Filter类型。
在实际开发中,省略枚举名所带来的空间上的节省可能会相当可观,特别是在与Cocoa通信时,枚举类型名通常都会很长。比如:
let v = UIViewv.contentMode = .Center
UIView的contentMode属性是UIViewContentMode枚举类型。上述代码很简洁,因为我们无须在这里显式使用名字UIViewContentMode。.Center要比UIViewContentMode.Center更加整洁,但二者都是合法的。
枚举声明中的代码可以在不使用点符号的情况下使用case名。枚举是个命名空间,声明中的代码位于该命名空间下面,因此能够直接看到case名。
相同case的枚举实例是相等的。因此,你可以比较枚举实例与case来判断它们是否相等。第1次比较时就获悉了枚举的类型,因此第2次之后就可以省略枚举名字了:
func filterExpecter(type:Filter) { if type == .Albums { print(/"it/'s albums/") }}filterExpecter(.Albums) // /"it/'s albums/"
4.2.1 带有固定值的Case
在声明枚举时,你可以添加类型声明。接下来,所有case都会持有该类型的一个固定值(常量)。如果类型是整型数字,那么值就会隐式赋予,并且默认从0开始。在如下代码中,.Mannie持有值0,.Moe持有值1,以此类推:
enum PepBoy : Int { case Mannie case Moe case Jack}
如果类型为String,那么隐式赋予的值就是case名字的字符串表示。在如下代码中,.Albums持有值/"Albums/",以此类推:
enum Filter : String { case Albums case Playlists case Podcasts case Books}
无论类型是什么,你都可以在case声明中显式赋值:
enum Filter : String { case Albums = /"Albums/" case Playlists = /"Playlists/" case Podcasts = /"Podcasts/" case Books = /"Audiobooks/"}
以这种方式附加到枚举上的类型只能是数字与字符串,赋的值必须是字面值。case所持有的值叫作其原生值。该枚举的一个实例只会有一个case,因此只有一个固定的原始值,并且可以通过rawValue属性获取到:
let type = Filter.Albumsprint(type.rawValue) // Albums
让每个case都有一个固定的原始值会很有意义。在我开发的Albumen应用中,Filter case持有的就是上述String值,当视图控制器想获取标题字符串并展现在屏幕顶部时,它只需获取到当前类型的rawValue即可。
与每个case关联的原生值在当前枚举中必须唯一;编译器会强制施加该规则。因此,我们还可以进行反向匹配:给定一个原生值,可以得到与之对应的case。比如,你可以通过rawValue:初始化器实例化具有该原生值的枚举:
let type = Filter(rawValue:/"Albums/")
不过,以这种方式来实例化枚举可能会失败,因为提供的原生值可能不对应任何一个case;因此,这是一个可失败初始化器,其返回值是Optional。在上述代码中,type并非Filter,它是个包装了Filter的Optional。这可能不那么重要,不过由于你要做的事情很可能是比较枚举与其case,因此可以使用Optional而无须展开。如下代码是合法的,并且执行正确:
let type = Filter(rawValue:/"Albums/")if type == .Albums { // ...
4.2.2 带有类型值的Case
4.2.1节介绍的原生值是固定的:给定的case会持有某个原生值。此外,你可以构建这样一个case,其常量值是在实例创建时设置的。为了做到这一点,请不要为枚举声明任何类型;相反,请向case的名字附加一个元组类型。通常来说,该元组中只会有一个类型;因此,其形式就是圆括号中会有一个类型名,其中可以声明任何类型,如下示例所示:
enum Error { case Number(Int) case Message(String) case Fatal}
上述代码的含义是:在实例化期间,带有.Number case的Error实例必须要赋予一个Int值,带有.Message case的Error实例必须要赋予一个String值,带有.Fatal case的Error实例不能赋予任何值。带有赋值的实例化实际上会调用一个初始化函数;若想提供值,你需要将其作为实参放到圆括号中:
let err : Error = .Number(4)
这里的附加值叫作关联值。这里所提供的实际上是个元组,因此它可以包含字面值或值引用;如下代码是合法的:
let num = 4let err : Error = .Number(num)
元组可以包含多个值,可以提供名字,也可以不提供名字;如果值有名字,那么必须在初始化期间使用:
enum Error { case Number(Int) case Message(String) case Fatal(n:Int, s:String)}let err : Error = .Fatal(n:-12, s:/"Oh the horror/")
声明了关联值的枚举case实际上是个初始化函数,这样就可以捕获到对该函数的引用并在后面调用它:
let fatalMaker = Error.Fatallet err = fatalMaker(n:-1000, s:/"Unbelievably bad error/")
第5章将会介绍如何从这样的枚举实例中提取出关联值。
下面我来揭示Optional的工作原理。Optional实际上是一个带有两个case的枚举:.None与.Some。如果为.None,那么它就没有关联值,并且等于nil;如果为.Some,那么它就会将包装值作为关联值。
4.2.3 枚举初始化器
显式的枚举初始化器必须要实现与默认初始化相同的工作:它必须返回该枚举特定的一个case。为了做到这一点,请将self设定给case。在该示例中,我扩展了Filter枚举,使之可以通过数字参数进行初始化:
enum Filter: String { case Albums = /"Albums/" case Playlists = /"Playlists/" case Podcasts = /"Podcasts/" case Books = /"Audiobooks/" static var cases : [Filter] = [Albums, Playlists, Podcasts, Books] init(_ ix:Int) { self = Filter.cases[ix] }}
现在有3种方式可以创建Filter实例:
let type1 = Filter.Albumslet type2 = Filter (rawValue:/"Playlists/")!let type3 = Filter (2) // .Podcasts
在该示例中,如果调用者传递的数字超出了范围(小于0或大于3),那么第3行将会崩溃。为了避免这种情况的出现,我们可以将其作为可失败初始化器,如果数字超出了范围就返回nil:
enum Filter: String { case Albums = /"Albums/" case Playlists = /"Playlists/" case Podcasts = /"Podcasts/" case Books = /"Audiobooks/" static var cases : [Filter] = [Albums, Playlists, Podcasts, Books] init!(_ ix:Int) { if !(0...3).contains(ix) { return nil } self = Filter.cases[ix] }}
一个枚举可以有多个初始化器。枚举初始化器可以通过调用self.init(...)委托给其他初始化器,前提是在调用链的某个点上将self设定给一个case;如果不这么做,那么枚举将无法编译通过。
该示例改进了Filter枚举,这样它可以通过一个String原生值进行初始化而无须调用rawValue:。为了做到这一点,我声明了一个可失败初始化器,它接收一个字符串参数,并且委托给内建的可失败rawValue:初始化器:
enum Filter: String { case Albums = /"Albums/" case Playlists = /"Playlists/" case Podcasts = /"Podcasts/" case Books = /"Audiobooks/" static var cases : [Filter] = [Albums, Playlists, Podcasts, Books] init!(_ ix:Int) { if !(0...3).contains(ix) { return nil } self = Filter.cases[ix] } init!(_ rawValue:String) { self.init(rawValue:rawValue) }}
现在有4种方式可以创建Filter实例:
let type1 = Filter.Albumslet type2 = Filter (rawValue:/"Playlists/")let type3 = Filter (2) // .Podcastslet type4 = Filter (/"Playlists/")
4.2.4 枚举属性
枚举可以拥有实例属性与静态属性,不过有一个限制:枚举实例属性不能是存储属性。这是有意义的,因为如果相同case的两个实例拥有不同的存储实例属性值,那么它们彼此之间就不相等了——这有悖于枚举的本质与目的。
不过,计算实例属性是可以的,并且属性值会根据self的case发生变化。如下示例来自于我所编写的代码,我将搜索函数关联到了Filter枚举的每个case上,用于从音乐库中获取该类型的歌曲:
enum Filter : String { case Albums = /"Albums/" case Playlists = /"Playlists/" case Podcasts = /"Podcasts/" case Books = /"Audiobooks/" var query : MPMediaQuery { switch self { case .Albums: return MPMediaQuery.albumsQuery case .Playlists: return MPMediaQuery.playlistsQuery case .Podcasts: return MPMediaQuery.podcastsQuery case .Books: return MPMediaQuery.audiobooksQuery } }
如果枚举实例属性是个带有Setter的计算变量,那么其他代码就可以为该属性赋值了。不过,代码中对枚举实例的引用必须是个变量(var)而不能是常量(let)。如果试图通过let引用为枚举实例属性赋值,那么编译器就会报错。
4.2.5 枚举方法
枚举可以有实例方法(包括下标)与静态方法。编写枚举方法是相当直接的。如下示例来自于我之前编写的代码。在纸牌游戏中,每张牌分为矩形、椭圆与菱形。我将绘制代码抽象为一个枚举,它会将自身绘制为一个矩形、椭圆或菱形,取决于其case的不同:
enum ShapeMaker { case Rectangle case Ellipse case Diamond func drawShape (p: CGMutablePath, inRect r : CGRect) -> { switch self { case Rectangle: CGPathAddRect(p, nil, r) case Ellipse: CGPathAddEllipseInRect(p, nil, r) case Diamond: CGPathMoveToPoint(p, nil, r.minX, r.midY) CGPathAddLineToPoint(p, nil, r.midX, r.minY) CGPathAddLineToPoint(p, nil, r.maxX, r.midY) CGPathAddLineToPoint(p, nil, r.midX, r.maxY) CGPathCloseSubpath(p) } }}
修改枚举自身的枚举实例方法应该被标记为mutating。比如,一个枚举实例方法可能会为self的实例属性赋值;虽然这是个计算属性,但这种赋值还是不合法的,除非将该方法标记为mutating。枚举实例方法甚至可以修改self的case;不过,方法依然要标记为mutating。可变实例方法的调用者必须要有一个对该实例的变量引用(var)而非常量引用(let)。
在该示例中,我向Filter枚举添加了一个advance方法。想法在于case构成了一个序列,序列可以循环。通过调用advance,我可以将Filter实例转换为序列中的下一个case:
enum Filter : String { case Albums = /"Albums/" case Playlists = /"Playlists/" case Podcasts = /"Podcasts/" case Books = /"Audiobooks/" static var cases : [Filter] = [Albums, Playlists, Podcasts, Books] mutating func advance { var ix = Filter.cases.indexOf(self)! ix = (ix + 1) % 4 self = Filter.cases[ix] }}
下面是调用代码:
var type = Filter.Bookstype.advance // type is now Filter.Albums
(下标Setter总被认为是mutating,不必显式标记。)
4.2.6 为何使用枚举
枚举是个拥有状态名的switch。很多时候我们都需要使用枚举。你可以自己实现一个多状态值;比如,如果有5种可能的状态,你可以使用一个值介于0到4之间的Int。不过接下来还有不少工作要做,要确保不会使用其他值,并且要正确解释这些数值。对于这种情况来说,5个具名case会更好一些!即便只有两个状态,枚举也比Bool好,这是因为枚举的状态拥有名字。如果使用Bool,那么你就得知道true与false到底表示什么;借助枚举,枚举的名字与case的名字会告诉你这一切。此外,你可以在枚举的关联值或原生值中存储额外的信息,但Bool却做不到这些。
比如,在我实现的LinkSame应用中,用户可以使用定时器开始真正的游戏,也可以不使用定时器进行练习。在代码的不同位置处,我需要知道进行的是真正的游戏还是练习。游戏类型是枚举的case:
enum InterfaceMode : Int { case Timed = 0 case Practice = 1}
当前的游戏类型存储在实例属性interfaceMode中,其值是个InterfaceMode。这样就可以轻松根据case的名字设定游戏了:
// ... initialize new game ...self.interfaceMode = .Timed
也可以轻松根据case名字检测游戏类型:
// notify of high score only if user is not just practicingif self.interfaceMode == .Timed { // ...
那原生整型值起什么作用呢?它们对应于界面中UISegmentedControl的分割索引。当修改了interfaceMode属性时,Setter观察者会选择UISegmentedControl中相应的分割部分(self.timedPractice),这只需获取到当前枚举case的rawValue即可:
var interfaceMode : InterfaceMode = .Timed { willSet (mode) { self.timedPractice?.selectedSegmentIndex = mode.rawValue }}