第六章 枚舉和switch語句

許多編程語言都有一個共同的特點,有一個叫枚舉的類型enum 。枚舉是將相似的數據組合在一起。例如,在Cocoa中處理文本的對齊問題時,你可能會看到如NSTextAlignment.Center之類的枚舉值。NStextAlignment枚舉可以讓你用較爲輕鬆且易讀的名字來標記不同的類型,就像Center或Left。

swfit的枚舉類型不僅僅是一個枚舉。相對於像傳統的枚舉有離散的值,更像是一個類或結構。在Swift中,枚舉可以關聯相關的方法甚至是構造函數!

在這一章中,你將使用playground來學習並練習如何將枚舉和switch語句運用在你的app中。switch語句和枚舉在一起使用非常強大。你將看到這兩個基本功能組合在一起時是如何強大的編程技術。

騰空下大腦,準備下瞭解枚舉吧!

Basic enumerations - 枚舉基本用法

枚舉是多種語言裏的語句塊,開發人員可以定義一組值來一起構成一個類型。

卡片遊戲用來舉例子蠻合適的。一個卡片遊戲的app可能會用到一個枚舉來描述下列幾種情況:梅花,方塊,黑桃,紅桃:

enum Suit { 
    case Heart 
    case Club 
    case Diamond 
    case Spade 
} 

在Swift中定義枚舉的方式和c或oc是非常像的。讓我們看看在Swift中是如何使用枚舉的。

新建一個playground並將下面的代碼填入其中:

