通俗的方式理解程式動態型別,靜態型別;強型別,弱型別

什麼是動態(靜態)型別,強(弱)型別

基礎版本

編譯時就知道變數型別的是靜態型別;執行時才知道一個變數型別的叫做動態型別

。比如:

編譯器在將

int age = 18;

這段程式碼編譯的時候就會把 age 的型別確定,換言之,你不能對他進行除以 0 的操作等等,因為型別本身就定義了可操作的集合;但是像 C++ 裡常見的

auto ite = vec。iterator();

這種也屬於靜態型別,這種叫做

型別推導

,透過已知的型別在

編譯時期

推匯出不知道的變數的型別。在靜態型別語言中對一個變數做該變數型別所不允許的操作會報出

語法錯誤

但是像

var name = student。getName();

這行 JavaScript 程式碼就是動態型別的,因為這行程式碼只有在被執行的時候才知道 name 是字串型別的,甚至是 null 或 undefined 型別。你也沒辦法進行型別推導,因為

student.getName

函式簽名根本不包含返回值型別資訊。後面會介紹透過一些其他手段來給函式簽名加上型別。在動態型別中對一個變數做該變數型別所不允許的操作會報出

執行時錯誤

不允許隱式轉換的是強型別,允許隱式轉換的是弱型別

。比如:

在 Python 中進行

‘666’ / 2

你會得到一個

型別錯誤

,這是因為強型別語言中是不允許隱式轉換的,而在 JavaScript 中進行

‘666’ / 2

你會得到整數 333,這是因為在執行運算的時候字串 ‘666’ 先被轉換成整數 666,然後再進行除法運算。

高階版本

需要先介紹一些基本概念:

Program Errors(程式錯誤)

trapped errors:導致程式終止執行(程式

意識

到出錯,

使用

對應的錯誤處理機制),如除 0,Java 中陣列越界訪問

untrapped errors:程式出錯後繼續執行(其實並不一定保證繼續執行,程式本身並

不知道

出錯,也

沒有

對應的錯誤處理機制),如 C 語言裡的緩衝區溢位,Jmp 到錯誤地址

Forbidden Behaviors(禁止行為)

程式在設計的時候會定義一組 forbidden behaviors,包括了所有的 untrapped errors,可能包括 trapped errors。

Well behaved、ill behaved

well behaved: 如果程式的執行不可能出現 forbidden behaviors,則稱為 well behaved

ill behaved: 只要有可能出現 forbidden behaviors,則稱為 ill behaved

他們之間的關係可以用下圖來表達:

通俗的方式理解程式動態型別,靜態型別;強型別,弱型別

從圖中可以看出,綠色的 program 表示所有程式(所有程式,你能想到和不能想到的),error 表示出錯的程式,error 不僅僅包括 trapped error 和 untrapped error。

根據圖我們可以嚴格的定義動態型別,靜態型別;強型別,弱型別

強型別:如果一門語言寫出來的程式在紅色矩形外部,則這門語言是強型別的,也就是上面說的 well behaved

弱型別:如果一門語言寫出來的程式可能在紅色矩形內部,則這門語言是弱型別的,也就是上面說的 ill behaved

靜態型別:一門語言在

編譯

時排除可能出現在紅色矩形內的情況(透過語法報錯),則這門語言是

靜態型別

動態型別:一門語言在

執行

時排除可能出現在紅色矩形內的情況(透過執行時報錯,但如果是弱型別可能會觸發 untrapped error,比如隱式轉換,使得程式看起來似乎是正常執行的),則這門語言是

動態型別

舉個栗子:

在 Python 中執行

test = ‘666’ / 3

你會在執行時得到一個 TypeError 錯誤,相當於執行時排除了 untrapped error,因此 Python 是動態型別,強型別語言。

在 JavaScript 中執行

var test = ‘666’ / 3‘

你會發現 test 的值變成了 222,因為這裡發生了隱式轉換,因此 JavaScript 是動態型別,弱型別的。更為誇張的是

[] == ![]

這樣的程式碼在 JavaScript 中返回的是 true,這裡是具體的 原因。

在 Java 中執行

int[] arr = new int[10]; arr[0] = ’666‘ / 3;

你會在編譯時期得到一個語法錯誤,這說明 Java 是靜態型別的,執行

