使用枚举构建数据模型
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. 如何选择继承和枚举?
当很多类型共享许多属性时,而又可以预知这一组类型比较稳定将来不会改变时,那么优先选择classic subclassing,但subclassing 也会使数据结构进入一个严格的层次结构;
当一些类型既有相似之处,又有分歧,那么选择enums and structs会是不错的选择,枚举提供了更大的灵活性
enums 每新增一个case时,必须实现所有的case,否则编译器会帮你做检查,如果有缺失会报错,确保你没有忘记刚新增的case
enums 在写下的那一刻便不可扩展,除非你有源码,这也是和classes 相比缺失的地方,比如app中引入一个thirdLib中的一个enums,那么我们无法对这个enums进行扩展
如果你能确保数据模型是固定的、可管理的几种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. 解决方案
完全删除原始值
使用原始值时进行完整的单元测试
明确原始值
/// 明确原始值
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. 哪些类型可以进入关联值的内部?
- 所有类型