enum Shape { 
    case Rectangle 
    case Square 
    case Triangle 
    case Circle 

聲明瞭一個叫Shape的枚舉,裏面有四種狀態,每種狀態都是一個枚舉值。

既然你已經定義了枚舉,現在你就可以將這個枚舉作爲是一種類型進行使用了。後面緊接着添加代碼:

var aShape = Shape.Triangle 

注意你引用這個值時使用的語法:Shape是枚舉的名字,Triangle是枚舉裏面的類型。

如果你定義了變量的類型,則你可以直接從你和定義的類型名相同的枚舉中獲取值,就像下面這樣:

var aShape: Shape = .Triangle 

聲明瞭這個類型是Shape後,你可以直接修改原來變量的值,如下:
aShape = .Square

Raw values - 原始值

如果你是oc開發者,你可能知道每個枚舉值都可以分配一個值。Swift也提供了這個相同的功能,你還會看到Swift的枚舉比oc的更強。

修改Shape的定義如下:

enum Shape: Int { 
    case Rectangle 
    case Square 
    case Triangle 
    case Circle 
} 

你肯定會不由自主的想Shape是否是繼承了Int類型,畢竟語法和類以及結構的繼承太像了。在枚舉中,這個語法表示枚舉持有的原始值是Int類型。

現在你的枚舉有了原始值的類型,枚舉也有兩個方法:rawValue和rawValue:。在playground中添加如下兩行:

var triangle = Shape.Triangle 
triangle.rawValue

rawValue返回和提供的枚舉值相關聯的原始值,查看playground的右邊欄,你可以看到Triangle的原始值是2:

正如你猜的那樣,原始值是從0開始的,Rectangle是0,Square是1,Triangle是2,Circle是3.

與此相反的是rawValue:將原始值轉換爲對應的枚舉值,繼續往後添加代碼:

var square = Shape(rawValue: 1)

你可能也注意到右邊欄的輸出有點不一樣了,你會看到返回的枚舉值是可選類型。因爲並不是每個整數都有着對應的枚舉值,所以要返回一個可選類型。

在後面繼續添加如下代碼:

var notAShape = Shape(rawValue: 100)

注意到右邊欄顯示對應的值是nil。

如果你願意,你也可以給每個枚舉都定義一個特殊的值,而不是讓Swift自動給他們賦值。修改其中一個枚舉值如下:

case Rectangle = 10 

注意下現在Triangle有個原始值是12了。當你的枚舉是個整數時,裏面的每個枚舉值都可以不用特意去分配值,Swift會自動根據前一個枚舉值的原始值加1賦值。

使用原始值的一個主要原因是你可以序列化也可以反序列化枚舉的值。舉個例子,枚舉的類型只是一個符號,並不能像數字或字符串那樣在在服務器請求json。如果你給每個枚舉值都定義了值,你就可以在服務器上進行處理了。然後服務器和app就可以進行交互,可以將值直接映射到枚舉值上了。

枚舉值的類型並不是僅限於只可以使用Int,可以是任何的數據類型,例如Float和Double,他甚至可以是String。

挑戰:修改Shape的原始值類型爲Sting。立馬你就會注意到編譯器報錯。這是因爲如果你的類型不是Int(或者文字可轉換爲Int),則你就必須爲每一個枚舉值設置原始值。

下面舉個例子:

enum Shape: String {
case Rectangle = "Rectangle" 

你需要爲你的每個枚舉值都設置一個值。以及改變rawValue:方法調用時的參數類型。

繼續往後添加代碼:

enum Ratios: Float { 
    case pi = 3.141 
    case tau = 6.283 
    case phi 
} 

編譯器報錯,如果枚舉不是integer類型時,需要爲所有的枚舉賦一個原始值。整數值可以自動在前一個枚舉值的原始值的基礎上加1.

Switch statements - switch語句

當枚舉和Switch成對出現時,使用非常的安全。最關鍵的是你可以使用switch語句去測試匹配一個枚舉值。

在Swift中,switch的語句比其他語言多了一些使用技巧,現在來看一下。

新建一個playground,並添加代碼:

enum Shape { 
    case Rectangle 
    case Square 
    case Triangle 
    case Circle 
} 

這個和前面創建的簡單枚舉一樣。接着,在代碼後面繼續添加:

var aShape = Shape.Rectangle 
switch(aShape) { 
    case .Rectangle: 
    print("This is a rectangle") 
} 

如果你是oc開發員,則對這個語法一定很熟悉。聲明瞭一個switch並去匹配aShape的枚舉值。

但是此處編譯器報錯提示你還沒有完整的列出所有不同枚舉值的匹配情況。

在Swift中的switch必須要十分的詳盡,也就是說你需要處理枚舉的每一個匹配情況。這是一種安全機制,因爲我們經常會因爲疏忽大意而漏寫了某個匹配情況造成bug,尤其是在匹配枚舉值的時候!在其他語言中,你可以只添加一個枚舉值而switch會跳過不匹配的情況,在Swift中則會拋出錯誤進行提示。

修改爲完整的switch如下:

switch(aShape) {

    case .Rectangle:

        print("This is a rectangle") 
    case .Square:

        print("This is a square") 
    default: break 
} 

添加了一個匹配Square的情況,同時也添加了一個default的匹配情況用於處理其他沒有進行匹配的情況。你會看到調試顯示“This is a rectangle ”,匹配了aShape的值。

現在的這個語句塊非常的詳盡,因爲他分別獨立的處理了Rectangle,Square以及其他沒有匹配情況的值。

如果你對其他語句中的switch語句熟悉的話,你可能已經發現這個switch中的前兩句沒有break。你可能會認爲控制器在匹配了Rectangle後,還會繼續往下匹配。在其他語言如oc中確實如此,他們都有貫通的情況,依靠break來跳出switch,如果沒有則會繼續往下匹配不同的狀態。

然而Swift不會有貫通的情況。沒有貫通的情況可以消除因爲疏忽忘寫break造成的bug。所以我得謝謝你咯<( ̄3 ̄)> ,Swift!

在上面的代碼中default裏面必須要寫一個break,因爲不能在case裏放一個空的代碼,這是唯一需要用到break的時候。

Swift也可以實現匹配多個情況達到和貫通一樣的效果,修改代碼如下:

    case .Rectangle, .Square: 
        print("This is a quadrilateral") 
    case .Circle: 
        print("This is a circle") 
    default: break 
}

正如你所見的,Swift一次性匹配多個情況的方法非常的簡單。使用逗號(不用單獨的寫一行)來分割不同的枚舉值,不僅讀起來更加簡潔,而且摒除了忘記寫break造成的bug。

本章的剩下內容你還會瞭解到更多的和switch語句有關的姿勢。

Associated values - 關聯值

到目前爲止,你都可能認爲Swift的枚舉和其他語言的枚舉並沒有什麼不同的,現在就來爲大家展示下不同之處。

Swift允許你爲每一個枚舉值都分配一個或多個值,叫關聯值。這個特點讓枚舉在Swift中異常強大。

現在就來看個例子,在新的playground中添加如下代碼:

enum Shape {

