讀古今文學網 > 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
    }
}