映月读书网 > iOS编程基础:Swift、Xcode和Cocoa入门指南 > 4.2 枚举 >

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 = UIView
v.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.Albums
print(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 = 4
let 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.Fatal
let 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.Albums
let 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.Albums
let type2 = Filter (rawValue:\"Playlists\")
let type3 = Filter (2) // .Podcasts
let 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.Books
type.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 practicing
if self.interfaceMode == .Timed { // ...
  

那原生整型值起什么作用呢?它们对应于界面中UISegmentedControl的分割索引。当修改了interfaceMode属性时,Setter观察者会选择UISegmentedControl中相应的分割部分(self.timedPractice),这只需获取到当前枚举case的rawValue即可:


var interfaceMode : InterfaceMode = .Timed {
    willSet (mode) {
        self.timedPractice?.selectedSegmentIndex = mode.rawValue
    }
}