SwiftInDepth_02_使用枚举构建数据模型

使用枚举构建数据模型

1. 使用结构体构建数据模型

1. 引入枚举之前我们先看下如何使用struct构建消息模型

场景:直播消息类型有

  • 加入消息
  • 退出消息
  • 发文字消息
  • 发图片消息

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. 创建消息

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. 假设消息参数hasJoined hasLeft 都为true,则消息就无法区分是进入还是离开聊天室

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. 使用枚举构建数据

1. Enums构建消息

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

2. 单枚举结构不包含数据,因此enums+tuples 组合成一个包含数据的枚举

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中进行模式匹配,那么优先使用struct
  • 相对struct使用enum的优势是编译器会进行安全检查
  • 枚举的关联值是没有附加逻辑的容器,需要手动添加
  • 下次构建数据模型时,可尝试使用枚举对属性进行分组

3. 枚举多态应用

1. 数组中包含多个数据类型

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"
    }
}
  • 数组中包含多个数据类型 遍历匹配时类型匹配,必须实现default case,未匹配到的值类型,由于数组中的数据类型是未知的,因此匹配变得困难.

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. 如果枚举有变更,编译器会进行安全检查

eg:枚举新增一个case year ,如果使用枚举时没有实现,则编译器会报错提示

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: 你必须知道有多少种已知的数据类型,编译器会帮助枚举进行安全检查

4. 枚举取代继承

1. 继承 构建数据示例

  • 继承是OOP(面向对象编程的三大特征<封装、继承、多态>之一 )
  • 继承可构建有层次的数据结构。

例如,你可以有一家快餐店,像往常一样卖汉堡、薯条。为此,你需要创建一个快餐的超类,包括汉堡、薯条和苏打水等子类。

/// 快餐
struct FastFood {
  /// 产品名称
  let productName:String
  /// 产品价格
  let productPrice:Float
  /// 产品id
  let productId:Int
}

struct Burger: FastFood {
  let burgerType:Int
}

struct Fries: FastFood {
  let friesType:Int
}

struct Soda: FastFood {
  let sodaType:Int
}

使用层次结构(继承)对软件建模的一个限制是这样做会限制在一个特定的方向上,而这个方向并不总是符合需求。

例如,前面提到的这家餐厅一直受到顾客的投诉,他们希望在薯条中配上正宗的日本寿司。他们打算适应客户,但是他们的子类化模型不适合这个新的需求。

在理想情况下,按层次结构建模数据是有意义的。但在实践中,可能会遇到不适合模型的边缘情况和异常。

在本节中,我们将探讨通过在更多示例中进行子类化来建模数据的这些限制并在枚举的帮助下解决这些问题。

2. 枚举取代继承 案例:构建一个运动app模型

  • 为一个运动app构建一个模型层,用于跟踪某人的跑步和自行车训练。训练包括开始时间、结束时间和距离。
1. 创建一个Run和一个Cycle结构体来表示正在建模的数据。
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 这两个类有很多共同的属性,我们是不是可以创建一个superClass来解决重复属性
/// 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
}
  • 好像解决了刚才属性重复的问题,也产生新的问题,假设现在新增一种Workout的子类Pushups
class Pushups: Workout { 
  let repetitions: [Int]
  let date: Date
}
  • 但是Pushups只有一个属性let id: String 和父类共用,它不需要要Workout强加给自己的其他是三个属性let startTime: Date let endTime: Date let distance: Float,因此整个继承结构涉及的类都需要重构
/// 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
}
  • 使用子类化一旦引入新的子类就需要重构父类和其他不相关的子类,这和于程序稳定性相违背

  • 让我们引入枚举来替代子类化避免这个问题

3. 使用Enums 重构运动app数据模型

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有新增,这样就不用重构Workout run cycle pushups,只需要新增一个case即可
enum Workout {
  case run(Run)
  case cycle(Cycle)
  case pushups(Pushups)
  case abs(Abs) 
}

