SwiftInDepth_02_使用枚舉構(gòu)建數(shù)據(jù)模型

使用枚舉構(gòu)建數(shù)據(jù)模型

1. 使用結(jié)構(gòu)體構(gòu)建數(shù)據(jù)模型

1. 引入枚舉之前我們先看下如何使用struct構(gòu)建消息模型

場景:直播消息類型有

  • 加入消息
  • 退出消息
  • 發(fā)文字消息
  • 發(fā)圖片消息

1. 每種消息都有userId 和 date

struct Message {
    let userId: String
    let contents: String?
    let date: Date
    
    let hasJoined: Bool
    let hasLeft: Bool
    
    let isBeingDrafted: Bool
    let isSendingBalloons: Bool
}

2. 創(chuàng)建消息

let joinMessage = Message(userId: "1",
                          contents: nil,
                          date: Date(),
                          hasJoined: true, // We set the joined boolean
                          hasLeft: false,
                          isBeingDrafted: false,
                          isSendingBalloons: false)


let textMessage = Message(userId: "2",
                          contents: "Hey everyone!", // We pass a message
                          date: Date(),
                          hasJoined: false,
                          hasLeft: false,
                          isBeingDrafted: false,
                          isSendingBalloons: false)

// chatroom.sendMessage(joinMessage)
// chatroom.sendMessage(textMessage)

3. 假設(shè)消息參數(shù)hasJoined hasLeft 都為true,則消息就無法區(qū)分是進(jìn)入還是離開聊天室

let brokenMessage = Message(userId: "1",
                            contents: "Hi there", // We have text to show
                            date: Date(),
                            hasJoined: true, // But this message also signals a joining state
                            hasLeft: true, // ... and a leaving state
                            isBeingDrafted: false,
                            isSendingBalloons: false)

// chatroom.sendMessage(brokenMessage)

4. 為了解決這個問題我們引入Enums

2. 使用枚舉構(gòu)建數(shù)據(jù)

1. Enums構(gòu)建消息

enum Message {
  case text
  case draft
  case join
  case leave
  case balloon
}

2. 單枚舉結(jié)構(gòu)不包含數(shù)據(jù),因此enums+tuples 組合成一個包含數(shù)據(jù)的枚舉

enum Message {
    case text(userId: String, contents: String, date: Date)
    case draft(userId: String, date: Date)
    case join(userId: String, date: Date)
    case leave(userId: String, date: Date)
    case balloon(userId: String, date: Date)
}

3. 初始化消息

/// 文本消息
let textMessage = Message.text(userId: "2", contents: "Bonjour!", date: Date())

/// 加入聊天室消息
let joinMessage = Message.join(userId: "2", date: Date())

4. 打印消息

func logMessage(message: Message) {
    switch message {
    case let .text(userId: id, contents: contents, date: date):
        print("[(date)] User (id) sends message: (contents)")
    case let .draft(userId: id, date: date):
        print("[(date)] User (id) is drafting a message")
    case let .join(userId: id, date: date):
        print("[(date)] User (id) has joined the chatroom")
    case let .leave(userId: id, date: date):
        print("[(date)] User (id) has left the chatroom")
    case let .balloon(userId: id, date: date):
        print("[(date)] User (id) is sending balloons")
    }
}

logMessage(message: joinMessage) // User 2 has joined the chatroom
logMessage(message: textMessage) // User 2 sends message: Bonjour!

/// 完美解決!

5. 如何選擇使用Structs 還是使用 Enums?

  • 如果在單個case中進(jìn)行模式匹配,那么優(yōu)先使用struct
  • 相對struct使用enum的優(yōu)勢是編譯器會進(jìn)行安全檢查
  • 枚舉的關(guān)聯(lián)值是沒有附加邏輯的容器,需要手動添加
  • 下次構(gòu)建數(shù)據(jù)模型時,可嘗試使用枚舉對屬性進(jìn)行分組

3. 枚舉多態(tài)應(yīng)用

1. 數(shù)組中包含多個數(shù)據(jù)類型

let arr: [Any] = [Date(), "Why was six afraid of seven?", "Because...", 789]

for element: Any in arr {
    // element is "Any" type
    switch element {
        case let stringValue as String: "received a string: (stringValue)"
        case let intValue as Int: "received an Int: (intValue)"
        case let dateValue as Date: "received a date: (dateValue)"
        default: "I don't want anything else"
    }
}
  • 數(shù)組中包含多個數(shù)據(jù)類型 遍歷匹配時類型匹配,必須實(shí)現(xiàn)default case,未匹配到的值類型,由于數(shù)組中的數(shù)據(jù)類型是未知的,因此匹配變得困難.

