歡迎加入QQ討論群258996829
一葉知秋 頭像
蘋果2袋
2
一葉知秋

七個Swift中的陷阱以及避免方法

發(fā)布時間:2016-02-25 12:35  回復:0  查看:2771   最后回復:2016-02-25 12:35  

文章總結(jié)翻譯自:Seven Swift Snares & How to Avoid Them

Swift正在完成一個驚人的壯舉,它正在改變我們在蘋果設備上編程的方式,引入了很多現(xiàn)代范例,例如:函數(shù)式編程和相比于OC這種純面向?qū)ο笳Z言更豐富的類型檢查。

Swift語言希望通過采用安全的編程模式去幫助開發(fā)者避免bug。然而這也會不可避免的產(chǎn)生一些人造的陷阱,他們會在編譯器不報錯的情況下引入一些Bug。這些陷阱有的已經(jīng)在Swift book中提到,有一些還沒有。這里有七個我在去年遇到的陷阱,它們涉及Swift協(xié)議擴展、可選鏈和函數(shù)式編程。

協(xié)議擴展:強大但是需要謹慎使用

一個Swift類可以去繼承另一個類,這種能力是強大的。繼承將使類之間的特定關系更加清晰,并且支持細粒度代碼分享。但是,Swift中如果不是引用類型的話(如:結(jié)構體、枚舉),就不能具有繼承關系。然而,一個值類型可以繼承協(xié)議,同時協(xié)議可以繼承另一個協(xié)議。雖然協(xié)議除了類型信息外不能包含其他代碼,但是協(xié)議擴展(protocol extension)可以包含代碼。照這種方式,我們可以用繼承樹來實現(xiàn)代碼的分享共用,樹的葉子是值類型(結(jié)構體或枚舉類),樹的內(nèi)部和根是協(xié)議和與他們對應的擴展。

但是Swift協(xié)議擴展的實現(xiàn)依然是一片新的、未開發(fā)的領域,尚存在一些問題。代碼并不總是按照我們期望的那樣執(zhí)行。因為這些問題出現(xiàn)在值類型(結(jié)構體與枚舉)與協(xié)議組合使用的場景下,我們將使用類與協(xié)議組合使用的例子去說明這種場景下不存在陷阱。當我們重新改為使用值類型和協(xié)議的時候?qū)l(fā)生令人驚奇的事。

開始介紹我們的例子:classy pizza

假設這里有使用兩種不同谷物制作的三種Pizza:

enum Grain  { case Wheat, Corn }
 
class  NewYorkPizza  { let crustGrain: Grain = .Wheat }
class  ChicagoPizza  { let crustGrain: Grain = .Wheat }
class CornmealPizza  { let crustGrain: Grain = .Corn  }
我們可以通過crustGrain屬性取得披薩所對應的原料

NewYorkPizza().crustGrain     // returns Wheat
ChicagoPizza().crustGrain     // returns Wheat
CornmealPizza().crustGrain     // returns Corn
因為大多數(shù)的Pizza是用小麥(wheat)做的,這些公共代碼可以放進一個超類中作為默認執(zhí)行的代碼。

enum Grain { case Wheat, Corn }
 
class Pizza {
    var crustGrain: Grain { return .Wheat }
    // other common pizza behavior
}
class NewYorkPizza: Pizza {}
class ChicagoPizza: Pizza {}
這些默認的代碼可以被重載去處理其它的情況(用玉米制作)

class CornmealPizza: Pizza {
    override var crustGain: Grain { return .Corn }
}
哎呀!這代碼是錯的,并且很幸運的是編譯器發(fā)現(xiàn)了這些錯誤。你能發(fā)現(xiàn)這個錯誤么?我們在第二個crustGain中少寫了r。Swift通過顯式的標注override避免這種錯誤。比如在這個例子中,我們用到了override,但是拼寫錯誤的"crustGain"其實并沒有重寫任何屬性,下面是修改后的代碼:

class CornmealPizza: Pizza {
        override var crustGrain: Grain { return .Corn }
}
現(xiàn)在它可以通過編譯并成功運行:

