原文鏈接: Thinking in Swift, Part 1: Saving ponies
原博客地址:Crunchy Development
原文日期: 2015-09-06
譯者:ray16897188
我??匆奡wift的新手試著將它們的ObjC代碼翻譯成Swift。但是開始用Swift寫代碼的時(shí)候最難的事情并不是語法,而是思維方式的轉(zhuǎn)變,去用那些ObjC里并沒有的Swift新概念。
在這一系列的文章中,我們會拿一個(gè)ObjC代碼做例子,然后在把它轉(zhuǎn)成Swift代碼的全程中引入越來越多的對新概念的講解。
本文的第一部分內(nèi)容:可選類型(optionals),對可選類型的強(qiáng)制拆包,小馬,
if let,guard和??。
ObjC代碼
假設(shè)你想創(chuàng)建一個(gè)條目列表(比如過會兒要顯示在一個(gè)TableView里)- 每個(gè)條目都有一個(gè)圖標(biāo),標(biāo)題和網(wǎng)址 - 這些條目都通過一個(gè)JSON初始化。下面是ObjC代碼看起來的樣子:
@interface ListItem : NSObject
@property(strong) UIImage* icon;
@property(strong) NSString* title;
@property(strong) NSURL* url;
@end
@implementation ListItem
+(NSArray*)listItemsFromJSONData:(NSData*)jsonData {
NSArray* itemsDescriptors = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil];
NSMutableArray* items = [NSMutableArray new];
for (NSDictionary* itemDesc in itemsDescriptors) {
ListItem* item = [ListItem new];
item.icon = [UIImage imageNamed:itemDesc[@"icon"]];
item.title = itemDesc[@"title"];
item.url = [NSURL URLWithString:itemDesc[@"title"]];
[items addObject:item];
}
return [items copy];
}
@end
直譯成Swift
想象一下有多少Swift的新手會把這段代碼翻譯成這樣:
class ListItem {
var icon: UIImage?
var title: String = ""
var url: NSURL!
static func listItemsFromJSONData(jsonData: NSData?) -> NSArray {
let jsonItems: NSArray = try! NSJSONSerialization.JSONObjectWithData(jsonData!, options: []) as! NSArray
let items: NSMutableArray = NSMutableArray()
for itemDesc in jsonItems {
let item: ListItem = ListItem()
item.icon = UIImage(named: itemDesc["icon"] as! String)
item.title = itemDesc["title"] as! String
item.url = NSURL(string: itemDesc["url"] as! String)!
items.addObject(item)
}
return items.copy() as! NSArray
}
}
對Swift稍有經(jīng)驗(yàn)的人應(yīng)該會看出來這里面有很多代碼異味。Swift的資深使用者讀到這段代碼之后就很可能心臟病突發(fā)而全部掛掉。
哪里會出錯?
上面例子中第一個(gè)看起來像代碼異味的地方就是一個(gè)Swift新手經(jīng)常犯的壞毛病:到處使用隱式解析可選類型(value!),強(qiáng)制轉(zhuǎn)型(value as! String)和強(qiáng)制使用try(try!)。
可選類型是你的朋友:它們很棒,因?yàn)樗鼈兡芷仁鼓闳ニ伎寄愕闹凳裁磿r(shí)候是nil,以及在這種情形下你該做什么。比如"如果沒有圖標(biāo)的話我該顯示什么呢?在我的TableViewCell里我該用一個(gè)占位符(placeholder)么?或者用另外一個(gè)完全不同的cell模板?"。
這些就是我們在ObjC中經(jīng)常忘了考慮進(jìn)去的用例,但是Swift幫助我們?nèi)ビ涀∷鼈儯援?dāng)值是nil的時(shí)候把它們強(qiáng)制拆包導(dǎo)致程序崩潰,把可選類型這個(gè)高級特性扔在一邊不用,是很可惜的。
你絕不應(yīng)該對一個(gè)值進(jìn)行強(qiáng)制拆包,除非你真的知道你在干什么。記住,每次你加一個(gè)
!去安撫編譯器的時(shí)候,你就屠殺了一匹小馬??。
很可悲,Xcode是鼓勵犯這種錯誤的,因?yàn)閑rror提示到:"value of optional type ‘NSArray?’ not unwrapped. Did you mean to use ! or ?",修改提示建議...你在后面加一個(gè)!??。噢,Xcode,你是有多菜。
我們來拯救這些小馬吧
那么我們該怎樣去避開這些無處不在的糟糕的!呢?這兒有一些技巧:
- 使用可選綁定(optional binding)
if let x = optional { /* 使用 x */ } - 用
as?替換掉as!,前者在轉(zhuǎn)型失敗的時(shí)候返回nil;你當(dāng)然可以把它和if let結(jié)合使用 - 你也可以用
try?替換掉try!,前者在表達(dá)式失敗時(shí)返回nil1。
好了,來看看用了這些規(guī)則之后我們的代碼2:
class ListItem {
var icon: UIImage?
var title: String = ""
var url: NSURL!
static func listItemsFromJSONData(jsonData: NSData?) -> NSArray {
if let nonNilJsonData = jsonData {
if let jsonItems: NSArray = (try? NSJSONSerialization.JSONObjectWithData(nonNilJsonData, options: [])) as? NSArray {
let items: NSMutableArray = NSMutableArray()
for itemDesc in jsonItems {
let item: ListItem = ListItem()
if let icon = itemDesc["icon"] as? String {
item.icon = UIImage(named: icon)
}
if let title = itemDesc["title"] as? String {
item.title = title
}
if let urlString = itemDesc["url"] as? String {
if let url = NSURL(string: urlString) {
item.url = url
}
}
items.addObject(item)
}
return items.copy() as! NSArray
}
}
return [] // In case something failed above
}
}
判決的金字塔
可悲的是,滿世界的添加這些if let讓我們的代碼往右挪了好多,形成了臭名昭著的判決金字塔(此處插段悲情音樂)。
Swift中有些機(jī)制能幫我們做簡化:
- 將多個(gè)
if let語句合并為一個(gè):if let x = opt1, y = opt2 - 使用
guard語句,在某個(gè)條件不滿足的情況下能讓我們盡早的從一個(gè)函數(shù)中跳出來,避免了再去運(yùn)行函數(shù)體剩下的部分。
當(dāng)類型能被推斷出來的時(shí)候,我們再用此代碼把這些變量類型去掉來消除冗余 - 比如簡單的用let items = NSMutableArray() - 并利用guard語句再確保我們的json確實(shí)是一個(gè)NSDictionary對象的數(shù)組。最后,我們用一個(gè)更"Swift化"的返回類型[ListItem]替換掉ObjC的NSArray:
class ListItem {
var icon: UIImage?
var title: String = ""
var url: NSURL!
static func listItemsFromJSONData(jsonData: NSData?) -> [ListItem] {
guard let nonNilJsonData = jsonData,
let json = try? NSJSONSerialization.JSONObjectWithData(nonNilJsonData, options: []),
let jsonItems = json as? Array<NSDictionary>
else {
*// If we failed to unserialize the JSON*
*// or that JSON wasn't an Array of NSDictionaries,*
*// then bail early with an empty array*
return []
}
var items = [ListItem]()
for itemDesc in jsonItems {
let item = ListItem()
if let icon = itemDesc["icon"] as? String {
item.icon = UIImage(named: icon)
}
if let title = itemDesc["title"] as? String {
item.title = title
}
if let urlString = itemDesc["url"] as? String, let url = NSURL(string: urlString) {
item.url = url
}
items.append(item)
}
return items
}
}
guard語句真心很贊,因?yàn)樗诤瘮?shù)的開始部分就把代碼集中在了對輸入的有效性檢查上,然后在代碼剩下的部分中你就不用再為這些檢查操心了。如果輸入并非所想,我們就盡早跳出,幫助我們專注在事情都如所期的正軌上。
Swift難道不應(yīng)該比ObjC更簡潔么?