2. 引入枚舉解決這個問題

enum DateType {
    case singleDate(Date)
    case dateRange(Range<Date>)
}

let now = Date()
let hourFromNow = Date(timeIntervalSinceNow: 3600)

let dates: [DateType] = [
    DateType.singleDate(now),
    DateType.dateRange(now..<hourFromNow)
]

for dateType in dates {
    switch dateType {
        case .singleDate(let date): print("Date is (date)")
        case .dateRange(let range): print("Range is (range)")
    }
}

3. 如果枚舉有變更,編譯器會進(jìn)行安全檢查

eg:枚舉新增一個case year ,如果使用枚舉時沒有實(shí)現(xiàn),則編譯器會報(bào)錯提示

enum DateType {
    case singleDate(Date)
    case dateRange(Range<Date>)
    case year(Int8)
}

for dateType in dates {
    switch dateType {
        case .singleDate(let date): print("Date is (date)")
        case .dateRange(let range): print("Range is (range)")
    }
}

error: switch must be exhaustive

  switch dateType {

  ^

add missing case: '.year(_)' switch dateType {

  • 正確的switch case
for dateType in dates {
    switch dateType {
        case .singleDate(let date): print("Date is (date)")
        case .dateRange(let range): print("Range is (range)")
            case year(let date):print("date is (date)")
    }
}
  • Tips: 你必須知道有多少種已知的數(shù)據(jù)類型,編譯器會幫助枚舉進(jìn)行安全檢查

4. 枚舉取代繼承

1. 繼承 構(gòu)建數(shù)據(jù)示例

  • 繼承是OOP(面向?qū)ο缶幊痰娜筇卣?lt;封裝、繼承、多態(tài)>之一 )
  • 繼承可構(gòu)建有層次的數(shù)據(jù)結(jié)構(gòu)。

例如,你可以有一家快餐店,像往常一樣賣漢堡、薯?xiàng)l。為此,你需要創(chuàng)建一個快餐的超類,包括漢堡、薯?xiàng)l和蘇打水等子類。

/// 快餐
struct FastFood {
  /// 產(chǎn)品名稱
  let productName:String
  /// 產(chǎn)品價(jià)格
  let productPrice:Float
  /// 產(chǎn)品id
  let productId:Int
}

struct Burger: FastFood {
  let burgerType:Int
}

struct Fries: FastFood {
  let friesType:Int
}

struct Soda: FastFood {
  let sodaType:Int
}

使用層次結(jié)構(gòu)(繼承)對軟件建模的一個限制是這樣做會限制在一個特定的方向上,而這個方向并不總是符合需求。

例如,前面提到的這家餐廳一直受到顧客的投訴,他們希望在薯?xiàng)l中配上正宗的日本壽司。他們打算適應(yīng)客戶,但是他們的子類化模型不適合這個新的需求。

在理想情況下,按層次結(jié)構(gòu)建模數(shù)據(jù)是有意義的。但在實(shí)踐中,可能會遇到不適合模型的邊緣情況和異常。

在本節(jié)中,我們將探討通過在更多示例中進(jìn)行子類化來建模數(shù)據(jù)的這些限制并在枚舉的幫助下解決這些問題。

2. 枚舉取代繼承 案例:構(gòu)建一個運(yùn)動app模型

  • 為一個運(yùn)動app構(gòu)建一個模型層,用于跟蹤某人的跑步和自行車訓(xùn)練。訓(xùn)練包括開始時間、結(jié)束時間和距離。
1. 創(chuàng)建一個Run和一個Cycle結(jié)構(gòu)體來表示正在建模的數(shù)據(jù)。
struct Run {
    let id: String
    let startTime: Date
    let endTime: Date
    let distance: Float
    let onRunningTrack: Bool
}

struct Cycle {
    
    enum CycleType {
        case regular
        case mountainBike
        case racetrack
    }
    
    let id: String
    let startTime: Date
    let endTime: Date
    let distance: Float
    let incline: Int
    let type: CycleType
}

let run = Run(id: "3", startTime: Date(), endTime: Date(timeIntervalSinceNow: 3600), distance: 300, onRunningTrack: false)

let cycle = Cycle(id: "4", startTime: Date(), endTime: Date(timeIntervalSinceNow: 3600), distance: 400, incline: 20, type: .mountainBike)