NewYorkPizza().crustGrain         // returns Wheat
ChicagoPizza().crustGrain         // returns Wheat
CornmealPizza().crustGrain         // returns Corn
同時Pizza超類允許我們的代碼在不知道Pizza具體類型的時候去操作pizzas。我們可以聲明一個Pizza類型的變量。

var pie: Pizza
但是通用類型Pizza仍然可以去得到特定類型的信息。

pie =  NewYorkPizza();        pie.crustGrain     // returns Wheat
pie =  ChicagoPizza();      pie.crustGrain     // returns Wheat
pie = CornmealPizza();      pie.crustGrain     // returns Corn

Swift的引用類型在這個Demo中工作的很好。但是如果這個程序涉及到并發(fā)性、競爭條件,我們可以使用值類型來避免這些。讓我們來試一下值類型的Pizza吧!

這里和上面一樣簡單,只需要把class修改為struct即可:

enum Grain { case Wheat, Corn }
 
struct  NewYorkPizza     { let crustGrain: Grain = .Wheat }
struct  ChicagoPizza     { let crustGrain: Grain = .Wheat }
struct CornmealPizza     { let crustGrain: Grain = .Corn  }
執(zhí)行

NewYorkPizza()    .crustGrain     // returns Wheat
ChicagoPizza()    .crustGrain     // returns Wheat
CornmealPizza()    .crustGrain     // returns Corn
當我們使用引用類型的時候,我們通過一個超類Pizza來達到目的。但是對于值類型將要求一個協(xié)議和一個協(xié)議擴展來合作完成。

protocol Pizza {}
 
extension Pizza {  var crustGrain: Grain { return .Wheat }  }
 
struct  NewYorkPizza: Pizza { }
struct  ChicagoPizza: Pizza { }
struct CornmealPizza: Pizza {  let crustGain: Grain = .Corn }
這段代碼可以通過編譯,我們來測試一下:

NewYorkPizza().crustGrain         // returns Wheat
ChicagoPizza().crustGrain         // returns Wheat
CornmealPizza().crustGrain         // returns Wheat  What?!
對于執(zhí)行結(jié)果,我們想說cornmeal pizza并不是Wheat制作的,返回結(jié)果出現(xiàn)錯誤!哎呀!我把

struct CornmealPizza: Pizza {  let crustGain: Grain = .Corn }
中的  crustGrain寫成了crustGain,再一次忘記了r,但是對于值類型這里沒有override關鍵字去幫助編譯器去發(fā)現(xiàn)我們的錯誤。沒有編譯器的幫助,我們不得不更加小心的編寫代碼。

在協(xié)議擴展中重寫協(xié)議中的屬性時要仔細核對

ok,我們把這個拼寫錯誤改正過來:

struct CornmealPizza: Pizza {  let crustGrain: Grain = .Corn }
重新執(zhí)行

NewYorkPizza().crustGrain         // returns Wheat
ChicagoPizza().crustGrain         // returns Wheat
CornmealPizza().crustGrain     // returns Corn  Hooray!
為了在討論Pizza的時候不需要擔心到底是New York, Chicago, 還是 cornmeal,我們可以使用Pizza協(xié)議作為變量的類型。

var pie: Pizza
這個變量能夠在不同種類的Pizza中去使用

pie =  NewYorkPizza(); pie.crustGrain  // returns Wheat
pie =  ChicagoPizza(); pie.crustGrain  // returns Wheat
pie = CornmealPizza(); pie.crustGrain  // returns Wheat    Not again?!

為什么這個程序顯示cornmeal pizza 包含wheat?Swift編譯代碼的時候忽略了變量的目前實際值。代碼只能夠使用編譯時期的知道的信息,并不知道運行時期的具體信息。程序中可以在編譯時期得到的信息是pie是pizza類型,pizza協(xié)議擴展返回wheat,所以在結(jié)構體CornmealPizza中的重寫起不到任何作用。雖然編譯器本能夠在使用靜態(tài)調(diào)度替換動態(tài)調(diào)度時,為潛在的錯誤提出警告,但它實際上并沒有這么做。這里的粗心將帶來巨大的陷阱。