4. 如何选择继承和枚举?

  1. 当很多类型共享许多属性时,而又可以预知这一组类型比较稳定将来不会改变时,那么优先选择classic subclassing,但subclassing 也会使数据结构进入一个严格的层次结构;

  2. 当一些类型既有相似之处,又有分歧,那么选择enums and structs会是不错的选择,枚举提供了更大的灵活性

  3. enums 每新增一个case时,必须实现所有的case,否则编译器会帮你做检查,如果有缺失会报错,确保你没有忘记刚新增的case

  4. enums 在写下的那一刻便不可扩展,除非你有源码,这也是和classes 相比缺失的地方,比如app中引入一个thirdLib中的一个enums,那么我们无法对这个enums进行扩展

  5. 如果你能确保数据模型是固定的、可管理的几种case,那么选择enums也是不错的

5. 练习题

1. 请列举使用Enums 替代 Subclassing 的两个优点
  • 使用Eunms+Struct 替代 Subclassing 后续新增case 更灵活不需要重构子类和超类, 可以不使用类

  • Enums 编译器会做安全检查,防止漏掉新增case

2. 请列举使用Subclassing 替代 Enums 的两个优点
  • 继承 可以保证数据模型保证严格的层次结构,覆盖父类属性及方法

  • 继承,在没有源码时也可以继承父类的属性,而Enums 不可以

3. 枚举数据类型

1. 总和类型

  • 枚举默认是基本数据类型,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. 产品类型

  • 支付类型
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. 练习题

1. 请使用 Enums+Tuples对 Enum+Struct 进行整合


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可更安全地使用字符串

  • 枚举可以存储的原始值仅保留给字符串、字符、整数和浮点数类型。
  • 带有原始值的枚举意味着每个case都有一个在编译时定义的值。
  • 相反,在前面的小节中使用的具有关联类型的枚举在运行时存储其值。

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. 原始价值的危险性

  • Enum 允许原始值和case name 不一致,但如果中途修改原始值,编译器不会报错和提示,在运行时使用RawValue时如果和预期不一致,程序会出错

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"]

  • 这种情况很有可能发生,比如你的应用程序很负责,结构庞大,enum 在其他模块或者另一个framework定义,在你负责的模块使用,如果其他模块对枚举的原始值进行修改,而你不知道,这时使用enum rawValue 时就很危险,编译器也不会有提示

3. 解决方案

  1. 完全删除原始值

  2. 使用原始值时进行完整的单元测试

  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. 传统模式:直接使用进行字符串进行模式匹配时,可能会漏掉某个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. 创建一个带字符串原始值的枚举


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

  • 当在iconName函数中进行匹配时,首先通过传递一个rawValue将字符串转换为枚举。这样就知道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函数将返回“assetIconUnknown”。

3. 现在我们通过同时匹配多个字符串来解决这个问题。可以实现初始值设定项,它接受原始值字符串。

  • 添加一个拥有自定义初始化器的枚举

    a. 初始化枚举时对传入的rawValue lowercased,获取小写字母

    b. 多选项匹配转化为指定类型


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: 对不同的字符串进行匹配验证


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. 练习题

1. 枚举支持哪些原始类型?

  • 枚举可以存储的原始值仅保留给字符串、字符、整数和浮点数类型。

2. 枚举的原始值是在编译时还是在运行时设置的?

  • 编译时

3. 枚举的关联值是在编译时还是在运行时设置的?

  • 运行时

4. 哪些类型可以进入关联值的内部?

  • 所有类型

6. Enum优势

1. 枚举有时是子类化<继承>的替代方案,允许灵活的体系结构。
2. 枚举能够在编译时而不是运行时捕获问题。
3. 可以使用枚举将属性分组在一起。
4. 枚举有时称为和类型,基于代数数据类型。
5. 结构可以分布在枚举上。
6. 使用enum的原始值时,可以避免在编译时捕获问题。
7. 通过将字符串转换为枚举,可以更安全地处理字符串。
8. 将字符串转换为枚举时,分组案例并使用小写字符串使转换更容易。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容