原文鏈接: Thinking in Swift, Part 1: Saving ponies
原博客地址:Crunchy Development
原文日期: 2015-09-06
譯者:ray16897188
我惩鞯看見Swift的新手試著將它們的ObjC代碼翻譯成Swift蹋艺。但是開始用Swift寫代碼的時候最難的事情并不是語法,而是思維方式的轉(zhuǎn)變钦铺,去用那些ObjC里并沒有的Swift新概念小泉。
在這一系列的文章中芦疏,我們會拿一個ObjC代碼做例子冕杠,然后在把它轉(zhuǎn)成Swift代碼的全程中引入越來越多的對新概念的講解。
本文的第一部分內(nèi)容:可選類型(optionals)酸茴,對可選類型的強制拆包分预,小馬,
if let
薪捍,guard
和??笼痹。
ObjC代碼
假設(shè)你想創(chuàng)建一個條目列表(比如過會兒要顯示在一個TableView里)- 每個條目都有一個圖標,標題和網(wǎng)址 - 這些條目都通過一個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īng)該會看出來這里面有很多代碼異味凳干。Swift的資深使用者讀到這段代碼之后就很可能心臟病突發(fā)而全部掛掉。
哪里會出錯被济?
上面例子中第一個看起來像代碼異味的地方就是一個Swift新手經(jīng)常犯的壞毛簿却汀:到處使用隱式解析可選類型(value!),強制轉(zhuǎn)型(value as! String)和強制使用try(try!)只磷。
可選類型是你的朋友:它們很棒经磅,因為它們能迫使你去思考你的值什么時候是nil
,以及在這種情形下你該做什么喳瓣。比如"如果沒有圖標的話我該顯示什么呢馋贤?在我的TableViewCell里我該用一個占位符(placeholder)么?或者用另外一個完全不同的cell模板畏陕?"配乓。
這些就是我們在ObjC中經(jīng)常忘了考慮進去的用例,但是Swift幫助我們?nèi)ビ涀∷鼈兓莼伲援斨凳?code>nil的時候把它們強制拆包導(dǎo)致程序崩潰犹芹,把可選類型這個高級特性扔在一邊不用,是很可惜的鞠绰。
你絕不應(yīng)該對一個值進行強制拆包腰埂,除非你真的知道你在干什么。記住蜈膨,每次你加一個
!
去安撫編譯器的時候屿笼,你就屠殺了一匹小馬??。
很可悲翁巍,Xcode是鼓勵犯這種錯誤的驴一,因為error提示到:"value of optional type ‘NSArray?’ not unwrapped. Did you mean to use ! or ?",修改提示建議...你在后面加一個!
??灶壶。噢肝断,Xcode,你是有多菜。
我們來拯救這些小馬吧
那么我們該怎樣去避開這些無處不在的糟糕的!
呢胸懈?這兒有一些技巧:
- 使用可選綁定(optional binding)
if let x = optional { /* 使用 x */ }
- 用
as?
替換掉as!
,前者在轉(zhuǎn)型失敗的時候返回nil
担扑;你當然可以把它和if let
結(jié)合使用 - 你也可以用
try?
替換掉try!
,前者在表達式失敗時返回nil
1趣钱。
好了涌献,來看看用了這些規(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中有些機制能幫我們做簡化:
- 將多個
if let
語句合并為一個:if let x = opt1, y = opt2
- 使用
guard
語句间唉,在某個條件不滿足的情況下能讓我們盡早的從一個函數(shù)中跳出來绞灼,避免了再去運行函數(shù)體剩下的部分。
當類型能被推斷出來的時候呈野,我們再用此代碼把這些變量類型去掉來消除冗余 - 比如簡單的用let items = NSMutableArray()
- 并利用guard
語句再確保我們的json確實是一個NSDictionary
對象的數(shù)組低矮。最后,我們用一個更"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
語句真心很贊被冒,因為它在函數(shù)的開始部分就把代碼集中在了對輸入的有效性檢查上军掂,然后在代碼剩下的部分中你就不用再為這些檢查操心了。如果輸入并非所想昨悼,我們就盡早跳出蝗锥,幫助我們專注在事情都如所期的正軌上。
Swift難道不應(yīng)該比ObjC更簡潔么率触?
嗯好吧葱蝗,這代碼好像是比它的ObjC版本更復(fù)雜穴张。但是別愁,在即將到來的本文第二部分中我們會把它大幅度簡化两曼。
但更重要的是皂甘,這段代碼比它ObjC的版本更加安全。實際上ObjC的代碼更短只是因為我們忘了去執(zhí)行一大堆的安全測試悼凑。即使我們的ObjC代碼看起來蠻正常偿枕,它還是會在一些情況中立即崩潰,比如我們給它一個無效的JSON户辫,或者一個并不是由string類型的dictionary的array構(gòu)造出來的東西(比如創(chuàng)建JSON的那個人覺得"icon"這個key值對應(yīng)的就是一個用來提示該條目是否有圖標的Boolean渐夸,而不是一個String...)。在ObjC中我們僅僅是忘了去處理這些用例寸莫,因為ObjC沒有引導(dǎo)我們?nèi)タ紤]這些情況捺萌,而Swift迫使我們?nèi)タ紤]。
所以O(shè)bjC代碼當然更短:因為我們就是忘了去處理所有這些事情。如果你去不防止自己程序崩潰的話桃纯,把代碼寫的更短是很輕松的酷誓。開車的時候不留意路上的障礙當然輕松,但你就是這樣把小馬給撞死的态坦。
結(jié)論
Swift是為了更高的安全性而設(shè)計盐数。不要把所有東西都強制拆包而忽視了可選類型:當你在你的Swift代碼中看見了一個!
,你就總是要把它看做是一處代碼異味伞梯,某些事情是要出錯的玫氢。
在即將到來的本文第二部分中,我們會看到怎么讓這個Swift代碼更加簡潔谜诫,并延續(xù)Swift的編程思想:將for
循環(huán)和if-let
搬走漾峡,替換成map
和flatmap
。
與此同時喻旷,安全駕駛生逸,還有,沒錯且预,拯救小馬槽袄!??
- 注意這個
try?
默默的將error
丟棄了:用它的時候你不會知道更多關(guān)于為什么代碼出錯的原因。所以通常來說如果可能的話用do { try ... } catch { }
替換掉try?
會更好锋谐。但是在我們的例子中遍尺,因為我們希望在JSON因某種原因序列化失敗時返回一個空數(shù)組,這里用try?
是OK的涮拗。 - 如你所見乾戏,我在代碼的最后保留了一個
as!
(items.copy() as! NSArray
)。有時殺死小馬強制轉(zhuǎn)型是OK的多搀,如果你真的歧蕉,真的知道返回的類型不是其他任何東西,就像這里的mutableArray.copy()
康铭」咄耍可是這種例外十分罕見,只有在你一開始的時候就認真思考過這個用例的情況下才可以接受(當心从藤,如果那匹??死了催跪,你將會受到良心的譴責)。