足球资料库数据/孙祥/nba五佳球/足球直播哪个平台好 - cctv5今日现场直播

首頁 > 知識庫 > 正文

Swift 中的 7 個陷阱以及如何避免
2016-02-11 16:45:12   來源: mengyidan1988   評論:0 點擊:

作者: David Ungar 翻譯:BigNerdCoding 原文鏈接 前言 伴隨著Swift語言的快速發展,我們對于蘋果設備編程的認識也發生著變化。與原來的Objective-C語言相比,Swift語言帶來的更多現代化的特征,例如函數式編程和更多的類型檢查。 Swift語言采用一些安全的編程模式來幫助開發者避免一些bug。然而不可避免的是,這種雄心勃勃的做法也會讓我們的程序中引入一些陷阱(至少
作者: David Ungar
翻譯:BigNerdCoding 原文鏈接
前言
伴隨著Swift語言的快速發展,我們對于蘋果設備編程的認識也發生著變化。與原來的Objective-C語言相比,Swift語言帶來的更多現代化的特征,例如函數式編程和更多的類型檢查。

Swift語言采用一些安全的編程模式來幫助開發者避免一些bug。然而不可避免的是,這種雄心勃勃的做法也會讓我們的程序中引入一些陷阱(至少目前是這樣),并且在編譯的時候編譯器無法檢查出來并給出任何警告提示。這其中的一些陷阱在官方的Swift書里面,但是還有一些書中并沒有提及。下面會介紹7個陷阱,其中的大部分坑我都進過。它們涉及到Swift的協議擴展(protocol extensions),可選鏈(optional chaining),以及函數式編程(functional programming)

協議擴展:強大,但是小心使用
對于程序員來說,Swift中類的繼承特性是自己武器庫里的一件有力武器,因為它能讓類之間的特殊關系變的明確,而且讓代碼的分享和復用變的可行。但是Swift中的值類型(value types)并不能和引用類型(reference types)一樣能相互之間進行繼承。然而,一個值類型卻可以繼承自一個協議,反過來協議還能繼承自另一個協議。雖然協議里面不能包含代碼只能含有類型信息,但是類型的擴展卻可以包含代碼。通過這種方式,代碼可以以樹形層級結構來說實現繼承共享:值類型作為葉子節點,而根節點和中間節點則是協議于相應的協議擴展。

但是協議擴展的實現,作為一個新的處女地,還存在幾個問題。代碼可能不會總是如我們所期待的那樣運行。因為Swift中結構和枚舉都是值類型于協議在一起的使用的時候會有坑,我們在示例中先使用類來避免這個坑,然后再對比看看前者的一下神奇的坑。

簡單實例:類類型的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  }   

每一種pizza都能返回制作的原料:
NewYorkPizza().crustGrain     // returns WheatChicagoPizza().crustGrain     // returns WheatCornmealPizza().crustGrain     // returns Corn  

因為大部分的pizza都是由Wheat制作的,我們可以將通用的代碼分解出來放在一個超類的默認的實現里面:
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 }}

哎呀!這里代碼是錯的,幸好編譯器檢查出來了。我們在寫變化crustGain遺漏了一個字符'r'。Swift通過強制類中重載的代碼必須明確對應來避免這種錯誤。因此,這里代碼聲明為重載,拼寫錯誤就會被檢查出來。修改后:
class CornmealPizza: Pizza {    override var crustGrain: Grain { return .Corn }}  

現在,編譯器會通過:
NewYorkPizza().crustGrain         // returns WheatChicagoPizza().crustGrain         // returns WheatCornmealPizza().crustGrain         // returns Corn  

我們可以更進一步分解出通用代碼,父項Pizza允許我們不必知道具體的pizza類型就可以進行操作,因為我們可以聲明一個通用的pizza變量。
var pie: Pizza  

通用型的pizza變量依然可以如下獲得具體的信息:
pie =  NewYorkPizza();        pie.crustGrain     // returns Wheatpie =  ChicagoPizza();      pie.crustGrain     // returns Wheatpie = CornmealPizza();      pie.crustGrain     // returns Corn  

上面的引用類型是個很好的例子。但是當程序涉及到并發的時候,就會面臨一些條件競爭,而值類型則由于不可變的語言特性支持而不會出現這些情況。接下來看看值類型下的pizza。

簡單的值類型的例子
pizza的三種種類和原料可以使用struct之類的值類型表示,就像引用類型一樣簡單。
enum Grain { case Wheat, Corn }struct  NewYorkPizza     { let crustGrain: Grain = .Wheat }struct  ChicagoPizza     { let crustGrain: Grain = .Wheat }struct CornmealPizza     { let crustGrain: Grain = .Corn  }  

如下調用:
NewYorkPizza()    .crustGrain     // returns WheatChicagoPizza()    .crustGrain     // returns WheatCornmealPizza()    .crustGrain     // returns Corn   

