使用枚舉構(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. 如何選擇繼承和枚舉?
當(dāng)很多類型共享許多屬性時,而又可以預(yù)知這一組類型比較穩(wěn)定將來不會改變時,那么優(yōu)先選擇classic subclassing,但subclassing 也會使數(shù)據(jù)結(jié)構(gòu)進(jìn)入一個嚴(yán)格的層次結(jié)構(gòu);
當(dāng)一些類型既有相似之處,又有分歧,那么選擇enums and structs會是不錯的選擇,枚舉提供了更大的靈活性
enums 每新增一個case時,必須實(shí)現(xiàn)所有的case,否則編譯器會幫你做檢查,如果有缺失會報(bào)錯,確保你沒有忘記剛新增的case
enums 在寫下的那一刻便不可擴(kuò)展,除非你有源碼,這也是和classes 相比缺失的地方,比如app中引入一個thirdLib中的一個enums,那么我們無法對這個enums進(jìn)行擴(kuò)展
如果你能確保數(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. 解決方案
完全刪除原始值
使用原始值時進(jìn)行完整的單元測試
明確原始值
/// 明確原始值
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)部?
- 所有類型