原文
接之前一篇 Pattern Matching 的文章,Type System 是另一項編程語言,或者說編譯器所提供的便利。Pattern Matching 可以讓我們少寫代碼,而 Type System 可以讓我們少犯錯誤,減少 Type 相關的各種 bug。
一般來說,我們寫代碼時為了降低 bug 率,一是依賴於程序員自身的經驗積累,二是靠編譯器做各種靜態檢查,type system 則是屬於靜態檢查這一類。Swift 較之 Objective C 的 type system 有了很大的改進,下面文章中主要是介紹 Swift 相關的一些特性。在開始之前,先聊下如何靠程序員經驗來降低 bug。
Bug 第六感
從我自身的體驗推斷,我相信大部分程序員在寫代碼的時候,對於代碼是否存在 bug 是有一定感知的。只不過有些新入行的朋友,在寫代碼的時候操之過急,或者由於和產品經理討論吃了敗仗心情不佳,coding 時目標變成了寫能 work 的代碼,而不是寫高質量的代碼。
facebook 面試有一個環節叫 whiteboard coding,要求程序員能在白板上寫出幾乎是 「bug free」的代碼,這聽起來有點聳人聽聞,寫代碼沒有 Xcode 提示就罷了,bug free 更是難上加難了。寫一段幾乎沒有 bug 的代碼到底有多難呢?說難不難,說易不易。
寫代碼時,慢一點,再慢一點。好好的想下代碼有可能出錯的地方在哪,想清楚了 bug 就少。一般來說,要減少 bug 量,一是靠程序員自身修養,二是靠編譯器提供的靜態檢查。Type System 屬於第二類,在深入之前,先簡單聊下如何靠自身修養降低 bug 率,提升 bug 感知的第六感。
少寫 Bug 的簡易准則
要提升程序員的自身修養來降低 bug 率,是個大話題,而且多和自身的知識積累有關,需要長年累月的學習和養成。本文的目的不在於此,所以只介紹一個小技巧來養成感知 bug 的好習慣。
我們可以粗略的將我們所寫代碼分為 data 和 behavior,behavior 圍繞 data 執行各種邏輯。一個函數可以看做是一個 behavior,而函數本身又由若干 data 和 behavior 所構成。很多時候,代碼有 bug,是因為 data 出現了預料之外的變化。有一個簡易准則可以減少這類 bug:只要遇到 data,就做 aggressive check。
具體到一個自定義的函數,函數會包含哪些 data 呢?細心理一理沒幾個。
函數入參
內部臨時變量
依賴的外部變量
返回的最終結果
這幾類 data 是我們在一個函數中最經常遇到的,只要我們對他們做好檢查就可保平安。做哪些檢查呢?最常見的也就那麼幾樣,比如是否為 0,為 nil,數組元素 count 為 0,如果期待正數則是否為負數,數組是否越界,多線程是否安全等。做下總結就可以完成大部分的可靠性檢查。簡而言之,只要是使用 data 的時候,就圍繞 data 做好應該的檢查,做到這點,寫一個幾乎沒有 bug 的函數就不怎麼難了。
這個原則更精准的表達是:在任何場景下,無論是定義變量還是使用變量,都對變量的各種可能性做檢查和保護。
Type System
回到我們的正題 Type System,Type System 是由編程語言和 type 相關的各種規則所構成。它的用處也簡單,可以幫助我們減少和 type 相關的 bug。
編程語言大多都有自己的 Type System,Objective C 和 Swift 都有。在開始討論 Type System 之前,要明確 Type 的定義。
Type 就像自然語言裡的名詞,動詞,介詞等等,可以規范我們的表達。在編程語言中,type 則是一種避免代碼表達錯誤的約束。Type 不僅僅包括諸如 int,float,bool 這類 primitive type,對象的 class type,還包括 function,block 等不那麼明顯的 type。變量,常量,函數等等都(且一定)具備 type 信息,有些一眼能看出,有些要靠推斷。
Static vs Dynamic
有些 type 信息是交由程序員去推斷和維護的,有些則是留給編譯器去管理的。前者的 type 約束是在 runtime 檢查的,偏向「dynamic」,後者則在 compile 的時候就做了 check,偏向「static」。
很多技術文章都會討論編程語言的 dynamic 和 static 屬性,我們要分清楚 dynamic 和 static 其實是個寬泛的說法,他們可能包含不同的語義和場景。dynamic 和 static 既可以用來討論 type system,又可以用來形容函數調用機制。比如我們認為 Swift 是 statically typed,但 Objective C 的 runtime 和 message 機制又顯然是 dynamic 的,這兩種場景下 static 和 dynamic 說的其實不是一回事。
回到 type system 的場景,討論下語言是 statically typed 還是 dynamically typed。還是要進一步看場景,在 Objective C 中,type 信息既可以是 static 的,也可以是 dynamic 的,看我們如何使用了,比如下面的代碼中 type 信息是 static 的:
因為 type 的上下文信息是完整的,編譯器可以做類型判斷。而如下代碼中 type 信息則是 dynamic 的:
由於 id 可以指向任意對象類型,id 可以在不同的時間點裡指向不同的類型,編譯器此時無法根據類型信息作出判斷,是否存在類型使用錯誤的。所以我們會說像 Objective C 這類編程語言在 type system 上,是同時具備 static 和 dynamic 屬性的,關鍵還是看具體的使用場景。
但 Swift 卻是貨真價實的,純粹的 statically typed 編程語言,不具備任何 dynamically typed 的屬性。比如在 Swift 中,如下代碼是無法通過編譯的:
編譯器會提示:Type annotation missing in pattern,也就是缺少 type 信息。要聲明一個變量,我們可以通過如下兩種方式來提供 type 信息:
方式一是通過賦值來做 type inference,方式二是通過顯式的提供 type 信息。Swift 在 type 的使用上非常苛刻,當之無愧為 statically typed。
顯然,static type 比 dynamic type 更安全,編譯器可以幫我們做類型檢查,這也是為什麼 Swift 比 Objective C 在 type safety 上更優秀的原因。當然,dynamic type 並非全無好處,初期開發起來速度會快於 static type,而且省去了編譯時的 type 檢查,每次編譯速度更快。缺點是一旦出現 runtime 中的類型錯誤,要花更多的時間去調試,要寫更多的 test case,准備更多的文檔。這種缺陷在較大規模的項目上會更明顯,Swift 選擇 static type 策略應該也有這方面的考慮。
Type Inference
類型推斷(type inference)也是 type system 當中的一個常見概念。不少編程語言比如 Swift 都有 type inference 的功能。type inference 有什麼用處呢?statically typed 的編程語言決定了變量都必須具備類型信息,意味著我們每次使用變量的時候都需要顯式的聲明 type 信息,比如在 Objective C中,這樣會顯得有些繁瑣和啰嗦,一旦有了 type inference,我們可以在代碼中省略掉很多關於 type 累贅的表述。我們看如下代碼:
var i = 0
這行代碼中,有兩個實體有 type 信息,變量 i 和常量 0,0 默認的 type 信息是 int,i 的 type 信息沒有顯示的聲明出來,但在 Swift 中,由於 0 被賦值給了 i,所以可以通過 type inference 推斷出 i 的 type 信息也是 int。這種類型推斷會發生在很多程序員意識不到的角落,這種具備傳染特性的 type 信息可以層層疊疊,一級一級的輸送到更多的其他變量實體。編譯器就是通過這種傳染的特性來做 type inference 的。
在 Swift 中,type inference 配合 static type 讓代碼既精煉又安全。
Optional Type
前面提到 type 信息本質上是一種約束,可以避免 type 的使用錯誤。我們在編寫代碼時,經常遇到的一種 bug 是對於空對象或者說對象為 nil 情況,漏寫了為空的判斷。Swift 通過引入 optional type 來強制開發者考慮 nil 的場景,更妙的是,當「是否為 nil 」成為 type 信息之後,編譯器也可以一起來幫助檢查 nil 的使用場景。看下 optional type 的定義就一清二楚了:
通過 enum type 來定義 optional type,以表達是否為 nil 的含義。這也是 Swift 為什麼要引入 optional type 的根本原因,讓編譯器以類型檢查的方式,來幫助開發者分析是否存在漏判 nil 的場景。
Generic Type
再次強調下,type 本質上是一種約束。當我們定義 int i 時,int 就成為了變量 i 的一種約束。我們可以把這種約束進一步強化,比如引入 generic type(泛型)。generic type 有兩個主要特性,其一是允許開發者在後期再指定 type 的值,其二是可以把 type 約束施加到指定的代碼范圍裡。理解這兩個特性,是我們掌握 generic type 各種表現形式的基礎。
generic type 可以讓我們寫出更加符合 type safety 的代碼,Objective C 和 Swift 都支持定義 generic type,只不過 Swift 中 generic 的概念更加廣泛,應用面也大很多,在一些顯式的和隱式的地方都存在 generic 的身影。比如前面提到的 optional,其實也是個 generic type。
generic 可以作用於很多其他的復雜 type,比如 optional 就是 generic 作用於 enum 的結果,除了 enum 之外,還有 struct,class,function 等都可以和 generic 搭配使用。我們再看一個 Swift 自帶的例子,Array:
只需要在 struct 名字後面以 的形式,就可以在 struct 的作用域內部聲明一個新的 xxx type(xxx 在 Array 的 extension 中也是可見的),xxx 可以在使用時再確定具體指代什麼 type。使用 Array 的時候,我們也不必顯示的指明 xxx 代表什麼,可以依賴前面提到的 type inference:
上面第二行會報錯,這是 Swift 和 Objective C 的差異之處,在 Objective C 中,我們可以在 Array 中放入不同類型的對象,而在 Swift 中,一旦 Array 中的元素類型被 type inference 確定,就不能放入其他類型的對象了。generic type 和 type inference 配合的場景在 Swift 當中經常出現。
Named Type vs Compound Type
named type 指的是我們傳統意義上所理解的 data type,例如 int,float,string,自定義的 class 等等。在 Swift 中,設計者引入了 compound type 的概念,可以把 compound type 理解成 named type 的某種集合,比如 function 和 tuple,他們往往都包含多個 named type。
我們知道在 Swift 中,function 是一等公民,可以作為變量聲明,參數,返回值等等,要理解並運用這一點,需要在思維上做轉換,把 function 也看做一種 data type(compound type),在原先使用 named type 的位置,我們幾乎都可以使用 compound type。
compound type 增強了語言的表達力,但其靈活性在一些場景下,也會一定程度的降低代碼的可閱讀性。compound type 可以包其他 compound type,可以一層層的套嵌,這種 nested compound type 有時候會讓代碼看上去沒那麼直觀,比如下面一段 Swift 代碼:
上面的函數裡,function 和 tuple 作為 compound type 存在套嵌,代碼本身雖然不長,要一眼把其中包含的 type 都識別出來不那麼容易。compound type 的使用可能是不少從 Objective C 轉向 Swift 的同學初期感覺難以適應的原因之一。
總結
上述所提到的概念都是和 type system 相關的基礎知識,雖然基礎,卻十分重要。對 type system 建立完整全面的認識,多利用語言本身的 type 制約來避免 bug,可以讓我們對自己代碼的安全性有更好的把握,對於代碼質量的提升也有極大的幫助。