Mit_6301_2021_中文個(gè)人翻譯_第一課_Static Checking
# 第一章:Static Checking
**課程目標(biāo)**
本節(jié)課有兩個(gè)目標(biāo):
* 靜態(tài)類(lèi)型
* 優(yōu)秀軟件的三大特點(diǎn)
## 冰雹序列
冰雹序列從一個(gè)自然數(shù) n 開(kāi)始,如果 n 是偶數(shù),那么它的下一個(gè)數(shù)為 n/2,如果 n 是奇數(shù),那么下一個(gè)數(shù)為 3n + 1;冰雹序列會(huì)不停迭代直到這個(gè)數(shù)變?yōu)?1 為止。
以下是一些冰雹序列的例子。
``````
2, 1
3, 10, 5, 16, 8, 4, 2, 1
4, 2, 1
2^n, 2^(n-1) , … , 4, 2, 1
5, 16, 8, 4, 2, 1
7, 22, 11, 34, 17, 52, 26, 13, 40, …? (尚未結(jié)束)
``````
由于存在奇數(shù)的 3n + 1 法則,冰雹序列在最終停止之前會(huì)反復(fù)的上升下降。但是根據(jù)推測(cè),任意的自然數(shù) n 起始的冰雹序列,最終都會(huì)達(dá)到 1 而停止,但這仍只是個(gè)猜想,沒(méi)有嚴(yán)格的證明。另外一提,冰雹序列之所以叫做冰雹序列,是因?yàn)楸⒃谠茖又蟹e攢重量時(shí),會(huì)在云層中上下跳動(dòng),直到它積攢了足夠的重量能夠落到地上去。
## 如何計(jì)算冰雹序列
這里給出一些計(jì)算冰雹序列的 Java 代碼和 Python 代碼,大家可以比較以下它們的區(qū)別
```java
// 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
# Python
n = 3
while n != 1:
? ? print(n)
? ? if n % 2 == 0:
? ? ? ? n = n / 2
? ? else:
? ? ? ? n = 3 * n + 1
? ? ? ?
print(n)
```
這里有一些值得注意的點(diǎn):
* Java 和 Python 的基礎(chǔ)語(yǔ)法是十分相似的:
? 比如 ``while`` 和 ``if`` 的使用是是非相似的
* Java 在每行的某位需要使用分號(hào)作為結(jié)尾
* Java 在使用 ``if`` 和 ``while`` 做判斷的時(shí)候,需要把條件用括號(hào)括起來(lái)
* Java 使用中括號(hào)而不是縮進(jìn)來(lái)標(biāo)識(shí)語(yǔ)句塊。Java 不關(guān)注你是否使用空格來(lái)區(qū)分語(yǔ)句直接的關(guān)系,但你還是應(yīng)當(dāng)使用縮進(jìn)來(lái)表明語(yǔ)句塊之間的關(guān)系。因?yàn)榫幊桃彩且环N交流,你不止需要面對(duì)編譯器,還需要面對(duì)閱讀你代碼的普通人。
? 而人,是需要這些縮進(jìn)的,我們以后還會(huì)提到這點(diǎn)。
## 類(lèi)型
Java 和 Python 在語(yǔ)法上的最重要的區(qū)別在于變量 ``n`` 的聲明,在 Java 中我們聲明了它的類(lèi)型 ``int``
**類(lèi)型** 涵蓋了從屬這些類(lèi)型的值和定義在這些值上的運(yùn)算
Java 有幾種基本數(shù)據(jù)類(lèi)型,以下是幾個(gè)例子:
* ``int`` (類(lèi)似于 5 或者 -200 的整數(shù),但是它有其上下限 ±2^31,或者說(shuō) ±20億)
* ``long`` (用于更大的整數(shù),±2^63)
* ``boolean`` (只有兩個(gè)值,``true`` 和 ``false``)
* ``double`` (用于浮點(diǎn)數(shù))
* ``char`` (用于單個(gè)字母,比如 ``A`` 和 ``$`` )
Java 還有引用類(lèi)型,比如:
* ``String`` 表示 ``char`` 的序列,就像 ``python`` 的 字符串 一樣
* ``BigInteger`` 表示任意大小的整數(shù),就像 ``python`` 中的整數(shù)一樣
在 Java 中,我們習(xí)慣上用小寫(xiě)來(lái)表示基本數(shù)據(jù)類(lèi)型,用大寫(xiě)來(lái)表示引用類(lèi)型
定義在這些類(lèi)型上的運(yùn)算,實(shí)際上是一些接受輸入,產(chǎn)生輸出的函數(shù)(有時(shí)會(huì)改變傳入的值),運(yùn)算使用時(shí)的語(yǔ)法千差萬(wàn)別,但是我們?nèi)耘f視它們?yōu)楹瘮?shù)。以下是一些 Python 或者 Java 中運(yùn)算的使用示例:
* 運(yùn)算符:``a + b`` 會(huì)調(diào)用操作 ``+ : int x int -> int`` 。(``+`` 是這個(gè)運(yùn)算的名字,``int x int`` 描述了兩個(gè)輸入,箭頭的右邊的 ``int`` 描述了輸出。)
* 實(shí)例方法:``bigint1.add(bigint2)`` 調(diào)用運(yùn)算:``add: BigInteger x BigInteger -> BigInteger`` 。
* 類(lèi)方法:``Math.sin(theta)`` 調(diào)用運(yùn)算 ``sin: double -> double`` 這里 ``Math`` 不是一個(gè)對(duì)象,而是一個(gè)類(lèi),這個(gè)類(lèi)中包含了 ``sin`` 函數(shù)。
我們對(duì)比一下 Java 中的 ``str.length()`` 和 Python 中的 ``len(str)`` 。它們的功能都是輸入一個(gè)字符串,返回字符串的長(zhǎng)度,只在語(yǔ)法上有一點(diǎn)區(qū)別。
有一些操作符是重載過(guò)的,這使得它們可以在不同的數(shù)據(jù)類(lèi)型中使用而不需要改變形式。Java 中用于基本數(shù)據(jù)類(lèi)型計(jì)算的操作符都是經(jīng)過(guò)重載的,比如``+, -, *, /`` 。Java 中方法也是可以被重載的,大多數(shù)的編程語(yǔ)言都可以實(shí)現(xiàn)不同程度的重載。
## 靜態(tài)類(lèi)型
Java 是 **靜態(tài)類(lèi)型語(yǔ)言**:所有變量的類(lèi)型在編譯期間(程序運(yùn)行前)就已經(jīng)確定,因此編譯器也可以通過(guò)變量的類(lèi)型來(lái)確定變量表達(dá)式結(jié)果的類(lèi)型。如果 ``a`` 和 ``b`` 被聲明為 ``int`` , 編譯器就會(huì)認(rèn)為 ``a + b`` 也是 ``int`` 類(lèi)型。Eclipse 在你還在編寫(xiě)程序的同時(shí)就會(huì)進(jìn)行類(lèi)型檢查,所以你能夠在程序編寫(xiě)階段就發(fā)現(xiàn)很多錯(cuò)誤。
在 **動(dòng)態(tài)類(lèi)型語(yǔ)言** 中,這種類(lèi)型檢查會(huì)被推遲倒程序運(yùn)行時(shí)執(zhí)行,Python 就是動(dòng)態(tài)類(lèi)型語(yǔ)言。
**靜態(tài)檢查**意味著在編譯時(shí)期就檢查bugs。Bugs 是編程的萬(wàn)惡之首。本課程的很多部分都在試圖教會(huì)你如何在代碼中消除 bug,而靜態(tài)檢查就是你最先看到的部分。靜態(tài)類(lèi)型可以減少你程序中的一大類(lèi)錯(cuò)誤:準(zhǔn)確的說(shuō),就是數(shù)據(jù)類(lèi)型的不匹配。如果你的代碼像下面一樣:
```java
"5" * "6"
```
這行代碼嘗試將兩個(gè)字符串相乘,靜態(tài)類(lèi)型語(yǔ)言就會(huì)發(fā)現(xiàn)這個(gè)錯(cuò)誤,并且在你還在編寫(xiě)程序的時(shí)候就提出來(lái)。
## 在動(dòng)態(tài)類(lèi)型語(yǔ)言中支持靜態(tài)類(lèi)型
盡管 Python 是一個(gè)動(dòng)態(tài)類(lèi)型語(yǔ)言,Python 3.5 以及更新的版本已經(jīng)支持使用 [type hints](https://peps.python.org/pep-0484/) 來(lái)在代碼中支持靜態(tài)類(lèi)型,代碼示例:
```python
# Python function declared with type hints
def hello(name:str)->str:
? ? return 'Hi, ' + name
```
可以使用像 [Mypy](https://mypy-lang.org/) 檢查器來(lái)在你還沒(méi)有運(yùn)行程序之前來(lái)檢查類(lèi)型錯(cuò)誤
在動(dòng)態(tài)類(lèi)型語(yǔ)言中添加靜態(tài)類(lèi)型反應(yīng)了軟件工程領(lǐng)域一個(gè)非常普遍的認(rèn)知:靜態(tài)類(lèi)型對(duì)于開(kāi)發(fā)和維持一個(gè)大型軟件系統(tǒng)是非常重要的。事實(shí)上本書(shū)的剩余部分或者說(shuō)整個(gè)課程都在致力于說(shuō)明這一認(rèn)知的理由。
與 Java 這種從誕生之初就是靜態(tài)類(lèi)型的語(yǔ)言不同,在動(dòng)態(tài)類(lèi)型語(yǔ)言中添加靜態(tài)類(lèi)型可以實(shí)現(xiàn)一種新的編程類(lèi)型:漸進(jìn)類(lèi)型。也就是代碼中的一部分是靜態(tài)類(lèi)型聲明,一部分是動(dòng)態(tài)類(lèi)型聲明。漸進(jìn)類(lèi)型能夠讓你從一個(gè)小的不完整的原型系統(tǒng)開(kāi)始,開(kāi)發(fā)為一個(gè)大型的穩(wěn)固的可維護(hù)的系統(tǒng)的過(guò)程中提供更加順暢的體驗(yàn)。
## 靜態(tài)檢查、動(dòng)態(tài)檢查、不檢查
讓我們來(lái)仔細(xì)思考一下以下三種編程語(yǔ)言能夠提供給我們的自動(dòng)檢查:
* **靜態(tài)檢查** :在程序還沒(méi)有運(yùn)行之前就能夠發(fā)現(xiàn) bug
* **動(dòng)態(tài)檢查** :在程序執(zhí)行時(shí)才發(fā)現(xiàn) bug
* **不檢查** :該編程語(yǔ)言完全不檢查任何 bug,你必須自己找到 bug,或者接受一個(gè)錯(cuò)誤的答案
無(wú)需多言,靜態(tài)檢查優(yōu)于動(dòng)態(tài)檢查,動(dòng)態(tài)檢查又優(yōu)于不檢查
以下是一些經(jīng)驗(yàn)之談來(lái)表明你能在不同的檢查類(lèi)型下能夠發(fā)現(xiàn)哪些 bug:
**靜態(tài)檢查**能夠發(fā)現(xiàn):
* 語(yǔ)法錯(cuò)誤,比如額外的標(biāo)點(diǎn)符號(hào)或者拼寫(xiě)錯(cuò)誤。即使是動(dòng)態(tài)類(lèi)型的語(yǔ)言也會(huì)檢查這種類(lèi)型的錯(cuò)誤,比如 Python 在你運(yùn)行程序之前就檢查縮進(jìn)錯(cuò)誤。
* 拼寫(xiě)錯(cuò)誤比如``Math.sine(2)`` (正確的拼寫(xiě)是 ``sin`` )
* 錯(cuò)誤的參數(shù):``Math.sin(30, 20)``?
* 錯(cuò)誤的參數(shù)類(lèi)型:``Math.sin("30")``?
* 錯(cuò)誤的返回類(lèi)型:函數(shù)聲明的返回類(lèi)型是 ``int`` ,而實(shí)際返回的卻是 ``return "30"``?
**動(dòng)態(tài)檢查**能夠發(fā)現(xiàn):
* 非法參數(shù)值:對(duì)于整數(shù)相除的表達(dá)式 ``x / y`` 只有當(dāng) ``y`` 為 0 的時(shí)候,才會(huì)出現(xiàn)除零錯(cuò)誤,當(dāng) ``y`` 不為 0 時(shí),這個(gè)表達(dá)式的值是正確且確定的。所以在這個(gè)例子中,除零錯(cuò)誤是一個(gè)動(dòng)態(tài)錯(cuò)誤,而不是一個(gè)靜態(tài)錯(cuò)誤。
* 不合法的類(lèi)型轉(zhuǎn)換:有些特定的值是不能轉(zhuǎn)換為其他數(shù)據(jù)類(lèi)型的。比如:```Integer.valueOf("hello")`` 是一個(gè)動(dòng)態(tài)錯(cuò)誤,因?yàn)?``hello`` 不能被轉(zhuǎn)換為一個(gè)十進(jìn)制整數(shù)。``Integer.valueOf("8000000000")`` 也是一個(gè)動(dòng)態(tài)錯(cuò)誤,因?yàn)?80 億超出了 ``int`` 類(lèi)型能夠接收的范圍。
* 超出索引范圍,例如在字符串索引時(shí)使用負(fù)數(shù)或者過(guò)大的索引
* 調(diào)用實(shí)例方法時(shí),對(duì)象引用為 ``null`` ,(``null`` 和 Python 中的 ``None`` 類(lèi)似)
靜態(tài)檢查可以檢測(cè)跟變量類(lèi)型有關(guān)的錯(cuò)誤,但是通常不能檢測(cè)這個(gè)變量類(lèi)型特定值相關(guān)的錯(cuò)誤。靜態(tài)檢查保證了變量具有其變量類(lèi)型的值,但是具體的值只有等到程序運(yùn)行期間才得以知曉。所以如果某個(gè)錯(cuò)誤只跟這個(gè)類(lèi)型的特定值有關(guān),比如除零錯(cuò)誤,那么編譯器就不會(huì)找到靜態(tài)錯(cuò)誤。
動(dòng)態(tài)檢查傾向于找到那些具體值所引發(fā)的錯(cuò)誤。
## 你或許不知道的:基本數(shù)據(jù)類(lèi)型并不是真正的數(shù)學(xué)數(shù)字
Java 和 其他一些編程語(yǔ)言中有一個(gè)經(jīng)典的陷阱:它們的基本數(shù)據(jù)數(shù)字類(lèi)型在極端情況下并不像我們?cè)谌粘?shù)學(xué)中使用的數(shù)字一樣。這會(huì)導(dǎo)致一些應(yīng)該被動(dòng)態(tài)檢查的錯(cuò)誤,沒(méi)有被檢測(cè)出。
* 整數(shù)除法:``5 / 2`` 并不會(huì)返回一個(gè)小數(shù),它會(huì)返回一個(gè)被截?cái)嗟恼麛?shù)。這常常會(huì)導(dǎo)致我們的程序產(chǎn)生錯(cuò)誤的答案,并且程序還不會(huì)檢測(cè)出動(dòng)態(tài)錯(cuò)誤。
* 整數(shù)溢出:``int`` 和 ``long`` 實(shí)際上是有限的整數(shù)集,具有上限和下限。當(dāng)你向這兩種數(shù)據(jù)類(lèi)型中填充一個(gè)過(guò)大的正整數(shù)或者過(guò)小的負(fù)整數(shù)時(shí)會(huì)發(fā)生什么呢?答案是產(chǎn)生溢出,并且是悄悄的溢出,它會(huì)返回一個(gè)類(lèi)型范圍內(nèi)的值,但不是正確的值。
? ```java
? int a = 2147483647;
? a = fun(a);
? public static int fun(int a) {
? ? ? return a + 1;
? }
? ```
* 浮點(diǎn)類(lèi)型中的特殊值:浮點(diǎn)類(lèi)型像 ```double``` 擁有幾種特殊值。```NaN``` (Not a Number),```POSITIVE_INFINITY``` , ```NEGATIVE_INFINITY``` 。當(dāng)你在```double``` 類(lèi)型上做一些操作時(shí),比如除零或者對(duì)負(fù)數(shù)開(kāi)根號(hào),你將不會(huì)得到一個(gè)動(dòng)態(tài)錯(cuò)誤,而是獲得以上的幾種特殊值。如果你把這些值當(dāng)作是中間值進(jìn)行計(jì)算,你最終可能會(huì)得到一個(gè)奇怪的答案。
## 練習(xí)
接下來(lái)我們來(lái)做一些練習(xí)來(lái)鞏固知識(shí),以下這些有 bug 的代碼,它們的錯(cuò)誤會(huì)被靜態(tài)檢查捕獲還是動(dòng)態(tài)檢查還是捕獲不到呢?
```java
int n = -5;
if (n) {
? System.out.println("n is a positive integer");
}
```
```java
int big = 200000; // 200,000
big = big * big;? // big should be 40 billion now
```
```java
double probability = 1/5;
```
```java
int sum = 0;
int n = 0;
int average = sum/n;
```
```java
double sum = 7;
double n = 0;
double average = sum/n;
```