靜態語言 / 動態語言、強型別 / 弱型別、静態作用域 / 動態作用域

前言

討論程式語言的型別時,通常「靜態、動態語言」和「強、弱型別」會放在一起討論。而「静態、動態作用域」則是屬於另一類型。

這邊放在一起說明,讓大家理解是不一樣的概念。

靜態語言 / 動態語言 (Static vs. Dynamic Typing)

語言的型態系統是對底層位元組 (byte) 的抽象化,因為人類是很難直接理解一連串的 01000011111 到底想表達什麼意思。例如:告訴開發者「這些位元組是個字串」,或是當開發者想將字串轉換為大寫時,可以直接下達「把字串轉為大寫」的命令,而不是「對這些位元組進行某些運算」。

只要瞭解型態,就可以知道如何以高階型態來操作和處理資料,不需要去處理底層細節 (01000011111)。因此,開發者選擇程式語言的第一步,也就是決定選用靜態語言或動態語言。

如何從語法得知型態資訊?變數帶有資料型態是靜態語言,且使用前一定要先宣告;反之則是動態語言。 例:

C# 中一旦宣告 st 變數為 string 型態,就不能再賦予 st 其他的型態。C# 是偏向於靜態語言

1
2
string st = "Jenifer";
st = 50; // 編譯錯誤

然而在 JavaScript 中,st 這個變數不具備固定的資料型態,因此可以斷定 JavaScript 是偏向於動態語言

1
2
let st = "Jenifer";
st = 50; // 正常運行

然而語言是靜態還是動態並不是那麼的絕對,它是連續性的光譜。有這個例子:Scala 具有型態推論(Type inference)特性。

靜態語言:

  • 優點是可避免執行時期型態錯誤、提供重構輔助與更多的文件形式。
  • 缺點是程式語法繁瑣、彈性不足,只能檢查出執行時期前的簡單錯誤。

動態語言:

  • 優點是語法簡潔、具有較高的彈性。
  • 缺點是型態錯誤在執行時期才會呈現出來,效能表現較不理想,編輯輔助工具較為缺乏,依賴慣例或實體文件來得知API使用方式。

參考資料:
程式語言的特性本質(一)靜態語言與動態語言的信任抉擇

強型別 / 弱型別 (Strong vs. Weak Typing)

語言容忍隱性 (implicit) 型別轉換的「程度」。也就是當開發者未告知「進行型別轉換」時,編譯器或直譯器會自動轉換還是不會。一樣是連續性的光譜

強型別的語言,偏向不容忍隱性的型別轉換。 C# 中,執行如下的程式碼,會得到一個編譯錯誤:

1
2
int x = 123 + "456";
Console.WriteLine(x);
1
Error CS0029: Cannot implicitly convert type 'string' to 'int' (CS0029)

如果想要將字串轉換成數字,必須這樣寫:

1
2
int x = 123 + Int32.Parse("456"); // 明白告訴編譯器先將字串轉換成數字
Console.WriteLine(x); // 579

或是數字轉換成字串:

1
2
string x = Convert.ToString(123) + "456"; // 先將數字轉換成字串
Console.WriteLine(x); // 123456

弱型別的語言,偏向容忍隱性的型別轉換,但是怎麼轉換是它自己決定。 在 JavaScript 中,直譯器自動將以下程式碼中的數字轉換成字串,再串接兩個字串。

1
2
let x = 123 + "456"; // 直譯器自動將數字轉換成字串,再串接兩個字串
console.log(x); // 123456

但是如果發生像下面這樣的錯誤,直譯器並不會產生錯誤,只能等待開發者自行察覺:

1
2
let x = 123 + "Hello";
console.log(x); // 123Hello

弱型別還允許不同型別之間的比對,因為它們可以被隱性型別轉換。== 比對兩者的=== 比對兩者的型別和值

1
2
3
4
let y = ("123" == 123) ? true : false;
let z = ("123" === 123) ? true : false;
console.log(y); // true
console.log(z); // false

以下的圖,可以看出不同語言的所屬類型:

參考資料:
你不可不知的 JavaScript 二三事#Day3:資料型態的夢魘——動態型別加弱型別(2)
Dynamic Typing is NOT Weak Typing

静態作用域 / 動態作用域 (Static vs. Dynamic Scoping)

作用域的類別則是在探討有相同的變數名稱時,該變數需要參照哪一個值,也就是變數和值之間的聯繫 (binding)。

靜態作用域又叫做詞法作用域 (lexical scoping),採用詞法作用域的變數叫詞法變數。編譯器或直譯器在讀取詞法變數的值時,會檢查包住該變數的函式被定義時的文字環境。也就是函式寫好時,作用域就被定義好了,開發者可以從程式碼中直接觀察到變數的作用域;不需要考慮「該函式是否被其他函式呼叫?」、「該變數是否需要參照其他函式中的同名變數?」等這類和 call stack 有關的問題。

In this scoping a variable always refers to its top level environment. This is a property of the program text and unrelated to the run time call stack.
Static scoping also makes it much easier to make a modular code as programmer can figure out the scope just by looking at the code. In contrast, dynamic scope requires the programmer to anticipate all possible dynamic contexts.

使用靜態作用域的語言,函式在執行之前 (定義的時候) 就已經確定了它的作用域,確定了它能使用的變數。現代大部分的語言都是屬於靜態作用域,例:C, C++, Java 和 JavaScript。

下面的例子中,f() 要回傳 x 時,因為 f() 內部沒有宣告 x,因此向定義 f() 的外部查找,也就是全域 global environment。因此,f() 只會回傳全域變數 x 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// A C program to demonstrate static scoping. 
#include<stdio.h>
int x = 10; // 全域變數 x

// Called by g()
int f() {
return x;
}

// g() has its own variable named as x and calls f()
int g() {
int x = 20;
return f();
}

int main() {
printf("%d", g()); // 10
printf("\n");
return 0;
}

而被函式聲明的一個變數,在該函式內層的其他所有函式都能被使用 (但是使用時,如果不是全域變數,記得傳遞進去)。除非該變數被另一個宣告於內部的同名變數所覆蓋。例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<stdio.h> 
int x = 10; // 全域變數 x

int f() {
return x;
}

void h(int x) {
printf("這是 h 中的:%d\n", x);
}
int g() { // g 和 x = 10 定義在同一層

printf("這是 g 中的:%d\n", x);
int x = 20; // 宣告另一個同名變數,覆蓋「全域變數 x」
printf("這是第二次 g 中的:%d\n", x);

h(x);

return f();
}

int main() {
printf("這是 f 回傳的:%d\n", g());
return 0;
}
1
2
3
4
// 這是 g 中的:10
// 這是第二次 g 中的:20
// 這是 h 中的:20
// 這是 f 回傳的:10

而動態作用域的語言則是當函式執行時,參照被呼叫時最近的變數。 例如以下的 pseudo code,因為呼叫 Fn1 時,最近的是 int value = 20

1
2
3
4
5
6
7
8
9
10
11
12
int value = 10;

void Fn1() {
Console.WriteLine(value); // 20
}
void Fn2() {
int value = 20;
Fn1();
}
void Main() {
Fn2();
}

參考資料:
Static and Dynamic Scoping