在這種情況下,Swift提供一種解決方案,除了在協(xié)議擴展中(extension)定義crustGrain屬性之外,還可以在協(xié)議中聲明。

protocol  Pizza {  var crustGrain: Grain { get }  }
extension Pizza {  var crustGrain: Grain { return .Wheat }  }

在協(xié)議內(nèi)聲明變量并在協(xié)議拓展中定義,這樣會告訴編譯器關注變量pie運行時的值。

在協(xié)議中一個屬性的聲明有兩種不同的含義,靜態(tài)還是動態(tài)調(diào)度,取決于是否這個屬性在協(xié)議擴展中定義。

補充了協(xié)議中變量的聲明后,代碼可以正常運行了:

pie =  NewYorkPizza();  pie.crustGrain     // returns Wheat
pie =  ChicagoPizza();  pie.crustGrain     // returns Wheat
pie = CornmealPizza();  pie.crustGrain     // returns Corn    Whew!

在協(xié)議擴展中定義的每一個屬性,需要在協(xié)議中進行聲明。

然而這個設法避免陷阱的方式并不總是有效的。

導入的協(xié)議不能夠完全擴展。

框架(庫)可以使一個程序?qū)虢涌谌ナ褂?,而不必包含相關實現(xiàn)。例如蘋果提供給我們提供了需要框架,實現(xiàn)了用戶體驗、系統(tǒng)設施和其他功能。Swift的擴展允許程序向?qū)氲念悺⒔Y(jié)構體、枚舉和協(xié)議中添加自己的屬性(這里的屬性并不是存儲屬性)。通過協(xié)議拓展添加的屬性,就好像它原來就在協(xié)議中一樣。但實際上定義在協(xié)議拓展中的屬性并非一等公民,因為通過協(xié)議拓展無法添加屬性的聲明。

我們首先實現(xiàn)一個框架,這個框架定義了Pizza協(xié)議和具體的類型

// PizzaFramework:
 
public protocol Pizza { }
 
public struct  NewYorkPizza: Pizza  { public init() {} }
public struct  ChicagoPizza: Pizza  { public init() {} }
public struct CornmealPizza: Pizza  { public init() {} }
導入框架并且擴展Pizza

import PizzaFramework
 
public enum Grain { case Wheat, Corn }
 
extension Pizza         { var crustGrain: Grain { return .Wheat    } }
extension CornmealPizza { var crustGrain: Grain { return .Corn    } }
和以前一樣,靜態(tài)調(diào)度產(chǎn)生一個錯誤的答案

var pie: Pizza = CornmealPizza()
pie.crustGrain                            // returns Wheat   Wrong!
這個是因為(與剛才的解釋一樣)這個crustGrain屬性并沒有在協(xié)議中聲明,而是只是在擴展中定義。然而,我們沒有辦法對框架的代碼進行修改,因此也就不能解決這個問題。因此,想要通過擴展增加其他框架的協(xié)議屬性是不安全的。

不要對導入的協(xié)議進行擴展,新增可能需要動態(tài)調(diào)度的屬性

正像剛才描述的那樣,框架與協(xié)議擴展之間的交互,限制了協(xié)議擴展的效用,但是框架并不是唯一的限制因素,同樣,類型約束也不利于協(xié)議擴展。

Attributes in restricted protocol extensions: declaration is no longer enough

回顧一下此前Pizza的例子:

enum Grain { case Wheat, Corn }
 
protocol  Pizza { var crustGrain: Grain { get }  }
extension Pizza { var crustGrain: Grain { return .Wheat }  }
 
struct  NewYorkPizza: Pizza  { }
struct  ChicagoPizza: Pizza  { }
struct CornmealPizza: Pizza  { let crustGrain: Grain = .Corn }
讓我們用Pizza做一頓飯。不幸的是,并不是每頓飯都會吃pizza,所以我們使用一個通用的Meal結(jié)構體來適應各種情況。我們只需要傳入一個參數(shù)就可以確定進餐的具體類型。

struct Meal: MealProtocol {
       let mainDish: MainDishOfMeal
}
結(jié)構體Meal繼承自MealProtocol協(xié)議,它可以測試meal是否包含谷蛋白。