包含所有pizza的協議和一個無法檢測到的錯誤
使用引用類型,我們可以聲明一個公共的父類來表示一個通用的"pizza"概念。在值類型中要實現相同的功能,我們需要兩個部分而不是一個:一個聲明通用類型的協議和定義新類型屬性的協議擴展。
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 WheatChicagoPizza().crustGrain         // returns WheatCornmealPizza().crustGrain         // returns Wheat  What?!

這里發生了錯誤,與上面提到的錯誤一樣也是忘記了字符'r'。但是在值類型,這里沒有override關鍵字去幫助編譯器發現錯誤。在語言中出現這樣的遺漏是很不合適的,否則你需要提供足夠的冗余去發現這個錯誤。沒有了編譯器的幫助,我們只能自己更加小心一點。第一個坑的準則:
引用

在重載協議擴展的屬性時候移動要復查,屬性名稱。

好了,讓我們修復這個問題并再次測試:
struct CornmealPizza: Pizza {  let crustGrain: Grain = .Corn }NewYorkPizza().crustGrain         // returns WheatChicagoPizza().crustGrain         // returns WheatCornmealPizza().crustGrain         // returns Corn  Hooray!

Pizza變量,但是錯誤的答案
為了討論一個通用的pizza而不關心具體的類型,我們可以使用Pizza協議作為一個變量的類型。然后我們使用變量來獲得不同pizza的原料:
var pie: Pizzapie =  NewYorkPizza(); pie.crustGrain  // returns Wheatpie =  ChicagoPizza(); pie.crustGrain  // returns Wheatpie = CornmealPizza(); pie.crustGrain  // returns Wheat    Not again?!

為什么對于cornmeal pizza程序返回給我們的是wheat?Swift編譯后的代碼忽略了其真實的值。編譯器能提供給編譯后的代碼信息就是程序編譯時的信息,而不是代碼運行時的信息。這里,我們在編譯時(compile-time)能知道的就是pie是一個pizza,并且在pizza的擴展里面聲明了是Wheat,所以CornmealPizza里面的聲明并不會起到任何作用,調用的時候自然無法返回我們希望的結果。盡管便一起可會對這個使用靜態而不是動態調用的潛在錯誤提出警告,但是這里沒有。我相信一不小心你就會掉進去,我稱之為大圈套。

這里提供了一個方案可以修復這個問題。除了在協議擴展里面定義屬性外:
protocol  Pizza {}extension Pizza {  var crustGrain: Grain { return .Wheat }  }

我們還在協議里進行聲明:
protocol  Pizza {  var crustGrain: Grain { get }  }extension Pizza {  var crustGrain: Grain { return Wheat }  }

使用既提供一個聲明并且加上定義這種做法給Swift,會讓它通知編譯器變量的運行時的類型和值。(但并不全是這樣,當我們沒有在協議擴展里面定義crustGrain的話,協議里的crustGrain聲明必須在每一個繼承Pizza類型里面[structure, class, or enumeration]實現。)

協議里面聲明的屬性意味這兩件不同的事,靜態和動態分發,而這取決于屬性有沒有在協議擴展里面進行定義。

協議里面添加聲明后,代碼正常工作了:
pie =  NewYorkPizza();  pie.crustGrain     // returns Wheatpie =  ChicagoPizza();  pie.crustGrain     // returns Wheatpie = CornmealPizza();  pie.crustGrain     // returns Corn    Whew!  

這是一個很嚴重的坑;即使我們已經弄清它了,但是這依然可能給我們程序帶來bug。這里要感謝一些這篇文章作者Alexandros Salazar。就像文章中提到的一樣這里沒有任何編譯時檢查,為了避免這個坑:
引用

每一個協議擴展中定義的屬性,請在協議中進行聲明。

但是這種規避并不是總是可能的。

導入的協議并不能完全被擴展
框架和類庫為程序代碼提供了接口以供使用,而且不需要知道框架代碼實現的細節。例如蘋果提供了實現的用戶體驗,系統工具等功能很多框架。Swift語言的擴展功能允許程序添加自己的屬性到導入的類、結構、枚舉和協議中。對于具體的類型(類、結構、枚舉),能夠很好的工作,這些屬性就像是導入框架中自己原有的定義一樣。但是對于協儀擴展來說,她定義的屬性并沒有一等公民的待遇。因為在協議擴展里面添加一個屬性并不能在協議里面進行聲明。

下面我們導定義了pizzas的協議框架。框架里面定義了協議和具體類型:
// PizzaFramework:public protocol Pizza { }public struct  NewYorkPizza: Pizza  { public init() {} }public struct  ChicagoPizza: Pizza  { public init() {} }public struct CornmealPizza: Pizza  { public init() {} }  

