麻省理工18年春软件构造课程阅读01“静态检查”
本文內(nèi)容來(lái)自MIT_6.031_sp18: Software Construction課程的Readings部分,采用CC BY-SA 4.0協(xié)議。
由于我們學(xué)校(哈工大)大二軟件構(gòu)造課程的大部分素材取自此,也是推薦的閱讀材料之一,于是打算做一些翻譯工作,自己學(xué)習(xí)的同時(shí)也能幫到一些懶得看英文的朋友。另外,該課程的閱讀資料中有許多練習(xí)題,但是沒(méi)有標(biāo)準(zhǔn)答案,所給出的答案均為譯者所寫,有錯(cuò)誤的地方還請(qǐng)指出。
譯者:李秋豪 王一航
審校:李秋豪
V1.0 Thu Mar 1 00:49:04 CST 2018
Reading 1: 靜態(tài)檢查
今天課程的目標(biāo):
- 學(xué)習(xí)靜態(tài)類型
- 了解好的軟件應(yīng)該具有的三個(gè)屬性
冰雹序列
作為一個(gè)運(yùn)行示例,我們先來(lái)了解一下“冰雹序列”,它是這樣定義的:從正整數(shù)n開(kāi)始,如果n是偶數(shù),則下一個(gè)數(shù)是n/2,否則下一個(gè)數(shù)是3n+1,直到n等于1。這里有幾個(gè)例子:
2, 1 3, 10, 5, 16, 8, 4, 2, 1 4, 2, 1 2n, 2n-1 , … , 4, 2, 1 5, 16, 8, 4, 2, 1 7, 22, 11, 34, 17, 52, 26, 13, 40, …? (會(huì)停止嗎?)由于存在3n+1這種變化,所以序列元素的大小可能會(huì)忽高忽低——這也是“冰雹序列”名稱的來(lái)歷,冰雹在落地前會(huì)在云層中忽上忽下。那么所有的序列都會(huì)最終“落地”變到1嗎?(這個(gè)猜想稱為考拉茲猜想 ,現(xiàn)在還沒(méi)有一個(gè)好的解決方法。)
計(jì)算冰雹序列
為了計(jì)算冰雹序列里的元素,我們可以在java和python中分別這樣寫:
// Java int n = 3; while (n != 1) {System.out.println(n);if (n % 2 == 0) {n = n / 2;} else {n = 3 * n + 1;} } System.out.println(n); # Python n = 3 while n != 1:print(n)if n % 2 == 0:n = n / 2else:n = 3 * n + 1 print(n)有些東西值得一提(譯者注:MIT大一學(xué)的是python,所以這里談了一下java和python語(yǔ)法上的區(qū)別,懂的讀者可以略過(guò)):
- Java和Python的基本語(yǔ)法很相似,例如while 和 if
- Java在每一個(gè)句子結(jié)束時(shí)要求以分號(hào)作為結(jié)尾。這看起來(lái)有些麻煩,但也給了你很多自由以便安排代碼——例如你可以將一行代碼寫成兩行然后以分號(hào)結(jié)束。
- Java在使用 if 和 while的時(shí)候,其中的條件判斷需要用括號(hào)括起來(lái)。
- Java使用花括號(hào)將一個(gè)語(yǔ)句塊分割開(kāi)來(lái)——python是以縮進(jìn)作為區(qū)分,所以你寫一些別的空格符也是可以的。但是編程也是一種交流,你不僅要和編譯器交流,還要和別的程序員交流,所以縮進(jìn)也是必要的,我們?cè)诤竺娴恼n中會(huì)專門講這個(gè)。
類型
在上面的代碼中,Python和Java最大的不同就是Java需要指定變量n的類型:int
類型是一些值的集合,以及這些值對(duì)應(yīng)的操作。
例如下面這5種常用的原始類型 :
- int 例如5和-200這樣的整數(shù),但是其范圍有限制,大概在±20億)
- long (比int更大范圍的整數(shù))
- boolean對(duì)或錯(cuò)這兩個(gè)值)
- double (浮點(diǎn)數(shù),其表示的是實(shí)數(shù)的子集)
- char (單個(gè)字符例如 'A' 和 '$')
Java也有對(duì)象類型 ,例如:
- String 表示一串連續(xù)的字符。
- BigInteger 表示任意大小的整數(shù)。
從Java的傳統(tǒng)來(lái)說(shuō),原始類型用小寫字母,對(duì)象類型的起始字母用大寫。
操作符是一些能接受輸入并輸出結(jié)果的功能。他們的語(yǔ)法各有區(qū)別,Java中常見(jiàn)的有下面這三種:
前綴、中綴、后綴操作符. 例如, a + b 調(diào)用這樣一種操作(映射) + : int × int → int
( + 是這個(gè)操作符的名字, int × int 描述了這兩個(gè)輸入, 最后的 int 描述的了輸出)- 一個(gè)對(duì)象的方法. 例如, bigint1.add(bigint2) 調(diào)用這樣一種操作(映射) add: BigInteger × BigInteger → BigInteger.
一個(gè)函數(shù). 例如: Math.sin(theta) 調(diào)用這樣一種操作(映射) sin: double → double. 注意, Math 不是一個(gè)對(duì)象,它是一個(gè)包含sin函數(shù)的類。
有一些操作符可以對(duì)不同類型的對(duì)象進(jìn)行操作,這時(shí)我們就稱之為可重載 (overloaded),例如Java中的算術(shù)運(yùn)算符 +, -, *, / 都是可重載的。一些函數(shù)也是可重載的。大多數(shù)編程語(yǔ)言都有不容程度的重載性。
靜態(tài)類型
Java是一種靜態(tài)類型的語(yǔ)言。所有變量的類型在編譯的時(shí)候就已經(jīng)知道了(程序還沒(méi)有運(yùn)行),所以編譯器也可以推測(cè)出每一個(gè)表達(dá)式的類型。例如,如果a和b是int類型的,那么編譯器就可以知道a+b的結(jié)果也是int類型的。事實(shí)上,Eclipse在你寫代碼的時(shí)候就在做這些檢查,所以你就能夠在編輯的同時(shí)發(fā)現(xiàn)這些問(wèn)題。
在動(dòng)態(tài)類型語(yǔ)言中(例如Python),這種類型檢查是發(fā)生在程序運(yùn)行的時(shí)候。
靜態(tài)類型是靜態(tài)檢查的一種——檢查發(fā)生在編譯的時(shí)候。本課程的一個(gè)重要目標(biāo)就是教會(huì)你避免bug的產(chǎn)生,靜態(tài)檢查就是我們知道的第一種方法。其中靜態(tài)類型就阻止了一大部分和類型相關(guān)的bug——確切點(diǎn)說(shuō),就是將操作符用到了不對(duì)應(yīng)的類型對(duì)象上。例如,如果你進(jìn)行下面這個(gè)操作,試圖將兩個(gè)字符串進(jìn)行算術(shù)乘法:
"5" * "6"那么靜態(tài)類型檢查就會(huì)在你編輯代碼的時(shí)候發(fā)現(xiàn)這個(gè)bug,而不是等到你編譯后運(yùn)行程序的時(shí)候(編譯也不通過(guò))。
譯者注:這里注意不要和強(qiáng)類型和弱類型弄混了,知乎上有一個(gè)問(wèn)答可以參考一下:弱類型、強(qiáng)類型、動(dòng)態(tài)類型、靜態(tài)類型語(yǔ)言的區(qū)別是什么?
(圖片來(lái)自知乎)
靜態(tài)檢查、動(dòng)態(tài)檢查、無(wú)檢查
編程語(yǔ)言通常能提供以下三種自動(dòng)檢查的方法:
- 靜態(tài)檢查: bug在程序運(yùn)行前發(fā)現(xiàn)
- 動(dòng)態(tài)檢查: bug在程序運(yùn)行中發(fā)現(xiàn)
- 無(wú)檢查: 編程語(yǔ)言本身不幫助你發(fā)現(xiàn)錯(cuò)誤,你必須通過(guò)特定的條件(例如輸出的結(jié)果)檢查代碼的正確性。
很明顯,靜態(tài)檢查好于動(dòng)態(tài)檢查好于不檢查。
這里有一些“經(jīng)驗(yàn)法則”,告訴你這靜態(tài)和動(dòng)態(tài)檢查通常會(huì)發(fā)現(xiàn)什么bug:
靜態(tài)檢查 :
- 語(yǔ)法錯(cuò)誤,例如多余的標(biāo)點(diǎn)符號(hào)或者錯(cuò)誤的關(guān)鍵詞。即使在動(dòng)態(tài)類型的語(yǔ)言例如Python中也會(huì)做這種檢查:如果你有一個(gè)多余的縮進(jìn),在運(yùn)行之前就能發(fā)現(xiàn)它。
- 錯(cuò)誤的名字,例如Math.sine(2). (應(yīng)該是 sin.)
- 參數(shù)的個(gè)數(shù)不對(duì),例如 Math.sin(30, 20).
- 參數(shù)的類型不對(duì) Math.sin("30").
- 錯(cuò)誤的返回類型 ,例如一個(gè)聲明返回int類型函數(shù)return "30";
動(dòng)態(tài)檢查 :
- 非法的變量值。例如整型變量x、y,表達(dá)式x/y 只有在運(yùn)行后y為0才會(huì)報(bào)錯(cuò),否則就是正確的。
- 無(wú)法表示的返回值。例如最后得到的返回值無(wú)法用聲明的類型來(lái)表示。
- 越界訪問(wèn)。例如在一個(gè)字符串中使用一個(gè)負(fù)數(shù)索引。
- 使用一個(gè)null對(duì)象解引用。(null相當(dāng)于Python中的None)
靜態(tài)檢查傾向于類型錯(cuò)誤 ,即與特定的值無(wú)關(guān)的錯(cuò)誤。正如上面提到過(guò)的,一個(gè)類型是一系列值的集合,而靜態(tài)類型就是保證變量的值在這個(gè)集合中,但是在運(yùn)行前我們可能不會(huì)知道這個(gè)值的結(jié)果到底是多少。所以如果一個(gè)錯(cuò)誤必須要特定的值來(lái)“觸發(fā)”(例如除零錯(cuò)誤和越界訪問(wèn)),編譯器是不會(huì)在編譯的時(shí)候報(bào)錯(cuò)的。
與此相對(duì)的,動(dòng)態(tài)類型檢查傾向于特定值才會(huì)觸發(fā)的錯(cuò)誤。
原始類型并不是真正的數(shù)字!
在Java和許多其他語(yǔ)言中存在一個(gè)“陷阱”——原始數(shù)據(jù)類型的對(duì)象在有些時(shí)候并不像真正的數(shù)字那樣得到應(yīng)有的輸出。結(jié)果就是本來(lái)應(yīng)該被動(dòng)態(tài)檢查發(fā)現(xiàn)的錯(cuò)誤沒(méi)有報(bào)錯(cuò)。例如:
- 整數(shù)的除法:5/2并不會(huì)返回一個(gè)小數(shù),而是一個(gè)去掉小數(shù)部分的整數(shù)對(duì)象,因?yàn)槌ú僮鞣麑?duì)兩個(gè)整數(shù)對(duì)象運(yùn)算后的結(jié)果還是整數(shù),而整數(shù)對(duì)象是無(wú)法表示5/2的精確值的(而我們期望它會(huì)是一個(gè)動(dòng)態(tài)檢查能發(fā)現(xiàn)的錯(cuò)誤)。
- 整形溢出: int 和 long類型的值的集合是一個(gè)有限集合——它們有最大的值和最小的值,當(dāng)運(yùn)算的結(jié)果過(guò)大或者過(guò)小的時(shí)候我們就很可能得到一個(gè)在合法范圍內(nèi)的錯(cuò)誤值。
- 浮點(diǎn)類型中的特殊值:在浮點(diǎn)類型例如double中有一些不是數(shù)的特殊值:NaN ( “Not a Number”), POSITIVE_INFINITY (正無(wú)窮), and NEGATIVE_INFINITY (負(fù)無(wú)窮).當(dāng)你對(duì)浮點(diǎn)數(shù)進(jìn)行運(yùn)算的時(shí)候可能就會(huì)得到這些特殊值(例如除零或者對(duì)一個(gè)負(fù)數(shù)開(kāi)更號(hào)),如果你拿著這些特殊值繼續(xù)做運(yùn)算,那你可能就會(huì)得到一個(gè)意想不到結(jié)果(譯者注:例如拿NaN和別的數(shù)進(jìn)行比較操作永遠(yuǎn)是False) 。
閱讀小練習(xí)
下面這些代碼都有各自的bug,請(qǐng)你判斷它們是靜態(tài)錯(cuò)誤還是動(dòng)態(tài)錯(cuò)誤還是無(wú)報(bào)錯(cuò),但是得到錯(cuò)誤的結(jié)果。
1
int n = 5; if (n) {n = n + 1; }[x] 靜態(tài)錯(cuò)誤
[ ] 動(dòng)態(tài)錯(cuò)誤
[ ] 無(wú)報(bào)錯(cuò),但是得到錯(cuò)誤的結(jié)果
2
int big = 200000; // 200,000 big = big * big; // big should be 40 billion now[ ] 靜態(tài)錯(cuò)誤
[ ] 動(dòng)態(tài)錯(cuò)誤
[x] 無(wú)報(bào)錯(cuò),但是得到錯(cuò)誤的結(jié)果
3
double probability = 1/5;[ ] 靜態(tài)錯(cuò)誤
[ ] 動(dòng)態(tài)錯(cuò)誤
[x] 無(wú)報(bào)錯(cuò),但是得到錯(cuò)誤的結(jié)果
4
int sum = 0; int n = 0; int average = sum/n;[ ] 靜態(tài)錯(cuò)誤
[x] 動(dòng)態(tài)錯(cuò)誤
[ ] 無(wú)報(bào)錯(cuò),但是得到錯(cuò)誤的結(jié)果
5
double sum = 7; double n = 0; double average = sum/n;[ ] 靜態(tài)錯(cuò)誤
[ ] 動(dòng)態(tài)錯(cuò)誤
[x] 無(wú)報(bào)錯(cuò),但是得到錯(cuò)誤的結(jié)果
Mr.Xu: 浮點(diǎn)數(shù)除零為Infinity
譯者注:下面的四小節(jié)講的是Java語(yǔ)言本身的一些性質(zhì),對(duì)Java很熟悉的同學(xué)可以跳過(guò)或略讀。
數(shù)組和聚集類型
現(xiàn)在讓我們把“冰雹序列”的結(jié)果存儲(chǔ)在數(shù)據(jù)結(jié)構(gòu)中而不僅僅是輸出。在Java中有兩種常用的線性存儲(chǔ)結(jié)構(gòu):數(shù)組和列表。
數(shù)組是一連串類型相同的元素組成的結(jié)構(gòu),而且它的長(zhǎng)度是固定的(元素個(gè)數(shù)固定)。例如,我們聲明一個(gè)int類型的數(shù)組:
int[] a = new int[100];對(duì)于數(shù)組,常用的操作符有下:
- 索引其中的一個(gè)元素: a[2]
- 賦予一個(gè)元素特定的值: a[2]=0
- 求這個(gè)數(shù)組的長(zhǎng)度: a.length (注意和 String.length() 的區(qū)別—— a.length 不是一個(gè)類內(nèi)方法調(diào)用,你不能在它后面寫上括號(hào)和參數(shù))
下面是我們利用數(shù)組寫的第一個(gè)求“冰雹序列”的代碼,它存在一些bug:
int[] a = new int[100]; // <==== DANGER WILL ROBINSON int i = 0; int n = 3; while (n != 1) {a[i] = n;i++; // very common shorthand for i=i+1if (n % 2 == 0) {n = n / 2;} else {n = 3 * n + 1;} } a[i] = n; i++;相信很快你就能發(fā)現(xiàn)錯(cuò)誤:幻數(shù)100?(譯者注:幻數(shù)是指那些硬編碼的數(shù)值)那如果n產(chǎn)生的“冰雹序列”非常長(zhǎng)呢?像這樣的bug稱為越界訪問(wèn),在Java中能夠被動(dòng)態(tài)檢查檢測(cè)出來(lái),但是在C和C++這樣的語(yǔ)言中則會(huì)造成 緩沖區(qū)溢出 (能通過(guò)編譯),這也是很多漏洞的來(lái)源。
解決方法是使用List類型。列表類型是一個(gè)長(zhǎng)度可變的序列結(jié)構(gòu)。我們可以這樣聲明列表:
List<Integer> list = new ArrayList<Integer>();常用的操作符有下:
- 索引一個(gè)元素: list.get(2)
- 賦予一個(gè)元素特定的值: list.set(2, 0)
- 求列表的長(zhǎng)度: list.size()
這里要注意List是一個(gè)接口,這種類型的對(duì)象無(wú)法直接用new來(lái)構(gòu)造,必須用能夠?qū)崿F(xiàn)List要求滿足的操作符的方法來(lái)構(gòu)造。我們會(huì)在后來(lái)講抽象數(shù)據(jù)類型的時(shí)候具體將價(jià)格這個(gè)。ArrayList是一個(gè)實(shí)類型的類(concrete type),它提供了List操作符的具體實(shí)現(xiàn)。當(dāng)然,ArrayList不是唯一的實(shí)現(xiàn)方法(還有LinkedList 等),但是是最常用的一個(gè))。你可以在Java API的文檔里找到很多這方面的信息(Google Java 8 API,這里的API指“應(yīng)用程序接口”,它會(huì)告訴你Java里面實(shí)現(xiàn)的很多有用的類和方法)。
另外要注意的是,我們要寫List<Integer> 而不是 List<int>.因?yàn)長(zhǎng)ist只會(huì)處理對(duì)象類型而不是原始類型。在Java中,每一個(gè)原始類型都有其對(duì)應(yīng)的對(duì)象類型(原始類型使用小寫字母名字,例如int,而對(duì)象類型的開(kāi)頭字母大寫,例如Integer)。當(dāng)我們使用尖括號(hào)參量化一個(gè)類型時(shí),Java要求我們使用對(duì)象類型而非原始類型。在其他的一些情況中,Java會(huì)自動(dòng)在原始類型和對(duì)等的對(duì)象類型之間相轉(zhuǎn)換。例如在上面的代碼中我們可以使用Integer i = 0 。
下面是用列表寫的“冰雹序列”的實(shí)現(xiàn):
List<Integer> list = new ArrayList<Integer>(); int n = 3; while (n != 1) {list.add(n);if (n % 2 == 0) {n = n / 2;} else {n = 3 * n + 1;} } list.add(n);這樣實(shí)現(xiàn)不僅看起來(lái)簡(jiǎn)潔,更重要的是安全,因?yàn)榱斜頃?huì)自動(dòng)擴(kuò)充它自己以滿足新添加的元素(當(dāng)然,直到你的內(nèi)存不夠用為止)
迭代
對(duì)于在一個(gè)序列結(jié)構(gòu)(例如列表和數(shù)組)遍歷元素,Java和Python的寫法差不多:
// find the maximum point of a hailstone sequence stored in list int max = 0; for (int x : list) {max = Math.max(x, max); }Math.max() 是一個(gè)Java API提供的方便的函數(shù)。你可以在Google中搜索“java 8 Math”獲得關(guān)于Math這個(gè)庫(kù)的一些詳細(xì)信息。
方法
在Java中,聲明通常必須在一個(gè)方法中,而每個(gè)方法都要在一個(gè)類型中,所以寫“冰雹序列”程序最簡(jiǎn)單可以這么寫:
public class Hailstone {/*** Compute a hailstone sequence.* @param n Starting number for sequence. Assumes n > 0.* @return hailstone sequence starting with n and ending with 1.*/public static List<Integer> hailstoneSequence(int n) {List<Integer> list = new ArrayList<Integer>();while (n != 1) {list.add(n);if (n % 2 == 0) {n = n / 2;} else {n = 3 * n + 1;}}list.add(n);return list;} }下面介紹一些新的東西。
public意味著任何在你程序中的代碼都可以訪問(wèn)這個(gè)類或者方法。其他的類型修飾符,例如private ,是用來(lái)確保程序的安全性的——它保證了可變類型不會(huì)被別處的代碼所修改。我們會(huì)在后面的課程中詳細(xì)提到。
static意味這這個(gè)方法沒(méi)有self這個(gè)參數(shù)——Java會(huì)隱含的實(shí)現(xiàn)它,所以你不會(huì)看到這個(gè)參數(shù)。靜態(tài)的方法不能通過(guò)對(duì)象來(lái)調(diào)用,例如List add() 方法 或者 String length()方法,它們要求先有一個(gè)對(duì)象。靜態(tài)方法的正確調(diào)用應(yīng)該使用類來(lái)索引,例如:
Hailstone.hailstoneSequence(83)另外,記得在定義的方法前面寫上注釋。這些注釋應(yīng)該描述了這個(gè)方法的功能,輸入輸出/返回,以及注意事項(xiàng)。記住注釋不要寫的啰嗦,而是應(yīng)該直切要點(diǎn),簡(jiǎn)潔明了。例如在上面的代碼中,n是一個(gè)整型的變量,這個(gè)在聲明的時(shí)候int已經(jīng)體現(xiàn)出來(lái)了,就不需要進(jìn)行注釋。但是如果我們?cè)O(shè)想的本意是n不能為負(fù)數(shù),而這個(gè)編譯器(聲明)是不能檢查和體現(xiàn)出來(lái)的,我們就應(yīng)該注釋出來(lái),方便閱讀理解和修改。
這些東西我們會(huì)在后面的課程中詳細(xì)介紹,但是你現(xiàn)在就要開(kāi)始試著正確使用他們。
變化的值 vs. 可被賦值的改變
在下一篇閱讀資料中我們會(huì)介紹“快照?qǐng)D”(snapshot diagrams),以此來(lái)辨別修改一個(gè)變量和修改一個(gè)值的區(qū)別。當(dāng)你給一個(gè)變量賦值的時(shí)候,你實(shí)際上是在改變這個(gè)變量指向的對(duì)象(值也不一樣)。
而當(dāng)你對(duì)一個(gè)可變的值進(jìn)行賦值操作的時(shí)候——例如數(shù)組或者列表——你實(shí)際上是在改變對(duì)象本身的內(nèi)容。
變化是“邪惡”的,好的程序員會(huì)避免可改變的東西,因?yàn)檫@些改變可能是意料之外的。
不變性(Immutability)是我們這門課程的一個(gè)重要設(shè)計(jì)原則。不變類型是指那些這種類型的對(duì)象一旦創(chuàng)建其內(nèi)容就不能被更改的類型(至少外部看起來(lái)是這樣,我們?cè)诤竺娴牡恼n程中會(huì)說(shuō)一些替代方案)。思考一下在上面的代碼中哪一些類型是可更改類型,哪一些不是?(譯者注:例如int就是不變的,List就是可變的,給int類型的對(duì)象賦值就會(huì)讓它指向一個(gè)新的對(duì)象)
Java也給我們提供了不變的索引:只要變量被初始化后就不能再次被賦值了——只要在聲明的時(shí)候加上final :
final int n = 5;如果編譯器發(fā)現(xiàn)你的final變量不僅僅是在初始化的時(shí)候被“賦值”,那么它就會(huì)報(bào)錯(cuò)。換句話說(shuō),final會(huì)提供不變索引的靜態(tài)檢查。
正確的使用final是一個(gè)好習(xí)慣,就好像類型聲明一樣,這不僅會(huì)讓編譯器幫助你做靜態(tài)檢查,同時(shí)別人讀起來(lái)也會(huì)更順利一些。
在hailstoneSequence方法中有兩個(gè)變量n和list,我們可以將它們聲明為final嗎?請(qǐng)說(shuō)明理由。(譯者注:n不行,list可以。因?yàn)槲覀冃枰淖僴指向的對(duì)象,而List對(duì)象本身是可以更改的,我們也不需要改變list對(duì)應(yīng)的對(duì)象)
public static List<Integer> hailstoneSequence(final int n) { final List<Integer> list = new ArrayList<Integer>();記錄你的設(shè)想
在文檔中寫下變量的類型記錄了一個(gè)關(guān)于它的設(shè)想, 例如這個(gè)變量總是指向一個(gè)整型. 在編譯的時(shí)候 Java 就會(huì)檢查這個(gè)設(shè)想, 并且保證在你的代碼中沒(méi)有任何一處違背這個(gè)設(shè)想。
而使用 final 關(guān)鍵字去定義一個(gè)變量也是一種記錄設(shè)想, 要求這個(gè)變量在其被賦值之后就永遠(yuǎn)不會(huì)再被修改, Java 也會(huì)對(duì)其進(jìn)行靜態(tài)地檢查。
不幸的是 Java 并不會(huì)自動(dòng)檢查所有設(shè)想,例如:n 必須為正數(shù)。
為什么我們需要寫下我們的設(shè)想呢? 因?yàn)榫幊叹褪遣粩嗟脑O(shè)想, 如果我們不寫下他們, 就可能會(huì)遺忘掉他們, 而且如果以后別人想要閱讀或者修改我們的軟件, 他們就會(huì)很難理解代碼, 不得不去猜測(cè)(譯者注: 變量的含義/函數(shù)的描述/返回值描述等等)
所以在編程的時(shí)候我們必須朝著如下兩個(gè)目標(biāo)努力:
- 與計(jì)算機(jī)交流. 首先說(shuō)服編譯器你的程序語(yǔ)法正確并且類型正確, 然后保證邏輯正確, 這樣就可以讓它在運(yùn)行的時(shí)候給我們正確的結(jié)果。
- 與其他人交流. 盡可能使你的程序易于理解, 所以當(dāng)有人想要在將來(lái)某些時(shí)候修正它, 改進(jìn)它或者對(duì)其進(jìn)行適配的時(shí)候, 他們可以很方便地實(shí)現(xiàn)自己的想法。
黑客派(Hacking) vs. 工程派(Engineering)
我們已經(jīng)在本門課程中編寫了一些黑客風(fēng)格的代碼, 黑客派的編程風(fēng)格可以理解為“放飛自我并且樂(lè)觀的”(譯者注:貶義):
- 缺點(diǎn): 在已經(jīng)編寫大量代碼以后才測(cè)試它們
- 缺點(diǎn): 將所有的細(xì)節(jié)都放在腦子里, 以為自己可以永遠(yuǎn)記住所有的代碼, 而不是將它們編寫在代碼中
- 缺點(diǎn): 認(rèn)為 BUG 都不存在或者它們都非常容易發(fā)現(xiàn)和被修復(fù).
而工程派對(duì)應(yīng)的做法是(譯者注:褒義):
- 優(yōu)點(diǎn): 一次只寫一點(diǎn)點(diǎn), 一邊寫一邊測(cè)試. 在將來(lái)的課程中, 我們將會(huì)探討"測(cè)試優(yōu)先編程" (test-first programming)
- 優(yōu)點(diǎn): 記錄代碼的設(shè)想、意圖 (document the assumptions that your code depends on)
- 優(yōu)點(diǎn): 靜態(tài)代碼檢查將會(huì)保護(hù)你的代碼不淪為“愚蠢的代碼”
閱讀小練習(xí)
思考下面這個(gè)Python片段:
from math import sqrt def funFactAbout(person):if sqrt(person.age) == int(sqrt(person.age)):print("The age of " + person.name + " is a perfect square: " + str(person.age))這塊代碼做了哪些設(shè)想(能夠正常運(yùn)行不報(bào)錯(cuò))?
[x] person 一定有 age 和 name 這兩個(gè)實(shí)例化變量
[x] person 不是 None
[x] person.age 不是負(fù)數(shù)
[x] person.age 是整數(shù)
[x] person.name 一定是字符串
如果這串代碼用Java來(lái)寫,下面的哪些設(shè)想可以用類型聲明來(lái)限制,或者能夠被編譯器靜態(tài)檢查出來(lái)?
[x] person 一定有 age 和 name 這兩個(gè)實(shí)例化變量
[ ] person 不是 None
[ ] person.age 不是負(fù)數(shù)
[x] person.age 是整數(shù)
[x] person.name 一定是字符串
本門課程(6.031)的目標(biāo)
本門課程的主要目標(biāo)為學(xué)習(xí)如何生產(chǎn)具有如下屬性的軟件:
- 遠(yuǎn)離bug. 正確性 (現(xiàn)在看起來(lái)是正確的), 防御性 (將來(lái)也是正確的)
- 易讀性. 我們不得不和以后有可能需要理解和修改代碼的程序員進(jìn)行交流 (修改 BUG 或者添加新的功能), 那個(gè)將來(lái)的程序員或許會(huì)是幾個(gè)月或者幾年以后的你, 如果你不進(jìn)行交流, 那么到了那個(gè)時(shí)候, 你將會(huì)驚訝于你居然忘記了這么多, 并且這將會(huì)極大地幫助未來(lái)的你有一個(gè)良好的設(shè)計(jì)。
- 可改動(dòng)性. 軟件總是在更新迭代的, 一些好的設(shè)計(jì)可以讓這個(gè)過(guò)程變得非常容易, 但是也有一些設(shè)計(jì)將會(huì)需要讓開(kāi)發(fā)者扔掉或者重構(gòu)大量的代碼。
譯者注:
- safe from bugs (SFB),
- easy to understand (ETU),
- ready for change (RFC).
當(dāng)然也有其他的非常重要的關(guān)于軟件的性質(zhì)(例如: 性能, 實(shí)用性以及安全性), 并且他們可能會(huì)和上述三種性質(zhì)是互相矛盾的。但是在 6.031 這門課程中, 我們重點(diǎn)關(guān)注上述三個(gè)重要性質(zhì), 并且軟件開(kāi)發(fā)者一般情況下會(huì)將這三種性質(zhì)列于軟件開(kāi)發(fā)過(guò)程中的最重要的性質(zhì)。
在這門課的學(xué)習(xí)中, 思考每一種編程語(yǔ)言的特性, 每一次編程練習(xí), 每一個(gè)設(shè)計(jì)模式是非常值得的, 試著理解它們?nèi)绾魏蜕鲜鋈N特性關(guān)聯(lián)起來(lái)。
閱讀小練習(xí)
上文中那些知識(shí)點(diǎn)能夠幫助你遠(yuǎn)離bug SFB ?
[x] 動(dòng)態(tài)檢查
[x] 常量(final)
[ ] 整型溢出
[x] 靜態(tài)類型
上文中那些知識(shí)點(diǎn)能夠幫助你的代碼易于理解和閱讀 ETU ?
[x] documented assumptions in comments
[x] 常量(final)
[ ] 整型溢出
[ ] 靜態(tài)類型
上文中那些知識(shí)點(diǎn)能夠幫助你的代碼能夠被方便的更改 RFC ?
[x] documented assumptions in comments
[ ] 長(zhǎng)度固定的數(shù)組
[x] 方法(methods)
我們?yōu)槭裁催x擇Java作為本課程的編程語(yǔ)言
如果你已經(jīng)學(xué)過(guò)了課程 6.009, 我們假設(shè)你已經(jīng)對(duì) Python 語(yǔ)言輕車熟路了, 那么為什么在這門課程中, 我們沒(méi)有使用 Python 語(yǔ)言而是使用了 Java 呢?
安全性是首要原因, Java 有靜態(tài)檢查機(jī)制 (主要檢查變量類型, 同時(shí)也會(huì)檢查函數(shù)的返回值和函數(shù)定義時(shí)的返回值類型是否匹配). 我們?cè)谶@門課中學(xué)習(xí)軟件開(kāi)發(fā), 而在軟件開(kāi)發(fā)中一個(gè)主要原則就是遠(yuǎn)離 BUG, Java 撥號(hào)安全性達(dá)到了 11 (Java dials safety up to 11), 這讓 Java 成為一個(gè)非常好的用來(lái)進(jìn)行軟件工程實(shí)踐的語(yǔ)言. 當(dāng)然, 在其他的動(dòng)態(tài)語(yǔ)言中也可以寫出安全的代碼, 例如 Python, 但是如果你學(xué)習(xí)過(guò)如何在一個(gè)安全的, 具有靜態(tài)代碼檢查機(jī)制的語(yǔ)言, 你就可以更加容易地理解這一過(guò)程。
普遍性是另一個(gè)原因, Java 在科學(xué)研究/教育/工業(yè)界廣泛被使用. Java 可以在許多平臺(tái)運(yùn)行, 不僅僅是 Windows/Mac/Linux. Java 也可以用來(lái)進(jìn)行 web 開(kāi)發(fā) (不僅可以在服務(wù)端使用, 也可以在客戶端使用), 而且原生安卓系統(tǒng)也是基于 Java 開(kāi)發(fā). 盡管其他的編程語(yǔ)言也有更加適合用來(lái)進(jìn)行編程教學(xué) (Scheme 和 ML 浮現(xiàn)在腦海中), 但是令人是讓的是這些語(yǔ)言并沒(méi)有在現(xiàn)實(shí)世界中被廣泛使用. 你的簡(jiǎn)歷上的 Java 經(jīng)驗(yàn)將會(huì)被認(rèn)為是一個(gè)非常有利的技能. 但是注意請(qǐng)不要理解錯(cuò)了, 你從本門課程中學(xué)到的并不是僅限定于 Java 語(yǔ)言, 這些知識(shí)是可以被套用在任何編程語(yǔ)言中的. 本門課程中最重要內(nèi)容: 安全性, 清晰性, 抽象, 工程化的本能, 這些知識(shí)將會(huì)讓你游刃有余地應(yīng)對(duì)各種編程語(yǔ)言的新潮流。
任何情況下, 一個(gè)好的程序員必須能熟練使用多種編程語(yǔ)言, 編程語(yǔ)言是一種工具, 而你必須使用正確的工具來(lái)完成你的工作. 在你完成你在 MIT 的學(xué)習(xí)生涯之前你肯定會(huì)學(xué)到其他的編程語(yǔ)言技能 (例如: JavaScript, C/C++, Scheme, Ruby, ML 或者 Haskell) 所以我們正在通過(guò)學(xué)習(xí)第二門語(yǔ)言來(lái)入門。
作為普遍性的一個(gè)結(jié)果, Java 有大量的有趣而且有用的庫(kù) (包括 Java 本身自帶的庫(kù), 以及在網(wǎng)絡(luò)上的庫(kù)), 也有非常多的免費(fèi)并且完美的工具 (IDE 例如 Eclipse; 編輯器, 編譯器, 測(cè)試框架, 性能分析工具, 代碼覆蓋率檢測(cè)工具, 代碼風(fēng)格檢查工具). 即使是 Python , 它的生態(tài)系統(tǒng)也沒(méi)有 Java 的更加豐富。
后悔使用 Java 的幾個(gè)原因.
- 它很啰嗦, 這個(gè)特性使得在黑板上寫出代碼樣例是非常麻煩的.
- 它很臃腫, 在這些年中已經(jīng)積累了非常多的不同的功能和特性.
- 它存在一些內(nèi)部矛盾, 例如: final 關(guān)鍵字在不同的上下文中會(huì)有不同的含義, static 關(guān)鍵字在 Java 中和靜態(tài)代碼檢查并沒(méi)有任何關(guān)系
- 它受到C / C ++等老式語(yǔ)言的影響, 原始數(shù)據(jù)類型和 switch 語(yǔ)句就是很好的例子
- 它并沒(méi)有一個(gè)像 Python 一樣的解釋器, 可以允許你在解釋器中編寫一些短小的測(cè)試代碼來(lái)學(xué)習(xí)這門語(yǔ)言
但是總體來(lái)說(shuō), Java 對(duì)現(xiàn)在來(lái)說(shuō)還是一款比較適合用來(lái)學(xué)習(xí)如何編寫安全的, 易于理解的, 對(duì)改變友好的代碼的編程語(yǔ)言, 以上就是我們課程的目標(biāo);)
總結(jié)
我們今天主要介紹的思想為靜態(tài)代碼檢查, 下面是該思想如何和我們的目標(biāo)進(jìn)行關(guān)聯(lián)
- 遠(yuǎn)離bug. 靜態(tài)代碼檢查可以通過(guò)捕捉類型錯(cuò)誤等其他BUG幫助我們?cè)谶\(yùn)行代碼之前就發(fā)現(xiàn)它們
- 易讀性. 它可以幫助我們理解, 因?yàn)樗械念愋驮诖a中被明確定義 (譯者注: 相比于 Python/PHP 這類動(dòng)態(tài)變量類型的語(yǔ)言)
- 可改動(dòng)性. 靜態(tài)代碼檢查可以在你在修改你的代碼的時(shí)候定位出也需要被修改的地方, 例如: 當(dāng)你改變一個(gè)變量的類型或者名稱的時(shí)候, 編譯器立即就會(huì)在所有使用到這個(gè)變量的地方顯示錯(cuò)誤, 提示你也需要更新它們。
轉(zhuǎn)載于:https://www.cnblogs.com/liqiuhao/p/8486545.html
總結(jié)
以上是生活随笔為你收集整理的麻省理工18年春软件构造课程阅读01“静态检查”的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 职业中专计算机基础试讲课,职业中专计算机
- 下一篇: 紫书 例题8-10 UVa 714 (二