protocol MealProtocol {
    typealias MainDish_OfMealProtocol
    var mainDish: MainDish_OfMealProtocol {get}
    var isGlutenFree: Bool {get}
}
為了避免中毒,代碼中使用了默認值(不含有谷蛋白)

extension MealProtocol {
    var isGlutenFree: Bool  { return false }
}
Swift中的 Where提供了一種方式去表達約束性協(xié)議擴展。當主菜是pizza的時候,我們知道pizza有scrustGrain屬性,我們就可以訪問這個屬性。如果沒where這里的限制,我們在不是Pizza的情況下訪問scrustGrain是不安全的。

extension MealProtocol  where  MainDish_OfMealProtocol: Pizza {
    var isGlutenFree: Bool  { return mainDish.crustGrain == .Corn }
}

一個帶有Where的擴展叫做約束性擴展。

讓我們做一份美味的cornmeal Pizza

let meal: Meal = Meal(mainDish: CornmealPizza())
結(jié)果:

meal.isGlutenFree    // returns false
// 根據(jù)協(xié)議拓展,理論上應該返回true
正像我們在前面小節(jié)演示的那樣,當發(fā)生動態(tài)調(diào)度的時候,我們應該在協(xié)議中聲明,并且在協(xié)議擴展中進行定義。但是約束性擴展的定義總是靜態(tài)調(diào)度的。為了防止由于意外的靜態(tài)調(diào)度而引起的bug:

如果一個新的屬性需要動態(tài)調(diào)度,避免使用約束性協(xié)議擴展。

使用可選鏈賦值和副作用

Swift可以通過靜態(tài)地檢查變量是否為nil來避免錯誤,并使用一種方便的縮略表達式,可選鏈,用于忽略可能出現(xiàn)的nil。這一點也正是Objective-C的默認行為。

不幸的是,如果可選鏈中被賦值的引用有可能為空,就可能導致錯誤,考慮下面這段代碼,Holder中存放一個整數(shù):

class Holder  {
    var x = 0
}
 
var n = 1
var h: Holder? = nil
h?.x = n++

在這段代碼的最后一行中,我們把n++賦值給h的屬性。除了賦值以外,變量n還會自增,我們稱此為副作用。

變量n最終的值會取決于h是否為nil。如果h不為nil,那么賦值語句執(zhí)行,n++也會執(zhí)行。但如果h為nil,不僅賦值語句不會執(zhí)行,n++也不會執(zhí)行。為了避免沒有發(fā)生副作用導致的令人驚訝的結(jié)果,我們應該:

避免把一個有副作用的表達式的結(jié)果通過可選鏈賦值給等號左邊的變量

函數(shù)編程陷阱

由于Swift的支持,函數(shù)式編程的優(yōu)點得以被帶入蘋果的生態(tài)圈中。Swift中的函數(shù)和閉包都是一等公民,不僅方便易用而且功能強大。不幸的是,其中也有一些我們需要小心避免的陷阱。

比如,inout參數(shù)會在閉包中默默的失效。

Swift的inout參數(shù)允許函數(shù)接受一個參數(shù)并直接對參數(shù)賦值,Swift的閉包支持在執(zhí)行過程中引用被捕獲的函數(shù)。這些特性有助于我們寫出優(yōu)雅易讀的代碼,所以你也許會把它們結(jié)合起來使用,但這種結(jié)合有可能會導致問題。

我們重寫crustGrain屬性來說明inout參數(shù)的使用,為簡單起見,開始時先不使用閉包:

enum Grain {
    case Wheat, Corn
}
 
struct CornmealPizza {
    func setCrustGrain(inout grain: Grain)  {
        grain = .Corn
    }
}
為了測試這個函數(shù),我們給它傳一個變量作為參數(shù)。函數(shù)返回后,這個變量的值應該從Wheat變成了Corn:

let pizza = CornmealPizza()
var grain: Grain = .Wheat
pizza.setCrustGrain(&grain)
grain        // returns Corn
現(xiàn)在我們嘗試在函數(shù)中返回閉包,然后在閉包中設置參數(shù)的值:

struct CornmealPizza {
    func getCrustGrainSetter() -> (inout grain: Grain) -> Void {
        return { (inout grain: Grain) in
            grain = .Corn
        }
    }
}
使用這個閉包只需要多一次調(diào)用:

var grain: Grain = .Wheat
let pizza = CornmealPizza()
let aClosure = pizza.getCrustGrainSetter()
grain            // returns Wheat (We have not run the closure yet)
aClosure(grain: &grain)
grain            // returns Corn
到目前為止一切正常,但如果我們直接把參數(shù)傳進getCrustGrainSetter函數(shù)而不是閉包呢?

struct CornmealPizza {
    func getCrustGrainSetter(inout grain: Grain)  ->  () -> Void {
        return { grain = .Corn }
    }
}
然后再試一次:

var grain: Grain = .Wheat
let pizza = CornmealPizza()
let aClosure = pizza.getCrustGrainSetter(&grain)
print(grain)                // returns Wheat (We have not run the closure yet)
aClosure()
print(grain)                // returns Wheat  What?!?
inout參數(shù)在傳入閉包的作用域外時會失效,所以:

避免在閉包中使用in-out參數(shù)

這個問題在Swift文檔中提到過,但還有一個與之相關的問題值得注意,這與創(chuàng)建的閉包的等價方法:柯里化有關。

在使用柯里化技術時,inout參數(shù)顯得前后矛盾。

在一個創(chuàng)建并返回閉包的函數(shù)中,Swift為函數(shù)的類型和主體提供了一種簡潔的語法。盡管這種柯里化看上去僅是一種縮略表達式,但它與inout參數(shù)結(jié)合使用時卻會給人們帶來一些驚訝。為了說明這一點,我們用柯里化語法實現(xiàn)上面那個例子。函數(shù)沒有聲明為返回一個閉包,而是在第一個參數(shù)列表后加上了第二個參數(shù)列表,然后在函數(shù)體內(nèi)省略了顯式的閉包創(chuàng)建:

struct CornmealPizza {
    func getCrustGrainSetterWithCurry(inout grain: Grain)() -> Void {
        grain = .Corn
    }
}
和顯式創(chuàng)建閉包時一樣,我們調(diào)用這個函數(shù)然后返回一個閉包:

var grain: Grain = .Wheat
let pizza = CornmealPizza()
let aClosure = pizza.getCrustGrainSetterWithCurry(&grain)
在上面的例子中,閉包被顯式創(chuàng)建但沒能成功為inout參數(shù)賦值,但這次就成功了:

aClosure()
grain                // returns Corn
這說明在柯里化函數(shù)中,inout參數(shù)可以正常使用,但是顯式的創(chuàng)建閉包時就不行了。

避免在柯里化函數(shù)中使用inout參數(shù),因為如果你后來將柯里化改為顯式的創(chuàng)建閉包,這段代碼就會產(chǎn)生錯誤

總結(jié):七個避免

  • 在協(xié)議擴展中重寫協(xié)議中的屬性時要仔細核對

  • 在協(xié)議擴展中定義的每一個屬性,需要在協(xié)議中進行聲明

  • 不要對導入的第三方協(xié)議進行屬性擴展,那樣可能需要動態(tài)調(diào)度

  • 如果一個新的屬性需要動態(tài)調(diào)度,避免使用約束性協(xié)議擴展

  • 避免把一個有副作用的表達式的結(jié)果通過可選鏈賦值給等號左邊的變量

  • 避免在閉包中使用inout參數(shù)

  • 避免在柯里化函數(shù)中使用inout參數(shù),因為如果你后來將柯里化改為顯式的創(chuàng)建閉包,這段代碼就會產(chǎn)生錯誤

One More Thing

更多好文敬請關注我和@Martin_wjl共同維護的專題——每周學點Swift,每周五更新幾篇精選教程,輕松學習Swift。


轉(zhuǎn)自:http://www.jianshu.com/p/c467809bedb3


您還未登錄,請先登錄

熱門帖子

最新帖子

?