    case Rectangle(Float, Float) 
    case Square(Float)

    case Triangle(Float, Float) 
    case Circle(Float) 
} 

和前面一樣,你聲明瞭一個叫Shape的枚舉。然而這一次你給每個枚舉值都聲明瞭一個關聯值。關聯值中的括號裏指明瞭他們的類型。

在這個例子中,你使用相關值來保存不同形狀的大小。兩個浮點數表示長方形的寬和高,正方形的邊長,三角形的底和高以及圓的半徑。

現在你可以在生成枚舉值時添加相關值。playground添加代碼:

var rectangle = Shape.Rectangle(5, 10)

生成了一個寬是5,高是10的新的Rectangle值。注意,這很難說清在這種情況下每個參數是什麼,第一個參數是寬還是高?爲了提示,你可以給每個參數命名,如下:

enum Shape {

    case Rectangle(width: Float, height: Float) 
    case Square(Float)

    case Triangle(base: Float, height: Float) 
    case Circle(Float) 
} 

在添加了名字後,前面對變量rectangle變量的聲明就必須修改了:

var rectangle = Shape.Rectangle(width: 5, height: 10) 

這看起來有點像類和結構了!在這點上,你可能會想嘗試用rectangle.width方式去訪問相關的值。

結果不足爲奇,你並不能像結構和類訪問屬性那樣直接訪問關聯值。關聯值和原始值不同,原始值提供了一種用其他方式展示枚舉內容的方式,例如使用interger時可以序列化或反序列化每個枚舉的值。另一點不同的是,你只能在switch語句中訪問到枚舉的關聯值。

讓我們看看他是如何工作的,在playground的底部繼續添加代碼:

switch (rectangle) {

        case .Rectangle(let width, let height): 
            print("Rectangle: \(width) x \(height)") 
        default: 
            print("Other shape") 
} 

控制檯輸出:

這裏寫圖片描述

上面的代碼和普通的switch看似並沒有什麼不同,除了在case中添加了對應的相關值中綁定了參數寬和高。

但是等等,這裏還有更多和相關值有關的技巧。switch語句不僅可以讀取枚舉的值。修改代碼:

switch (rectangle) {

        case .Rectangle(let width, let height) where width <= 10: 
            print("Narrow rectangle: \(width) x \(height)") 
        case .Rectangle(let width, let height): 
            print("Wide rectangle: \(width) x \(height)") 
        default:

            print("Other shape") 
} 

現在switch語句中有兩個匹配Rectangle的情況。注意下第一個匹配,添加了一個where子句用於匹配特定的Rectangle值-寬度不大於10.

因爲你的rectangle的寬度是5,所以你將看到輸出Narrow rectangle。

你也可以在where 的子句後面再做些其他事情,比如,修改下代碼:

case .Rectangle(let width, let height) where width == height: 
    print("Square: \(width) x \(height)") 
case .Square(let side): 
    print("Square: \(side) x \(side)") 

你用了一個長寬相同來匹配一個正方形。乾的漂亮!

不過請注意,case的順序很重要,來看看爲啥,將Rectangle的匹配移動到switch的最頂部:

switch (rectangle) {
   case .Rectangle(let width, let height): 
        print("Wide rectangle: \(width) x \(height)")
  case .Rectangle(let width, let height) where width == height: 
        print("Square: \(width) x \(height)") 
    case .Square(let side): 
        print("Square: \(side) x \(side)")
 case .Rectangle(let width, let height) where width <= 10: 
        print("Narrow rectangle: \(width) x \(height)") 
    default: 
        print("Other shape") 
}

現在控制檯輸出:

這裏寫圖片描述

這是你期望的嗎?寬度小於10那行沒有輸出呢?爲什麼匹配的不是另一個Rectangle。這說明switch語句只會使用第一個相匹配的情況。

挑戰:寫一下其他的值來匹配Square,Triangle和Circle。你的匹配情況應該如下:
1.Triangle的高大於10
2.Triangle的高是底的兩倍
3.Circle的半徑小於5

Enums as types - 枚舉的類型

在Swift中,枚舉在很多方面都和類與結構體很像。他有和結構一樣相同的值語義,這也就是說他通過值傳遞到函數中與結構一樣是複製。枚舉也可以有實例方法,就像類和結構一樣。

新建playground並添加代碼:

enum Shape {