2. Run 和 Cycle 這兩個類有很多共同的屬性,我們是不是可以創(chuàng)建一個superClass來解決重復(fù)屬性
/// superClass Workout
class Workout {
  let id: String
  let startTime: Date
  let endTime: Date
  let distance: Float
}

/// subClass Run
class Run: Workout {
  let onRunningTrack: Bool
}

/// subClass Cycle
class Cycle: Workout {
  enum CycleType {
    case regular
    case mountainBike
      case racetrack
  }
  let incline: Int
  let type: CycleType
}
  • 好像解決了剛才屬性重復(fù)的問題,也產(chǎn)生新的問題,假設(shè)現(xiàn)在新增一種Workout的子類Pushups
class Pushups: Workout { 
  let repetitions: [Int]
  let date: Date
}
  • 但是Pushups只有一個屬性let id: String 和父類共用,它不需要要Workout強(qiáng)加給自己的其他是三個屬性let startTime: Date let endTime: Date let distance: Float,因此整個繼承結(jié)構(gòu)涉及的類都需要重構(gòu)
/// superClass Workout
class Workout {
  let id: String
}

/// subClass Run
class Run: Workout {
  let onRunningTrack: Bool
  let startTime: Date
  let endTime: Date
  let distance: Float
}

/// subClass Cycle
class Cycle: Workout {
  enum CycleType {
    case regular
    case mountainBike
    case racetrack
  }

  let startTime: Date
  let endTime: Date
  let distance: Float
  let incline: Int
  let type: CycleType
}

/// subClass Pushups
class Pushups: Workout { 
  let repetitions: [Int]
  let date: Date
}
  • 使用子類化一旦引入新的子類就需要重構(gòu)父類和其他不相關(guān)的子類,這和于程序穩(wěn)定性相違背

  • 讓我們引入枚舉來替代子類化避免這個問題

3. 使用Enums 重構(gòu)運(yùn)動app數(shù)據(jù)模型

enum Workout {
  case run(Run)
  case cycle(Cycle)
  case pushups(Pushups)
}
  • 這樣,run cycle pushups 都不需要繼承自Workout
/// Creating a workout
let pushups = Pushups(repetitions: [22,20,10], date: Date()) 
let workout = Workout.pushups(pushups)

switch workout { 
  case .run(let run):    print("Run: (run)") 
  case .cycle(let cycle):    print("Cycle: (cycle)") 
  case .pushups(let pushups):    print("Pushups: (pushups)")
}
  • 如果Workout有新增,這樣就不用重構(gòu)Workout run cycle pushups,只需要新增一個case即可
enum Workout {
  case run(Run)
  case cycle(Cycle)
  case pushups(Pushups)
  case abs(Abs) 
}

4. 如何選擇繼承和枚舉?

  1. 當(dāng)很多類型共享許多屬性時,而又可以預(yù)知這一組類型比較穩(wěn)定將來不會改變時,那么優(yōu)先選擇classic subclassing,但subclassing 也會使數(shù)據(jù)結(jié)構(gòu)進(jìn)入一個嚴(yán)格的層次結(jié)構(gòu);

  2. 當(dāng)一些類型既有相似之處,又有分歧,那么選擇enums and structs會是不錯的選擇,枚舉提供了更大的靈活性

  3. enums 每新增一個case時,必須實(shí)現(xiàn)所有的case,否則編譯器會幫你做檢查,如果有缺失會報(bào)錯,確保你沒有忘記剛新增的case

  4. enums 在寫下的那一刻便不可擴(kuò)展,除非你有源碼,這也是和classes 相比缺失的地方,比如app中引入一個thirdLib中的一個enums,那么我們無法對這個enums進(jìn)行擴(kuò)展

  5. 如果你能確保數(shù)據(jù)模型是固定的、可管理的幾種case,那么選擇enums也是不錯的

5. 練習(xí)題

1. 請列舉使用Enums 替代 Subclassing 的兩個優(yōu)點(diǎn)
  • 使用Eunms+Struct 替代 Subclassing 后續(xù)新增case 更靈活不需要重構(gòu)子類和超類, 可以不使用類

  • Enums 編譯器會做安全檢查,防止漏掉新增case

2. 請列舉使用Subclassing 替代 Enums 的兩個優(yōu)點(diǎn)
  • 繼承 可以保證數(shù)據(jù)模型保證嚴(yán)格的層次結(jié)構(gòu),覆蓋父類屬性及方法

  • 繼承,在沒有源碼時也可以繼承父類的屬性,而Enums 不可以

