導(dǎo)讀:當(dāng)描述一門編程語言的時(shí)候民假,我們一般需要區(qū)分它是動(dòng)態(tài)類型還是靜態(tài)類型,區(qū)分它是強(qiáng)類型還是弱類型叠纷。然而愕贡,很多人會(huì)將這幾種類型搞錯(cuò)。本文的目的就是來辨析清楚這四種類型。文中涉及多種編程語言的比對(duì)赃梧,主要介紹的是各編程語言的共性話題滤蝠,希望能給你帶來一些啟發(fā)。
0x01 引言
今天和一個(gè)朋友討論 C++ 是強(qiáng)類型還是弱類型的時(shí)候授嘀,他告訴我 C++ 是強(qiáng)類型的物咳,他和我說因?yàn)?C++ 在寫的時(shí)候需要 int,float 等等關(guān)鍵字去定義變量蹄皱,因此 C++ 是強(qiáng)類型的览闰,我告訴他 C++ 是弱類型的,他竟然還嘲笑我不懂基礎(chǔ)巷折。
我又嘗試去問了另外一個(gè)同學(xué) Python 是強(qiáng)類型還是弱類型的時(shí)候压鉴,得到的竟然是弱類型,就因?yàn)槎x變量沒有 int盔几,float晴弃!
然后我想找一些網(wǎng)上的資料試圖告訴他們他們是錯(cuò)的(我是對(duì)的),結(jié)果發(fā)現(xiàn)網(wǎng)上的資料大多為了嚴(yán)謹(jǐn)結(jié)果把簡單的問題(其實(shí)并不簡單)說的很復(fù)雜逊拍。比如:知乎上的一些 回答上鞠。所以用通俗的方式,以大多數(shù)程序猿(媛)所需要了解的知識(shí)去介紹類型系統(tǒng)芯丧,但是又不喪失嚴(yán)謹(jǐn)性就是這篇文章寫的意義芍阎。
0x02 什么是動(dòng)態(tài)(靜態(tài))類型,強(qiáng)(弱)類型
基礎(chǔ)版本
編譯時(shí)就知道變量類型的是靜態(tài)類型缨恒;運(yùn)行時(shí)才知道一個(gè)變量類型的叫做動(dòng)態(tài)類型谴咸。比如:
編譯器在將 int age = 18;這段代碼編譯的時(shí)候就會(huì)把 age 的類型確定,換言之骗露,你不能對(duì)他進(jìn)行除以 0 的操作等等岭佳,因?yàn)轭愋捅旧砭投x了可操作的集合;但是像 C++ 里常見的 auto ite = vec.iterator(); 這種也屬于靜態(tài)類型萧锉,這種叫做類型推導(dǎo)珊随,通過已知的類型在編譯時(shí)期推導(dǎo)出不知道的變量的類型。在靜態(tài)類型語言中對(duì)一個(gè)變量做該變量類型所不允許的操作會(huì)報(bào)出語法錯(cuò)誤柿隙。
但是像 var name = student.getName(); 這行 Java 代碼就是動(dòng)態(tài)類型的叶洞,因?yàn)檫@行代碼只有在被執(zhí)行的時(shí)候才知道 name 是字符串類型的,甚至是 null 或 undefined 類型禀崖。你也沒辦法進(jìn)行類型推導(dǎo)衩辟,因?yàn)?student.getName 函數(shù)簽名根本不包含返回值類型信息。后面會(huì)介紹通過一些其他手段來給函數(shù)簽名加上類型波附。在動(dòng)態(tài)類型中對(duì)一個(gè)變量做該變量類型所不允許的操作會(huì)報(bào)出運(yùn)行時(shí)錯(cuò)誤艺晴。
不允許隱式轉(zhuǎn)換的是強(qiáng)類型昼钻,允許隱式轉(zhuǎn)換的是弱類型。比如:
在 Python 中進(jìn)行 '666' / 2 你會(huì)得到一個(gè)類型錯(cuò)誤财饥,這是因?yàn)閺?qiáng)類型語言中是不允許隱式轉(zhuǎn)換的换吧,而在 Java 中進(jìn)行 '666' / 2 你會(huì)得到整數(shù) 333,這是因?yàn)樵趫?zhí)行運(yùn)算的時(shí)候字符串 '666' 先被轉(zhuǎn)換成整數(shù) 666钥星,然后再進(jìn)行除法運(yùn)算沾瓦。
高級(jí)版本
需要先介紹一些基本概念:
Program Errors(程序錯(cuò)誤)
- trapped errors:導(dǎo)致程序終止執(zhí)行(程序意識(shí)到出錯(cuò),使用對(duì)應(yīng)的錯(cuò)誤處理機(jī)制)谦炒,如除 0贯莺,Java 中數(shù)組越界訪問
- untrapped errors:程序出錯(cuò)后繼續(xù)執(zhí)行(其實(shí)并不一定保證繼續(xù)執(zhí)行,程序本身并不知道出錯(cuò)宁改,也沒有對(duì)應(yīng)的錯(cuò)誤處理機(jī)制)缕探,如 C 語言里的緩沖區(qū)溢出,Jmp 到錯(cuò)誤地址
Forbidden Behaviors(禁止行為)
程序在設(shè)計(jì)的時(shí)候會(huì)定義一組 forbidden behaviors还蹲,包括了所有的 untrapped errors爹耗,可能包括 trapped errors。
Well behaved谜喊、ill behaved
- well behaved: 如果程序的執(zhí)行不可能出現(xiàn) forbidden behaviors潭兽,則稱為 well behaved
- ill behaved: 只要有可能出現(xiàn) forbidden behaviors,則稱為 ill behaved
他們之間的關(guān)系可以用下圖來表達(dá):
從圖中可以看出斗遏,綠色的 program 表示所有程序(所有程序山卦,你能想到和不能想到的),error 表示出錯(cuò)的程序诵次,error 不僅僅包括 trapped error 和 untrapped error账蓉。
根據(jù)圖我們可以嚴(yán)格的定義動(dòng)態(tài)類型,靜態(tài)類型逾一;強(qiáng)類型铸本,弱類型
- 強(qiáng)類型:如果一門語言寫出來的程序在紅色矩形外部,則這門語言是強(qiáng)類型的遵堵,也就是上面說的 well behaved
- 弱類型:如果一門語言寫出來的程序可能在紅色矩形內(nèi)部箱玷,則這門語言是弱類型的,也就是上面說的 ill behaved
- 靜態(tài)類型:一門語言在編譯時(shí)排除可能出現(xiàn)在紅色矩形內(nèi)的情況(通過語法報(bào)錯(cuò))鄙早,則這門語言是靜態(tài)類型的
- 動(dòng)態(tài)類型:一門語言在運(yùn)行時(shí)排除可能出現(xiàn)在紅色矩形內(nèi)的情況(通過運(yùn)行時(shí)報(bào)錯(cuò)汪茧,但如果是弱類型可能會(huì)觸發(fā) untrapped error椅亚,比如隱式轉(zhuǎn)換限番,使得程序看起來似乎是正常運(yùn)行的),則這門語言是動(dòng)態(tài)類型的
舉個(gè)栗子:
在 Python 中執(zhí)行 test = '666' / 3 你會(huì)在運(yùn)行時(shí)得到一個(gè) TypeError 錯(cuò)誤呀舔,相當(dāng)于運(yùn)行時(shí)排除了 untrapped error弥虐,因此 Python 是動(dòng)態(tài)類型扩灯,強(qiáng)類型語言。
在 Java 中執(zhí)行 var test = '666' / 3' 你會(huì)發(fā)現(xiàn) test 的值變成了 222霜瘪,因?yàn)檫@里發(fā)生了隱式轉(zhuǎn)換珠插,因此 Java 是動(dòng)態(tài)類型,弱類型的颖对。更為夸張的是 [] == ![] 這樣的代碼在 Java 中返回的是 true捻撑,這里是具體的 原因。
在 Java 中執(zhí)行 int[] arr = new int[10]; arr[0] = '666' / 3; 你會(huì)在編譯時(shí)期得到一個(gè)語法錯(cuò)誤缤底,這說明 Java 是靜態(tài)類型的顾患,執(zhí)行 int[] arr = new int[10]; arr[11] = 3; 你會(huì)在運(yùn)行時(shí)得到數(shù)組越界的錯(cuò)誤(trapped error),這說明 Java 通過自身的類型系統(tǒng)排除了 untrapped error个唧,因此 Java 是強(qiáng)類型的江解。
而 C 與 Java 類似,也是靜態(tài)類型的徙歼,但是對(duì)于 int test[] = { 1, 2, 3 }; test[4] = 5; 這樣的代碼 C 語言是沒辦法發(fā)現(xiàn)你的問題的犁河,因此這是 untrapped error,因此我們說 C 是弱類型的魄梯。
下圖是常見的語言類型的劃分:
另外桨螺,由于強(qiáng)類型語言一般需要在運(yùn)行時(shí)運(yùn)行一套類型檢查系統(tǒng),因此強(qiáng)類型語言的速度一般比弱類型要慢画恰,動(dòng)態(tài)類型也比靜態(tài)類型慢彭谁,因此在上述所說的四種語言中執(zhí)行的速度應(yīng)該是 C > Java > Java > Python。但是強(qiáng)類型允扇,靜態(tài)類型的語言寫起來往往是最安全的缠局。
0x03 動(dòng)態(tài)類型與靜態(tài)類型的區(qū)別,如何利用好動(dòng)態(tài)類型
靜態(tài)類型由于在編譯期會(huì)進(jìn)行優(yōu)化考润,所以一般來說性能是比較高的狭园。而動(dòng)態(tài)語言在進(jìn)行類型操作的時(shí)候(比如字符串拼接,整數(shù)運(yùn)算)還需要解釋器去猜測其類型糊治,因此性能很低唱矛;但是現(xiàn)代的解釋器一般會(huì)有一些優(yōu)化措施來提升速度,拿 Java 的 V8 解釋器舉個(gè)栗子:
V8 的優(yōu)化過程(粗略版本)
我們知道井辜,像 Java / C++ 這樣的靜態(tài)類型語言對(duì)于對(duì)象一般都會(huì)有個(gè)類模板(一般調(diào)用函數(shù)的時(shí)候都是去類模板找的)绎谦。而像 V8 這種則是會(huì)在運(yùn)行時(shí)創(chuàng)建類模板,從而在訪問屬性或調(diào)用方法的時(shí)候僅需要計(jì)算該屬性在類模板中的偏移就可以了粥脚;傳統(tǒng)的 Java 對(duì)象一般是通過 Hash 或 Trie 樹實(shí)現(xiàn)的窃肠,但是查找的效率很低。拿一段代碼舉例:
在使用 new 調(diào)用 Point 函數(shù)的時(shí)候會(huì)先生成一個(gè) class0 類模板(運(yùn)行時(shí)生成)刷允,執(zhí)行 this.x = x 的時(shí)候會(huì)生成 class1 類模板冤留,執(zhí)行 this.y = y 的時(shí)候會(huì)生成 class2 類模板碧囊。具體的轉(zhuǎn)換過程如下圖:
為一個(gè)對(duì)象確定一個(gè)類模板可以極大的提升屬性的訪問速度,類模板的確定就是通過走圖里的路徑(轉(zhuǎn)換路徑)纤怒。每當(dāng)你增加或刪除對(duì)象的屬性的時(shí)候都會(huì)導(dǎo)致對(duì)象的類模板發(fā)生改變糯而,甚至你增加的順序不同也會(huì)生成不同的類模板!
V8 如果發(fā)現(xiàn)一個(gè)方法被調(diào)用(傳入相同類型的參數(shù))多次時(shí)泊窘,會(huì)使用 JIT 將函數(shù)編譯成二進(jìn)制代碼熄驼,從而提升速度。
結(jié)合 V8 總結(jié)的優(yōu)化方案:
- 不要輕易的增加刪除一個(gè)對(duì)象的屬性烘豹,對(duì)于已有的屬性盡量做到保證類型的不變谜洽,保證隱藏類盡可能被復(fù)用
- 實(shí)例化屬性的時(shí)候盡可能保證屬性添加的順序一致性,保證隱藏類和優(yōu)化代碼可以被復(fù)用
- 盡可能重復(fù)調(diào)用方法吴叶,傳的參數(shù)的個(gè)數(shù)和類型要在多次調(diào)用時(shí)要保持一致
- 對(duì)于數(shù)組阐虚,最好使用 push,unshift 等方法去改變數(shù)組大小蚌卤,緊密的數(shù)組在 V8 中是以連續(xù)的地址存的实束,不要隨意去刪除數(shù)組中的元素,因?yàn)橄∈钄?shù)組在 V8 中是一個(gè) hash 表
- V8 存儲(chǔ)整數(shù)用的是 4 個(gè)字節(jié)逊彭,出現(xiàn)大整數(shù)時(shí)將會(huì)涉及到隱式類型轉(zhuǎn)換咸灿,性能降低,因此盡量不要讓整數(shù)超過 32 bit
0x04 如何避免弱類型語言的問題
弱類型語言由于在運(yùn)行時(shí)缺乏類型系統(tǒng)侮叮,因此很容易出現(xiàn)類型操作上的 untrapped error避矢;C 語言中我們前面介紹了數(shù)組訪問越界的情況,這里我們以弱類型語言 Java 為例:
- 盡量使用嚴(yán)格比較符號(hào)囊榜,如:===
- 盡量不要讓字符串與其他類型的變量進(jìn)行運(yùn)算操作
- 復(fù)雜對(duì)象不要在運(yùn)算符上進(jìn)行操作
0x05 語言類型靜態(tài)化的方案
像 Java 這種動(dòng)態(tài)類型的語言靜態(tài)化后對(duì)運(yùn)行時(shí)的安全性审胸,效率肯定會(huì)有很大的提升的,目前有 Type 這種預(yù)編譯的方案卸勺;還有就是像 flow 這樣的通過注釋來標(biāo)識(shí)類型的方案砂沛。