    case Rectangle(width: Float, height: Float) 
    case Square(Float)

    case Triangle(base: Float, height: Float) 
    case Circle(Float) 
}

接着在在枚舉中添加方法:

func area() -> Float {

    switch(self) {

        case .Rectangle(let width, let height): 
            return width * height 
        case .Square(let side): 
            return side * side

        case .Triangle(let base, let height): 
            return 0.5 * base * height 
        case .Circle(let radius):

            return Float(M_PI) * powf(radius, 2) 
    } 
} 

現在你應該相信爲什麼我會說Swift的枚舉比oc強很多了吧!

這裏添加了一個方法用來獲取Shape枚舉下的不同形狀的面積。自己本身會確認到底使用哪一個公式來進行計算,底部繼續添加代碼:

var circle = Shape.Circle(5) 
circle.area() 

你可以在右邊欄中看到計算結果:
這裏寫圖片描述
Perfect!你現在可以計算這些形狀的面積了。當然,你還有許多的其他類型的方法可以添加到Shape中。

挑戰:添加一個計算周長的方法在Shape枚舉中。他應該能計算形狀所有邊的長的總和。

你甚至可以在枚舉中實現初始化,在枚舉聲明代碼的最底部繼續添加:

init(_ rect: CGRect) {

    let width = Float(CGRectGetWidth(rect)) 
    let height = Float(CGRectGetHeight(rect)) 
    if width == height { 
        self = Square(width) 
    } else { 
        self = Rectangle(width: width, height: height) 
    } 
}

允許Shape的構造器使用CGRect。在初始化中,你分配值給自己,而不是返回一個值。在這個例子中,如果CGRect的邊長相同,則方法生成一個正方形Square,否則創建一個矩形Rectangle。

在playground的最底部添加代碼:

var shape = Shape(CGRect(x: 0, y: 0, width: 5, height: 10)) 
shape.area()

利用參數CGRect使用一個新的構造器生成了一個Shape,看一下右邊的輸入內容,正如你所期待的那樣,計算的面積值爲50.0。

枚舉的初始化必須爲self分配個內容,否則編譯器報錯,比如:

init(_ string: String) { 
    switch(string) {

        case "rectangle": 
            self = Rectangle(width: 5, height: 10) 
        case "square": 
            self = Square(5) 
        case "triangle": 
            self = Triangle(base: 5, height: 10) 
        case "circle": 
            self = Circle(5) 
        default: 
            break 
    } 
} 

儘管上面的初始化中基本所有情況都覆蓋了,但是switch語句中default裏沒有給self分配內容,所以依然編譯不通過。

這樣的初始化用一個工廠方法更好,在枚舉代碼底部添加:

static func fromString(string: String) -> Shape? { 
    switch(string) {

        case "rectangle": 
            return Rectangle(width: 5, height: 10) 
        case "square": 
            return Square(5) 
        case "triangle": 
            return Triangle(base: 5, height: 10) 
        case "circle": 
            return Circle(5) 
        default: 
            return nil 
    } 
} 

該方法是靜態的,意味着他屬於類型本身而不是任何的實例。

該方法使用了一個string並返回相關的Shape值。使用可選類型作爲返回值,允許返回nil,以防傳入的string匹配不到任何shape情況。

現在在最底部添加測試代碼:

if let anotherShape = Shape.fromString("rectangle") { 
    anotherShape.area() 
} 

這裏使用了一個新的工廠方法通過字符串來生成了一個shape。因爲返回的是一個可選值,所以他需要用解包。在這裏,使用的是if語句來打開綁定的內容給一個變量。

Optionals are enums - 可選類型是枚舉

Swift的可選類型是一個枚舉,這可能會讓你有點吃驚。他的實現基本和下面一樣(具體的細節刪除掉了)

enum Optional<T> : NilLiteralConvertible { 
    case None
  case Some(T) 
    init()
 init(_ some: T) 
    static func convertFromNilLiteral() -> T? 
} 

第一個case是None,也就是可選類型中的nil。第二個case是Some,即可選類型中包含的值。

這裏他使用了泛型允許其可選類型可以包含任何類型的值。同樣的對Some使用了關聯值。

可選的類型也表明,枚舉可以實現協議,就像其他完整類型的類和結構。

NiLiteraConvertible的協議允許你使用可選的nil-編譯器自動將其轉換爲一個調用convertFromNiLiteral的方法類型。

這些都說明了Swift中枚舉的強大,遠超過在oc中和他同名的枚舉。

JSON parsing using enums - 用枚舉來解析JSON

上面的內容聽起來可能很抽象。Shapes是有趣的,但不是大多數應用都會使用到shapes。你已經看了一些枚舉和可選類型一起使用的例子。他們是很強大,但是事實上你很少能看到枚舉用法。

讓我們來看一下另一個現實生活中的例子。JSON解析在應用中是非常常見的。在Swift中,他是類型安全的,但相對的是繁瑣的JSON解析響應的數據。你會得到大量的嵌套,讓我們來看看。

Parsing JSON the hard way - 解析JSON

創建一個新的playground,然後添加代碼:

let json = "{\"success\":true,\"data\":{\"numbers\":[1,2,3,4,5],\"animal\":\" dog\"}}"
if let jsonData = (json as NSString).dataUsingEncoding(NSUTF8StringEncoding)
{
    let parsed: AnyObject? = try NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions(rawValue: 0))

