Swift編程思想 Part 1: 拯救小馬

原文鏈接: 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搬走,替換成mapflatmap。

與此同時(shí),安全駕駛,還有,沒錯,拯救小馬!??


  1. 注意這個(gè)try?默默的將error丟棄了:用它的時(shí)候你不會知道更多關(guān)于為什么代碼出錯的原因。所以通常來說如果可能的話用do { try ... } catch { }替換掉try?會更好。但是在我們的例子中,因?yàn)槲覀兿M贘SON因某種原因序列化失敗時(shí)返回一個(gè)空數(shù)組,這里用try?是OK的。
  2. 如你所見,我在代碼的最后保留了一個(gè)as!(items.copy() as! NSArray)。有時(shí)殺死小馬強(qiáng)制轉(zhuǎn)型是OK的,如果你真的,真的知道返回的類型不是其他任何東西,就像這里的mutableArray.copy()??墒沁@種例外十分罕見,只有在你一開始的時(shí)候就認(rèn)真思考過這個(gè)用例的情況下才可以接受(當(dāng)心,如果那匹??死了,你將會受到良心的譴責(zé))。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 關(guān)于 Swift 重要這個(gè)文檔所包含的準(zhǔn)備信息, 是關(guān)于開發(fā)的 API 和技術(shù)的。這個(gè)信息可能會改變, 根據(jù)這個(gè)文...
    無灃閱讀 4,624評論 1 27
  • 132.轉(zhuǎn)換錯誤成可選值 通過轉(zhuǎn)換錯誤成一個(gè)可選值,你可以使用 try? 來處理錯誤。當(dāng)執(zhí)行try?表達(dá)式時(shí),如果...
    無灃閱讀 1,430評論 0 3
  • 1、隨機(jī)數(shù) 不需要隨機(jī)數(shù)種子 arc4random()%N + begin:產(chǎn)生begin~begin+N的隨機(jī)數(shù)...
    我是小胡胡123閱讀 4,408評論 0 2
  • 基礎(chǔ)部分(The Basics) 當(dāng)推斷浮點(diǎn)數(shù)的類型時(shí),Swift 總是會選擇Double而不是Float。 結(jié)合...
    gamper閱讀 1,493評論 0 7
  • 原文鏈接:http://alisoftware.github.io/swift/2015/09/06/thinki...
    大臉貓121閱讀 1,586評論 0 5

友情鏈接更多精彩內(nèi)容