編程語言按照類型檢查可以分為兩大類:靜態(tài)類型 (Static Typing) 和 動(dòng)態(tài)類型 (Dynamic Typing)。在現(xiàn)在比較流行的這些語言里,靜態(tài)類型的代表人物有 Java、C/C++、Golang 等,動(dòng)態(tài)類型有 Python、Ruby 等。
靜態(tài)類型和動(dòng)態(tài)類型有什么區(qū)別呢?為什么在程序語言設(shè)計(jì)時(shí)要考慮是靜態(tài)還是動(dòng)態(tài)?在寫代碼時(shí),Python 寫起來簡(jiǎn)潔,效率高,可能100行 Java 的程序10行 Python 就能搞定,所以以前我覺得靜態(tài)類型的語言有點(diǎn)太死板了,不如動(dòng)態(tài)類型的。但是經(jīng)過這段時(shí)間編程語言的學(xué)習(xí),不能說一個(gè)比另一個(gè)好,個(gè)有所長(zhǎng)吧。這篇文章從3個(gè)角度、6個(gè)方面對(duì)比靜態(tài)類型和動(dòng)態(tài)類型。
為什么會(huì)有靜態(tài)類型/動(dòng)態(tài)類型的概念?
程序都需要進(jìn)行錯(cuò)誤的檢查。比如 3 / 0,這個(gè)程序會(huì)有錯(cuò)誤,我們應(yīng)該在什么時(shí)候進(jìn)行檢查呢?
- 在寫程序時(shí),只要編輯器里出現(xiàn)除以0,報(bào)錯(cuò)。
- 在編譯時(shí),如果檢查到了除以0,報(bào)錯(cuò)。
- 在程序運(yùn)行時(shí),運(yùn)行到了除以9,報(bào)錯(cuò)。
- 不報(bào)錯(cuò),返回?zé)o窮大 (+inf.0)。
不同的語言設(shè)計(jì)上會(huì)選擇在這4個(gè)過程中不同時(shí)候去報(bào)錯(cuò)。靜態(tài)類型和動(dòng)態(tài)類型的區(qū)別在于什么時(shí)候報(bào)類型的錯(cuò)誤?,比如說 3 / "a“,靜態(tài)類型多是在編譯時(shí),動(dòng)態(tài)類型多是在程序運(yùn)行時(shí)。怎么報(bào)類型的錯(cuò)誤呢?語言里會(huì)有類型檢查的機(jī)制,類型檢查的目的是避免程序發(fā)生一些事情。
編程語言在設(shè)計(jì)時(shí),要考慮 什么程序要類型檢查?,怎么執(zhí)行類型檢查?。靜態(tài)類型和動(dòng)態(tài)類型是這兩個(gè)問題不同回答的產(chǎn)物。
從寫代碼的角度對(duì)比
方便性 Convenience
靜態(tài)類型更好:靜態(tài)類型比較方便,因?yàn)椴挥萌z查 x 是不是 number,* 默認(rèn)只能是 number。
; Racket
(define (cube x)
(if (not (number? x))
(error "bad arguments")
(* x x x)))
(* ML *)
fun cube x = x * x * x
動(dòng)態(tài)類型更好:動(dòng)態(tài)類型比較方便,因?yàn)橐粋€(gè)函數(shù)可以根據(jù)需要返回不同的類型。靜態(tài)類型卻需要去構(gòu)造一個(gè)新的數(shù)據(jù)類型才能實(shí)現(xiàn)。
; Racket
(define (f y) (if (> y 0) (+ y y) "hi"))
(* ML *)
datatype t = Int of int | String of string
fun f y = if y > 0 then Int(y+y) else String "hi"
fun foo x = case f x of
Int i => Int.toString i
| String s => s
更早的發(fā)現(xiàn)錯(cuò)誤 Catching bugs earlier
靜態(tài)類型在編譯時(shí)就能發(fā)現(xiàn)類型上的錯(cuò)誤,都不用寫 tests,可以比動(dòng)態(tài)類型更早的找到 bug。
但是喜歡動(dòng)態(tài)類型的人會(huì)說,靜態(tài)類型只能找到"簡(jiǎn)單"的錯(cuò)誤,還是需要寫單元測(cè)試的,在寫單元測(cè)試時(shí),肯定就能發(fā)現(xiàn)這些"簡(jiǎn)單"的錯(cuò)誤了。
性能 Performance
靜態(tài)類型的程序在運(yùn)行時(shí)更快,因?yàn)樵诰幾g時(shí)已經(jīng)進(jìn)行了檢測(cè),不需要去儲(chǔ)存和檢測(cè)類型,可以節(jié)省程序運(yùn)行的時(shí)間和空間。
但是喜歡動(dòng)態(tài)類型的人會(huì)說,動(dòng)態(tài)類型在性能很關(guān)鍵的部分,可以有一些辦法去優(yōu)化類型的儲(chǔ)存和檢測(cè),比如說 (let ([x (+ y y)]) (* x 4)),有兩個(gè) y,可以只檢測(cè)一個(gè),4 是一個(gè)整數(shù),不需要檢測(cè),x 是 y + y 的結(jié)果,所以后面的那個(gè) x 也可以不用檢測(cè)。通過這些方式可以對(duì)程序進(jìn)行一些優(yōu)化,而不需要像靜態(tài)類型一樣要受到各種類型的限制。
代碼重用 Code Reuse
動(dòng)態(tài)類型更好:動(dòng)態(tài)類型代碼重用率更高,因?yàn)闆]有嚴(yán)格的類型系統(tǒng),代碼可以被不同類型的數(shù)據(jù)重用。一個(gè)最簡(jiǎn)單的例子
# Ruby
def double x
x + x
end
x 可以是數(shù)字,把數(shù)字翻倍。x 可以是 string,把兩個(gè) string 連在一起。
動(dòng)態(tài)類型中一個(gè) list 里可以有不同的類型的數(shù)據(jù),在循環(huán)遍歷時(shí)會(huì),能重用更多代碼。
靜態(tài)類型更好:靜態(tài)類型也有代碼重用的很多方法,比如泛型,子類型等等。而且一個(gè) list 只有一種類型的數(shù)據(jù),可以避免一些難找的bug,也可以避免因?yàn)轭愋妥杂啥鵀E用一些庫(kù)。
原型開發(fā) Prototyping
動(dòng)態(tài)類型更好:動(dòng)態(tài)類型更適合原型開發(fā),因?yàn)樵?Prototyping 時(shí),不一定知道確切的數(shù)據(jù)結(jié)構(gòu)和函數(shù),類型上的自由可以不用在做原型時(shí)就確定數(shù)據(jù)結(jié)構(gòu),導(dǎo)致了要一直不斷的去滿足類型的檢查,降低了原型開發(fā)的效率。
靜態(tài)類型更好:雖然效率上不一定比的上動(dòng)態(tài)類型,靜態(tài)類型能更好的記錄整個(gè)系統(tǒng)在 Prototyping 的過程中,可以知道整個(gè)過程數(shù)據(jù)類型、數(shù)據(jù)結(jié)構(gòu)是怎么變化的。如果是跟之前完全不相關(guān)的代碼,可以直接重寫,不需要在之前的做更改。在之前代碼不確定的地方,可以用一些表示所有其他類型的方法,比如 | _ => raise Unimplemented。
再開發(fā)和維護(hù) Evolution & Maintaince
動(dòng)態(tài)類型更好:在更改代碼時(shí),可以把代碼能接受的變得更"寬",比如說修改函數(shù)返回類型,調(diào)用的代碼如果對(duì)返回類型沒有問題,可以不用更改。靜態(tài)類型必須要更改所有調(diào)用的代碼,不能進(jìn)行局部的測(cè)試。
靜態(tài)類型更好:動(dòng)態(tài)類型的不用更改舊代碼是一個(gè)隱患,坊間流傳動(dòng)態(tài)類型是「寫時(shí)一時(shí)爽,重構(gòu)火葬場(chǎng)」,靜態(tài)類型的類型檢查會(huì)列出所有需要更改的地方,可以避免一些隱藏的bug。
總結(jié)
經(jīng)過這些對(duì)比,可以看出靜態(tài)類型和動(dòng)態(tài)類型各有所長(zhǎng),不能簡(jiǎn)單粗暴的說一種比另外一種更好。個(gè)人而言,我覺得動(dòng)態(tài)類型更適合比較小的程序,像 Python,Ruby,做為腳本語言,能簡(jiǎn)單快速的寫完對(duì)文件的處理等。動(dòng)態(tài)類型 Java 和 C++ 則能過支持大型的軟件工程項(xiàng)目。
當(dāng)然具體選擇靜態(tài)類型或者動(dòng)態(tài)類型,取決于想要什么時(shí)候做類型檢查?想要什么樣的語言特性,并且應(yīng)該知道選擇的 trade-off 是什么樣的。
Reference
Coursera Programming Languages, Part B Week 3