嗯好吧,這代碼好像是比它的ObjC版本更復(fù)雜。但是別愁,在即將到來的本文第二部分中我們會把它大幅度簡化。
但更重要的是,這段代碼比它ObjC的版本更加安全。實(shí)際上ObjC的代碼更短只是因?yàn)槲覀兺巳?zhí)行一大堆的安全測試。即使我們的ObjC代碼看起來蠻正常,它還是會在一些情況中立即崩潰,比如我們給它一個(gè)無效的JSON,或者一個(gè)并不是由string類型的dictionary的array構(gòu)造出來的東西(比如創(chuàng)建JSON的那個(gè)人覺得"icon"這個(gè)key值對應(yīng)的就是一個(gè)用來提示該條目是否有圖標(biāo)的Boolean,而不是一個(gè)String...)。在ObjC中我們僅僅是忘了去處理這些用例,因?yàn)镺bjC沒有引導(dǎo)我們?nèi)タ紤]這些情況,而Swift迫使我們?nèi)タ紤]。
所以O(shè)bjC代碼當(dāng)然更短:因?yàn)槲覀兙褪峭巳ヌ幚硭羞@些事情。如果你去不防止自己程序崩潰的話,把代碼寫的更短是很輕松的。開車的時(shí)候不留意路上的障礙當(dāng)然輕松,但你就是這樣把小馬給撞死的。
結(jié)論
Swift是為了更高的安全性而設(shè)計(jì)。不要把所有東西都強(qiáng)制拆包而忽視了可選類型:當(dāng)你在你的Swift代碼中看見了一個(gè)!,你就總是要把它看做是一處代碼異味,某些事情是要出錯的。
在即將到來的本文第二部分中,我們會看到怎么讓這個(gè)Swift代碼更加簡潔,并延續(xù)Swift的編程思想:將for循環(huán)和if-let搬走,替換成map和flatmap。
與此同時(shí),安全駕駛,還有,沒錯,拯救小馬!??
- 注意這個(gè)
try?默默的將error丟棄了:用它的時(shí)候你不會知道更多關(guān)于為什么代碼出錯的原因。所以通常來說如果可能的話用do { try ... } catch { }替換掉try?會更好。但是在我們的例子中,因?yàn)槲覀兿M贘SON因某種原因序列化失敗時(shí)返回一個(gè)空數(shù)組,這里用try?是OK的。 - 如你所見,我在代碼的最后保留了一個(gè)
as!(items.copy() as! NSArray)。有時(shí)殺死小馬強(qiáng)制轉(zhuǎn)型是OK的,如果你真的,真的知道返回的類型不是其他任何東西,就像這里的mutableArray.copy()??墒沁@種例外十分罕見,只有在你一開始的時(shí)候就認(rèn)真思考過這個(gè)用例的情況下才可以接受(當(dāng)心,如果那匹??死了,你將會受到良心的譴責(zé))。