    // Actual JSON parsing section
    if let parsed = parsed as? [String:AnyObject] {
        if let success = parsed["success"] as? NSNumber {
            if success.boolValue == true {
                if let data = parsed["data"] as? NSDictionary {
                    if let numbers = data["numbers"] as? NSArray {
                        print(numbers)
                    }
    if let animal = data["animal"] as? NSString { 
                        print(animal) 
                    } 
                }   
            } 
        } 
    } 
} 

滿滿的一屏幕代碼是個什麼鬼!這裏使用了NSDictionary,NSArray等的。這麼複雜的寫法,白瞎了Swift的安全特性。在Swift中,這並不是唯一解析JSON的方法,但大多數方法總是最終被嵌套的相當複雜。

Introducing JSON.swift - 引入JSON.swift

相反,你可以使用枚舉讓其變得簡單。在第四章“泛型”中,在你不知道的時候就使用了JSON解析。Flickr類需要解析從FlickrAPI過來的信息。你可能點進去看過,也可能沒有。現在讓我們來看看他是怎麼工作的吧。

在這一章的資源文件中找到這個文件。打開叫JSON.swift的文件來看一下。他是個枚舉!這使得json的解析明顯的變得更加的容易。讓我們看看他是怎麼工作的。

首先看看枚舉的定義:

enum JSONValue {

    case JSONObject([String:JSONValue]) 
    case JSONArray([JSONValue])

    case JSONNumber(NSNumber)

    case JSONNull 
} 

這是解決這個問題的核心部分。每個在JSON中的值都可以是一個對象,一個數組,字符串,數字,布爾值或null。一個字典的對象需要一個字符串和JSONValue,一個數組是一個JSON值的數組。這完美的描述了一個枚舉的關聯值。so,這就是我們接下來要做的。

通過這種方式,你可以用一個json對象來描述枚舉下每一個JSONValue值。

大多數的JSON解析需要用string關鍵字來讀取。在之前的例子中,data和success值是從外部讀取到的。這裏可以使用下標subscript。下面是在枚舉中的下標定義:

subscript(key: String) -> JSONValue? { 
get { 
    switch self {

        case .JSONObject(let value): 
            return value[key] 
    default: return nil 
        }   
    } 
} 

注意這個下標語法返回的是一個可選類型的JSONValue。也就是說如果這個對象不存在則返回nil。同樣的,如果JSONValue不是個對象,同樣也以nil返回。如果你查看下JSON.swift 文件那麼你將看到一個整數參數的下標函數。和這個下標函數十分相似,但是是用來讀數組的值。

你還能夠從對象和數組中獲得更多的JSONValues,但怎麼從這個value中獲取到實際的字符串,Bool等對象呢?需要通過如下的屬性計算獲取:

var string: String? {

    switch self {

        case .JSONString(let value): 
            return value 
        default: 
            return nil 
    } 
}

就像這個下標語法,屬性返回一個可選類型。如果JSONValue是一個JSONString類型則應該是個字符串,所以返回他的關聯值,否則返回一個nil。其他的匹配類型也是同樣的使用這種屬性計算方法訪問。

最後,還需要一個方法將讀到的對象讀入到JSONValue中。用來將字典,數組或其他任何類型的對象表示到JSONValue的枚舉中。就像這樣:

static func fromObject(object: AnyObject) -> JSONValue? { 
    switch object {
        case let value as NSString: 
            return JSONValue.JSONString(value) 
        case let value as NSNumber: 
            return JSONValue.JSONNumber(value) 
        case let value as NSNull: 
            return JSONValue.JSONNull 
        case let value as NSDictionary: 
            var jsonObject: [String:JSONValue] = [:] 
            for (k: AnyObject, v: AnyObject) in value { 
                if let k = k as? NSString { 
                    if let v = JSONValue.fromObject(v) { 
                        jsonObject[k] = v 
                    } else {
                        return nil 
                    } 
                } 
            } 
            return JSONValue.JSONObject(jsonObject) 
        case let value as NSArray: 
            var jsonArray: [JSONValue] = [] for v in value { 
                if let v = JSONValue.fromObject(v) { 
                    jsonArray.append(v) 
                } else { 
                    return nil 
                } 
            } 
            return JSONValue.JSONArray(jsonArray) 
        default: 
            return nil 
    } 
} 

該方法使用了NSJSONSerialization,返回瞭如NSArray和NSDictionary等基礎對象。方法檢測了對象的類型並返回一個相關的JSONValue實例。

Putting it into practice - 實際演練

現在就讓我將JSON.swift的文件運用練習下!將這個文件中的代碼全部複製粘貼到一個playground中。你必須使用複製粘貼,因爲playground不能引用其他的文件。

現在在後面繼續添加代碼:

if let jsonData =(json as NSString).dataUsingEncoding(NSUTF8StringEncoding) 
{
  if let parsed: AnyObject = NSJSONSerialization.JSONObjectWithData( jsonData,
       options: NSJSONReadingOptions.fromRaw(0)!, error: nil) 
    {
      if let jsonParsed = JSONValue.fromObject(parsed) { 
            // Actual JSON parsing section 
            if jsonParsed["success"]?.bool == true {
               if let numbers = jsonParsed["data"]?["numbers"]?.array { 
                    print(numbers) 
                } 
            if let animal = jsonParsed["data"]?["animal"]?.string { 
                print(animal) 
                } 
            } 
        } 
    } 
} 

頂部有一個額外的if語句,因爲JSONValue.fromObject()返回的是一個可選類型所以在這裏需要使用到解包。然而,實際上這裏的JSON解析部分已經從5層嵌套變爲了現在的2層。

注意這裏使用了可選鏈來解鎖可選類型。例如,如果當在解析number時“data”這個key不存在,則這個表達式會返回一個nil且”numbers“key鍵不會調用。

同樣的如果“animal”key存在但不是一個字符串則if語句會返回nil不會執行print方法。

想必前面的代碼你保留住了Swift的安全特性,還有什麼比這更重要的?!我想現在你也會同意枚舉的強大了吧。所以你應該在編程的時候充分的利用好他。當你有一個可以組一組預定義不同東西的類型如JSON時,這個枚舉實在是太實用了。

Where to go from here? - 接着幹什麼?

在這一章中,你創建了一個簡單的枚舉並實現了枚舉額外的內容原始值和關聯值。
你還使用了switch語句來檢查枚舉的值。此外,你還看到了在Swift中,枚舉遠超其他傳統編程語言如oc的優點,例如詳盡的檢查以及先進的匹配模式。

最後,你看到了如何將枚舉關聯到方法和初始化構造器中,通過研究發現可選類型也是一個枚舉!

你可能會發現你在Swift中會頻繁的使用枚舉,因爲相比在oc等語言中,Swift的枚舉可以讓你做更多的事情。

將你所學的技巧付諸實踐,並利用枚舉完善你的應用程序!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章