3. 枚舉數(shù)據(jù)類型

1. 總和類型

  • 枚舉默認(rèn)是基本數(shù)據(jù)類型,enum 會為每一個case賦一個UInt8類型的值(0~255)
1. 星期時間枚舉

enum Day {
  case sunday
  case monday
  case tuesday
  case wednesday
  case thursday
  case friday
  case saturday
}

2. 年齡枚舉

enum Age {
    case known(UInt8)
     case unknown
}

2. 產(chǎn)品類型

  • 支付類型
a. PaymentType Enums

enum PaymentType {
   case invoice
   case creditcard
   case cash
}

b. PaymentStatus struct

struct PaymentStatus {
     let paymentDate: Date?
     let isRecurring: Bool
     let paymentType: PaymentType
}

  • Enum+Struct ==> Enums+Tuples整合之后
c. PaymentStatus containing cases

enum PaymentStatus {
   case invoice(paymentDate: Date?, isRecurring: Bool)
   case creditcard(paymentDate: Date?, isRecurring: Bool)
   case cash(paymentDate: Date?, isRecurring: Bool)
}

3. 練習(xí)題

1. 請使用 Enums+Tuples對 Enum+Struct 進(jìn)行整合


enum Topping {
  case creamCheese
  case peanutButter
  case jam 
}

enum BagelType {
  case cinnamonRaisin
  case glutenFree
  case oatMeal
  case blueberry
}

struct Bagel {
  let topping: Topping
  let type: BagelType
}

解: Enum+Struct==>Enum+tuple

enum Topping {
  case creamCheese
  case peanutButter
  case jam 
}

enum BagelType {
  case cinnamonRaisin(topping:topping)
  case glutenFree(topping:topping)
  case oatMeal(topping:topping)
  case blueberry(topping:topping)
}

2. Bagel 有幾種組合?

  • 12

3. 請使用Struct 替換 Enums


enum Puzzle {
  case baby(numberOfPieces: Int)
  case toddler(numberOfPieces: Int)
  case preschooler(numberOfPieces: Int)
  case gradeschooler(numberOfPieces: Int)
  case teenager(numberOfPieces: Int)
}

解:Enum+tuple ==> Enum+Struct

enum Person {
  case baby
  case toddler 
  case preschooler 
  case gradeschooler 
  case teenager 
}

struct Puzzle {
  let personType: Person
  let numberOfPieces: Int
}

4. Enums可更安全地使用字符串

  • 枚舉可以存儲的原始值僅保留給字符串、字符、整數(shù)和浮點(diǎn)數(shù)類型。
  • 帶有原始值的枚舉意味著每個case都有一個在編譯時定義的值。
  • 相反,在前面的小節(jié)中使用的具有關(guān)聯(lián)類型的枚舉在運(yùn)行時存儲其值。

1. 具有字符串原始值的枚舉


enum Currency: String { 
  case euro = "euro" 
  case usd = "usd"
  case gbp = "gbp"
}

  • 字符串枚舉是原始值類型。
  • 所有case都包含字符串值

2. 原始值<rawValue>和case 名稱一致的枚舉,可省略字符串值


enum Currency: String {
  case euro
  case usd
  case gbp 
}

3. 原始價(jià)值的危險(xiǎn)性

  • Enum 允許原始值和case name 不一致,但如果中途修改原始值,編譯器不會報(bào)錯和提示,在運(yùn)行時使用RawValue時如果和預(yù)期不一致,程序會出錯

1. 原始值RawValue和case name 一致


let currency = Currency.euro print(currency.rawValue) // "euro"

let parameters = ["filter": currency.rawValue] print(parameters) // ["filter": "euro"]

2. 修改原始值RawValue和case name 不一致


enum Currency: String { 
  case euro = "eur" 
  case usd
  case gbp
}

  • Unexpected rawvalue, expected "euro" but got "eur"

let parameters = ["filter": currency.rawValue] 

print(parameters) // ["filter": "eur"]

  • 這種情況很有可能發(fā)生,比如你的應(yīng)用程序很負(fù)責(zé),結(jié)構(gòu)龐大,enum 在其他模塊或者另一個framework定義,在你負(fù)責(zé)的模塊使用,如果其他模塊對枚舉的原始值進(jìn)行修改,而你不知道,這時使用enum rawValue 時就很危險(xiǎn),編譯器也不會有提示