int[] arr = new int[10]; arr[11] = 3;

你會在執行時得到陣列越界的錯誤(trapped error),這說明 Java 透過自身的型別系統排除了

untrapped error

,因此 Java 是強型別的。

而 C 與 Java 類似,也是靜態型別的,但是對於

int test[] = { 1, 2, 3 }; test[4] = 5;

這樣的程式碼 C 語言是沒辦法發現你的問題的,因此這是

untrapped error

,因此我們說 C 是弱型別的。

下圖是常見的語言型別的劃分:

通俗的方式理解程式動態型別,靜態型別;強型別,弱型別

另外,由於強型別語言一般需要在執行時執行一套型別檢查系統,因此強型別語言的速度一般比弱型別要慢,動態型別也比靜態型別慢,因此在上述所說的四種語言中執行的速度應該是 C > Java > JavaScript > Python。但是強型別,靜態型別的語言寫起來往往是最安全的。

動態型別與靜態型別的區別,如何利用好動態型別

靜態型別由於在編譯期會進行最佳化,所以一般來說效能是比較高的。而動態語言在進行型別操作的時候(比如字串拼接,整數運算)還需要直譯器去猜測其型別,因此效能很低;但是現代的直譯器一般會有一些最佳化措施來提升速度,拿 JavaScript 的 V8 直譯器舉個栗子:

V8 的最佳化過程(粗略版本)

我們知道,像 Java / C++ 這樣的靜態型別語言對於物件一般都會有個類模板(一般呼叫函式的時候都是去類模板找的)。而像 V8 這種則是會在執行時建立類模板,從而在訪問屬性或呼叫方法的時候僅需要計算該屬性在類模板中的偏移就可以了;傳統的 JavaScript 物件一般是透過 Hash 或 Trie 樹實現的,但是查詢的效率很低。拿一段程式碼舉例:

function Point(x, y) { this。x = x; this。y = y; } var p1 = new Point(1, 2);

在使用 new 呼叫 Point 函式的時候會先生成一個 class0 類模板(執行時生成),執行

this。x = x

的時候會生成 class1 類模板,執行

this。y = y

的時候會生成 class2 類模板。具體的轉換過程如下圖:

通俗的方式理解程式動態型別,靜態型別;強型別,弱型別

為一個物件確定一個類模板可以極大的提升屬性的訪問速度,類模板的確定就是透過走圖裡的路徑(轉換路徑)。每當你增加或刪除物件的屬性的時候都會導致物件的類模板發生改變,甚至你增加的順序不同也會生成不同的類模板!

V8 如果發現一個方法被呼叫(傳入相同型別的引數)多次時,會使用 JIT 將函式編譯成二進位制程式碼,從而提升速度。

結合 V8 總結的最佳化方案:

不要輕易的增加刪除一個物件的屬性,對於已有的屬性儘量做到保證型別的不變,保證隱藏類儘可能被複用

例項化屬性的時候儘可能保證屬性新增的順序一致性,保證隱藏類和最佳化程式碼可以被複用

儘可能重複呼叫方法,傳的引數的個數和型別要在多次呼叫時要保持一致

對於陣列,最好使用 push,unshift 等方法去改變陣列大小,緊密的陣列在 V8 中是以連續的地址存的,不要隨意去刪除陣列中的元素,因為稀疏陣列在 V8 中是一個 hash 表

V8 儲存整數用的是 4 個位元組,出現大整數時將會涉及到隱式型別轉換,效能降低,因此儘量不要讓整數超過 32 bit

如何避免弱型別語言所帶來的問題

弱型別語言由於在執行時缺乏型別系統,因此很容易出現型別操作上的 untrapped error;C 語言中我們前面介紹了陣列訪問越界的情況,這裡我們以弱型別語言 JavaScript 為例:

儘量使用嚴格比較符號,如:

===

儘量不要讓字串與其他型別的變數進行運算操作

複雜物件不要在運算子上進行操作

語言型別靜態化的方案

像 JavaScript 這種動態型別的語言靜態化後對執行時的安全性,效率肯定會有很大的提升的,目前有 TypeScript 這種預編譯的方案;還有就是像 flow 這樣的透過註釋來標識型別的方案。