接下來,我們導入框架并且進行擴展:
import PizzaFrameworkpublic enum Grain { case Wheat, Corn }extension Pizza         { var crustGrain: Grain { return .Wheat    } }extension CornmealPizza { var crustGrain: Grain { return .Corn    } }  

與前面一樣,靜態分發導致了一個錯誤的答案:
var pie: Pizza = CornmealPizza()pie.crustGrain                            // returns Wheat   Wrong!

與前面解釋的原因一樣,crustGrain屬性只進行了定義而沒有在協議中聲明。然而,我們不能修改框架里面的源碼,所以我們無法修復這里的問題。因此無法安全的從其它框架中擴展一個協議(除非你確信它永遠都不會需要動態分發)。為了避免這個問題:
引用
?? 永遠不要從導入的框架中擴展一個包含可能需要動態分發的屬性的協議

正如在任何大型系統一樣,Swift中的特性數量會導致一個與之數量匹配的潛在不良后果。如剛剛描述的,框架與協議擴展相互作用限制了后者的作用。但框架是不是唯一的問題,類型限制也會對協議擴展產生不利影響。

在受限的協議拓展中:聲明變量已經不夠
當我們拓展一個通用的協議,而該協議里面的某些屬性只在某些類型里面使用時,我們可以在一個受限的協議里面。但是語言可能并不是我們所期待的那樣。回顧一下我們前面的實例代碼:
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,所以我們將不同種類的食物定義為一個通用的膳食結構類型的變量類型:
struct Meal<MainDishOfMeal>: MealProtocol {    let mainDish: MainDishOfMeal}

Meal繼承了MealProtocol協議,該協議可以檢查食物是否有谷蛋白。為了是無谷蛋白的代碼能過在不同的meal中分享(如在沒有主食的meal),我們使用如下協議:
protocol MealProtocol {    typealias MainDish_OfMealProtocol    var mainDish: MainDish_OfMealProtocol {get}    var isGlutenFree: Bool {get}}

為了防止有人中毒,做到有備無患,代碼需要設定一個安全保守的默認值:
extension MealProtocol {    var isGlutenFree: Bool  { return false }}  

很高興,有一道菜是沒問題的:用corn而不是wheat做成的pizza。Swift中的where結構提供了一個方法將這個情況表示為一個受限的協議擴展。如果主食是pizza的話,我們知道它有一個crust,我們可以很安全的獲取該屬性。如果不使用where的話代碼是不安全的:
extension MealProtocol  where  MainDish_OfMealProtocol: Pizza {    var isGlutenFree: Bool  { return mainDish.crustGrain == .Corn }}

這個帶where的擴展被稱為受限擴展(restricted extension)。

接下來,我們看一下cornmeal pizza!
let meal: Meal<Pizza> = Meal(mainDish: CornmealPizza())meal.isGlutenFree // returns false// But there is no gluten! Why can’t I have that pizza?

就像前面提到的一樣,在協議中進行屬性聲明,并在協議擴展里面進行相應定義會導致動態分發。但是在受限的協議擴展里面的定義永遠都是靜態分發的。為了避免這個坑帶來的bug:
引用
?? 避免對一個協議進行受限擴展,特別是當擴展里面有個新的屬性需要動態分發的時候。

即使你避免了上面于雨協議相關的坑,Swift中還有一些其它的坑。其中大部分都在官方的書籍里面提到了,但是當我們將它單獨拿出來分析的時候會更加的突出、明顯,這其中就包括接下來要討論的。

可選鏈賦值以及相應的一些副作用
引用

注:這里的副作用side-effects是這樣理解的,就是發生了一些用戶意料意外的事。總之,“side effects”指的就是那些本不應有或者用戶意料之外的作用。

Swift中的可選類型使用對可能是nil值靜態檢查,避免了可能存在的錯誤。它提供了一個方便速記、可選鏈,來處理什么時候nil值需要忽略操作,就像Objective-C中默認的一昂。不幸的是,Swift可選鏈的一些細節可能導致錯誤的發生,那就是當我們對潛在的空應用變量進行賦值的時候。考慮如下情況,一個對象包含一個整型變量,有一個可能是空指針指向該對象,并且進行賦值:
class Holder  { var x = 0 }var n = 1var h: Holder? = ...h?.x = n++n  // 1 or 2?

上面代碼中n的值,取決于h是不是空值。如果不是空值,那么賦值語句執行,然后n會自增,結束的時候n就為2。反之,賦值語句不執行,自增語句可回跳過,結束的時候n就為1。為了避免這個坑照成的困惑:
引用
?? 不要將一個可能帶有副作用的語句表達式賦值給左側的可選鏈。