3. 解決方案

  1. 完全刪除原始值

  2. 使用原始值時進(jìn)行完整的單元測試

  3. 明確原始值


/// 明確原始值
let parameters: [String: String]
switch currency {
  case .euro: parameters = ["filter": "euro"] 
  case .usd: parameters = ["filter": "usd"] 
  case .gbp: parameters = ["filter": "gbp"]
}
// Back to using "euro" again
print(parameters) // ["filter": "euro"]

4. 字符串匹配

1. 傳統(tǒng)模式:直接使用進(jìn)行字符串進(jìn)行模式匹配時,可能會漏掉某個case


func iconName(for fileExtension: String) -> String { 
  switch fileExtension {
    case "jpg": return "assetIconJpeg"
    case "bmp": return "assetIconBitmap"
    case "gif": return "assetIconGif"
    default: return "assetIconUnknown"
  }
}
iconName(for: "jpg") // "assetIconJpeg"

  • 這里遍歷匹配字符串有一個問題,小寫的jpg 可以通過,但大寫的JPG 未匹配到
iconName(for: "JPG") // "assetIconUnknown", not favorable
  • 這個問題我們可以通過Enums rawValue 來解決

2. 創(chuàng)建一個帶字符串原始值的枚舉


enum ImageType: String { 
  case jpg
  case bmp
  case gif   
}

  • 當(dāng)在iconName函數(shù)中進(jìn)行匹配時,首先通過傳遞一個rawValue將字符串轉(zhuǎn)換為枚舉。這樣就知道ImageType是否添加了另一個case。編譯器將需要更新iconName并處理一個新case

func iconName(for fileExtension: String) -> String {
  guard let imageType = ImageType(rawValue: fileExtension) else {
    return "assetIconUnknown"     
  }
  switch imageType { 
    case .jpg: return "assetIconJpeg"
    case .bmp: return "assetIconBitmap"
    case .gif: return "assetIconGif"
  }
}

  • 仍然沒有解決大小寫的問題,例如“jpeg”或“jpeg”。如果您將“jpg”大寫,iconName函數(shù)將返回“assetIconUnknown”。

3. 現(xiàn)在我們通過同時匹配多個字符串來解決這個問題。可以實(shí)現(xiàn)初始值設(shè)定項(xiàng),它接受原始值字符串。

  • 添加一個擁有自定義初始化器的枚舉

    a. 初始化枚舉時對傳入的rawValue lowercased,獲取小寫字母

    b. 多選項(xiàng)匹配轉(zhuǎn)化為指定類型


enum ImageType: String {
  case jpg
  case bmp
  case gif
   
  init?(rawValue: String) {
    switch rawValue.lowercased() { 
      case "jpg", "jpeg": self = .jpg 
      case "bmp", "bitmap": self = .bmp
      case "gif", "gifv": self = .gif
      default: return nil
    }   
  }
}

eg: 對不同的字符串進(jìn)行匹配驗(yàn)證


iconName(for: "jpg") // "Received jpg"
iconName(for: "jpeg") // "Received jpg"
iconName(for: "JPG") // "Received a jpg"
iconName(for: "JPEG") // "Received a jpg"
iconName(for: "gif") // "Received a gif"

5. 練習(xí)題

1. 枚舉支持哪些原始類型?

  • 枚舉可以存儲的原始值僅保留給字符串、字符、整數(shù)和浮點(diǎn)數(shù)類型。

2. 枚舉的原始值是在編譯時還是在運(yùn)行時設(shè)置的?

  • 編譯時

3. 枚舉的關(guān)聯(lián)值是在編譯時還是在運(yùn)行時設(shè)置的?

  • 運(yùn)行時

4. 哪些類型可以進(jìn)入關(guān)聯(lián)值的內(nèi)部?

  • 所有類型

6. Enum優(yōu)勢

1. 枚舉有時是子類化<繼承>的替代方案,允許靈活的體系結(jié)構(gòu)。
2. 枚舉能夠在編譯時而不是運(yùn)行時捕獲問題。
3. 可以使用枚舉將屬性分組在一起。
4. 枚舉有時稱為和類型,基于代數(shù)數(shù)據(jù)類型。
5. 結(jié)構(gòu)可以分布在枚舉上。
6. 使用enum的原始值時,可以避免在編譯時捕獲問題。
7. 通過將字符串轉(zhuǎn)換為枚舉,可以更安全地處理字符串。
8. 將字符串轉(zhuǎn)換為枚舉時,分組案例并使用小寫字符串使轉(zhuǎn)換更容易。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容