一個欄位中返回了多種相似的型別
先來看下專案中我遇到的一個情況,服務端在人物中返回了一組資料。這些人物有幾個相同的屬性,但是又有各自不同的角色各有的屬性。json資料如下:
"characters" : [
{
type: "hero",
name: "Jake",
power: "Shapeshift"
},
{
type: "hero",
name: "Finn",
power: "Grass sword"
},
{
type: "princess",
name: "Lumpy Space Princess",
kingdom: "Lumpy Space"
},
{
type: "civilian",
name: "BMO"
},
{
type: "princess",
name: "Princess Bubblegum",
kingdom: "Candy"
}
]複製程式碼
那麼我們可以怎麼解析這樣的資料呢?
利用類和繼承
class Character {
type: String
name: String
}
class Hero : Character {
power: String
}
class Princess : Character {
kingdom: String
}
class Civilian : Character {
}
...
struct Model {
characters: [Character]
}複製程式碼
這其實就是專案中我原來使用的方案。但是很快就會覺得有點苦逼,因為使用的時候要不斷的型別判斷,然後型別轉換後才能訪問到某個具體型別的屬性:
// Type checking
if model.characters[indexPath.row] is Hero {
print(model.characters[indexPath.row].name)
}
// Type checking and Typecasting
if let hero = model.characters[indexPath.row] as? Hero {
print(hero.power)
}複製程式碼
利用結構體和協議
protocol Character {
var type: String { get set }
var name: String { get set }
}
struct Hero : Character {
power: String
}
struct Princess : Character {
kingdom: String
}
struct Civilian : Character {
}
...
struct Model {
characters: [Character]
}複製程式碼
這裡我們使用了結構體,解析的效能會好一些。但是看起來和前面類的方案差不多。我們並沒有利用上protocol的特點,使用的時候我們還是要進行型別判斷:
// Type checking
if model.characters[indexPath.row] is Hero {
print(model.characters[indexPath.row].name)
}
// Type checking and Typecasting
if let hero = model.characters[indexPath.row] as? Hero {
print(hero.power)
}複製程式碼
型別轉換的潛在問題
上面的這種型別轉換可能引入潛在的問題。如果後臺此時增加了一個型別對程式碼會產生什麼樣的影響呢?可能想到這種情況提前做了處理,也可能沒有處理導致崩潰。
{
type: "king"
name: "Ice King"
power: "Frost"
}複製程式碼
當我們在寫程式碼的時候,應該考慮到這樣的場景,當有新型別出現時能不能友好的提示哪裡需要處理呢?畢竟swift的設計目標之一就是更安全的語言。
另外一種可能:Enum
我們如何建立一個包含不同型別資料的陣列,然後訪問他們的屬性的時候不用型別轉換呢?
enum Character {
case hero, princess, civilian
}複製程式碼
當switch一個列舉時,每種case都需要被照顧到,所以使用enum可以很好的避免一些潛在的問題。但是如果只是這樣依然不夠好,我們可以更進一步:
Associated values:關聯值
enum Character {
case hero(Hero)
case princess(Princess)
case civilian(Civilian)
}
...
switch characters[indexPath.row] {
case .hero(let hero):
print(hero.power)
case .princess(let princess):
print(princess.kingdom)
case .civilian(let civilian):
print(civilian.name)
}複製程式碼
?! 現在使用的時候不再需要型別轉換了。並且如果增加一種新型別,只要在enum中增加一個case,你就不會遺漏需要再修改何處的程式碼,消除了潛在的問題。
Raw Value
enum Character : String { // Error: ❌
case hero(Hero)
case princess(Princess)
case civilian(Civilian)
}複製程式碼
你可能會發現這個列舉沒有實現RawRepresentable協議,這是因為關聯值型別的列舉不能同時遵從RawRepresentable協議,他們是互斥的。
如何初始化
如果實現了RawRepresentable協議,就會自帶一個利用raw value 初始化的方法。但是我們現在沒有實現這個協議,所以我們需要自定義一個初始化方法。 先定義一個內部使用的列舉表示型別:
enum Character {
private enum Type : String {
case hero, princess, civilian
static let key = "type"
}
}複製程式碼
Failable initializers
因為傳回來的json可能出現對映失敗的情況,比如增加的一個新型別,所以這裡的初始化方法是可失敗的。
// enum Character
init?(json: [String : AnyObject]) {
guard let
string = json[Type.key] as? String,
type = Type(rawValue: string)
else { return nil }
switch type {
case .hero:
guard let hero = Hero(json: json)
else { return nil }
self = .hero(hero)
case .princess:
guard let princess = Princess(json: json)
else { return nil }
self = .princess(princess)
case .civilian:
guard let civilian = Civilian(json: json)
else { return nil }
self = .civilian(civilian)
}
}複製程式碼
使用列舉解析json
// Model initialisation
if let characters = json["characters"] as? [[String : AnyObject]] {
self.characters = characters.flatMap { Character(json: $0) }
}複製程式碼
注意這裡使用了flatMap。當一條資料的type不在我們已經定義的範圍內時,Character(json: [String : AnyObject])返回一個nil。我們當然希望過濾掉這些無法處理的資料。所以使用flatMap,flatMap過程中會拋棄為nil的值,所以這裡使用了flapMap。
完成!
switch model.characters[indexPath.row] {
case .hero(let hero):
print(hero.power)
case .princess(let princess):
print(princess.kingdom)
case .civilian(let civilian):
print(civilian.name)
}複製程式碼
現在可以像最前面展示的那樣使用了。 可以告別那些將陣列型別宣告為 Any, AnyObject或者泛型,繼承組合的model,使用時再轉換型別的日子了。
One More Thing: 模式匹配
如果只處理列舉中的一種型別,我們會這麼寫:
func printPower(character: Character) {
switch character {
case .hero(let hero):
print(hero.power)
default:
break
}複製程式碼
然而我們可以利用swift提供的模式匹配,用這種更優雅的寫法:
func printPower(character: Character) {
if case .hero(let hero) = character {
print(hero.power)
}
}複製程式碼
github上的原始碼:playgrounds
歡迎關注我的微博:@沒故事的卓同學