Swift函數式編程的坑
Swift對函數式編程的支持,讓這一編程模式的有點更夠應用于整個蘋果生態系統中。Swift中的函數和閉包是語言中的第一等實體,它們容易使用且功能強大。但是,這里面也有坑在等你。

閉包中輸入輸出參數會失效
在Swift中輸入輸出參數,允許在調用函數時接收一個變量的值,并且設置該變量的值。而閉包則可以捕獲和抓取上下文中的常量和變量的引用。這兩個特性會讓代碼變得更加的優雅和容易理解。所有你可能同時使用這兩個特性,但是當他們一起使用的時候會導致一些問題。

首先讓我們來重寫crustGrain屬性來理解輸入輸出參數。我們以簡單的例子開始,不包含閉包:
enum Grain { case Wheat, Corn }struct CornmealPizza {    func setCrustGrain(inout grain: Grain)  { grain = .Corn }}  

下面我們來簡單的測試一下上面的函數,我們傳遞一個變量過去。當程序返回的時候,該變量的值會從Whwat變成了Corn。
let pizza = CornmealPizza()var grain: Grain = .Wheatpizza.setCrustGrain(&grain)grain      // returns Corn  

現在,我們寫一個函數,該函數會返回一個閉包,而這個閉包可以設置grain變量的值:
struct CornmealPizza {    func getCrustGrainSetter()   ->   (inout grain: Grain) -> Void {        return { (inout grain: Grain) in grain = .Corn }    }}   

使用這個閉包的話會需要更多的調用步驟:
var grain: Grain = .Wheatlet pizza = CornmealPizza()let aClosure = pizza.getCrustGrainSetter()grain   // returns Wheat (We have not run the closure yet)aClosure(grain: &grain)grain   // returns Corn  

到目前為止,代碼運行良好沒有出現問題。但是如果我們把輸入輸出參數grain傳遞給返回閉包的函數而不是閉包本身的時候會發生什么呢?
struct CornmealPizza {    func getCrustGrainSetter(inout grain: Grain)  ->  () -> Void {        return { grain = .Corn }        }}  

我們試著測試一下代碼:
var grain: Grain = .Wheatlet pizza = CornmealPizza()let aClosure = pizza.getCrustGrainSetter(&grain)grain    // returns Wheat (We have not run the closure yet)aClosure()grain    // returns Wheat  What?!?

輸入輸出參數傳遞到閉包外部的時候,沒有起到任何作用,因此:
引用
?? 盡量在閉包里面避免使用輸入輸出參數

這個問題在官方書籍中有提到,但是這里存在一個與之相關的問題。那就是使用Currring創建閉包的時候。

在 Currying 里面使用輸入輸出參數會導致與上面不一致的問題
對于創建和返回一個閉包的函數,Swift提供了一個緊湊的語法和結構。雖然這個Currying的語法很簡短緊湊,但是當在里面使用輸出輸入參數時會有一個隱藏的錯誤。為了揭示這個問題,我們使用一個帶有Curring語法的相同例子。不同于聲明一個返回函數類型的函數,這里在第一個參數列表后面還有另一個參數列表,而這也隱藏了閉包的創建:
struct CornmealPizza {    func getCrustGrainSetterWithCurry(inout grain: Grain)() -> Void {        grain = .Corn    }}

與顯式的創建閉包一樣,調用該函數也會返回一個閉包:
var grain: Grain = .Wheatlet pizza = CornmealPizza()let aClosure = pizza.getCrustGrainSetterWithCurry(&grain) 

但是與前面的設置失敗不同,這里成功了:
aClosure()grain    // returns Corn 

顯示構造閉包失敗的地方,Curring能夠成功起到作用

引用
?? 不要在Curring里面使用輸入輸出參數,因為如果你以后將代碼改為顯示創建閉包的話,代碼會不起作用而失效

總結
針對蘋果設備上的軟件編程,Swift語言進行了進行了精心的設計。就像任何雄心勃勃的承諾一樣,總有一些邊緣問題會導致程序不按我們的意愿運行。為了避免這些坑:

?? 在重載協議擴展的屬性時候移動要復查,屬性名稱。
?? 每一個協議擴展中定義的屬性,請在協議中進行聲明。
?? 永遠不要從導入的框架中擴展一個包含可能需要動態分發的屬性的協議
?? 避免對一個協議進行受限擴展,特別是當擴展里面有個新的屬性需要動態分發的時候。
?? 不要將一個可能帶有副作用的語句表達式賦值給左側的可選鏈。
?? 盡量在閉包里面避免使用輸入輸出參數
?? 不要在Curring里面使用輸入輸出參數,因為如果你以后將代碼改為顯示創建閉包的話,代碼會不起作用而失效

相關熱詞搜索:Swift mobile 移動開發

上一篇:2015最受歡迎的Java EE容器
下一篇:微軟宣布將推遲ASP.NET Core的發布日期

分享到: 收藏