首页 » iOS编程基础:Swift、Xcode和Cocoa入门指南 » iOS编程基础:Swift、Xcode和Cocoa入门指南全文在线阅读

《iOS编程基础:Swift、Xcode和Cocoa入门指南》4.2 枚举

关灯直达底部

枚举是一种对象类型,其实例表示不同的预定义值,可以将其看作已知可能的一个列表。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    }}