Weak Typing Explained

常常聽到有些人誤解了 weak typing 的意義,或是對 C 語言同時是 static typed 與 weak typed 感到疑惑,因此提出一些我自己的想法。

我們先從 type checker 說起吧。

Type Checking

為了要讓人類容易理解並撰寫程式,幾乎所有的高階程式語言都有型別系統(type system)。變數的型別限制我們可以如何操作這個變數,比如說數字可以加減乘除、字串可以串接或截取部份等等。拿數字去做字串操作,或是拿字串去加減乘除[1],都應該視為錯誤操作。[2]

在實作程式語言時,不管是 compiler 或是 interpreter,只要語言中包含型別系統,就會試圖阻止超出型別限制的變數操作,這個功能稱之為 type checking。

1
2
3
4
5
6
x = "foo";
y = "bar";
// 如果星號代表數字乘法,下面這行無法通過 type checking,
// compiler 或 interpreter 應該拒絕執行這行程式碼。
z = x * y;

Type checker 可以在 compile 的時候檢查變數的型別,也可以延遲到執行時才進行檢查,這就是 static type-checking 與 dynamic type-checking 的差別。

Static/Dynamic Type-Checking

這兩個名詞通常沒什麼爭議:static type-checking 意指程式中的變數在 compile time 時進行 type checking,而 dynamic type-checking 意指在 run time 才進行 type checking。

1
2
3
4
5
// Java code
String x = "foo";
String y = "bar";
String z = x * y;
// ^ error: bad operand types for binary operator '*'
1
2
3
4
5
6
7
8
# Ruby code
def hello
x = "foo"
y = "bar"
rerturn x * y # 不會發生 compile error
end
hello # 執行時產生 TypeError: can't convert String into Integer

Weak Typing

而所謂的 weak typing,一般會有兩種解釋方法:

  1. 語言在遇到 type error 時,會自動轉換型別以符合要求。
  2. 語言省略了部份的 type checking。

我個人比較傾向第二種解釋方法,以下將會說明為什麼我會選擇後者。

自動型別轉換

為了方便程式設計師,程式語言或多或少會進行自動型別轉換,比如說這個例子:

1
2
# Perl code
print "10" + 20; # print "30"

有些語言則認為這是不良習慣:

1
2
3
# Python code
print "10" + 20 # TypeError: cannot concatenate 'str' and 'int' objects
print int("10") + 20 # ok

有些人會藉此主張 Perl 與 C 屬於 weak typing,而 Python 與 Java 屬於 strong typing。然而實際上,自動型別轉換發生在各種地方:

1
2
# Python code
print 10 + 20.0 # 自動轉換成 float(10) + 20.0
1
2
// Java code
System.out.println("10" + 20); // 自動轉換成 "10" + Integer.toString(20)

大部份的主流語言 (Java、C#、Python、Ruby) 都會進行自動型別轉換,真要符合定義的話,大概要像 Haskell 這樣:

1
2
3
4
5
6
7
-- Haskell code
let x = 10 :: Float
let y = 20 :: Double
let z = x + y -- error: Couldn't match expected type `Float' with actual type `Double'
import GHC.Float
let z = float2Double(x) + y -- OK

OCaml 甚至區分了整數加法與浮點數加法:

1
2
3
4
(* OCaml code *)
1 + 2.0;; (* type error *)
1.0 + 2.0;; (* type error *) (* 加號只能用在整數 *)
1.0 +. 2.0;; (* OK *)

嚴格要求明確的型別轉換有好處也有壞處,不過大部份人恐怕都無法習慣 Haskell 或 OCaml 這麼嚴格的規則。即使是以 "Explicit is better than implicit" 為理念的 Python 也仍然在語言上允許相當多的自動(implicit)型別轉換。

我比較不傾向用自動型別轉換來區分程式語言屬於 strong typing 或 weak typing,因為大多數主流語言幾乎都做了自動型別轉換,你頂多只能說某個語言在型別上比另一個語言「強」[3],而不是直接把語言劃分為 strong typing 與 weak typing 兩大塊。

省略 Type Checking

另一個 weak typing 的定義是語言在某些地方會省略 type checking,或是允許程式設計師用某種方法繞過 type checker 的限制。這件事在 C 裡面實在太常見了:

1
2
3
/* C code */
int x = 10;
printf("%s\n", x); /* undefined behavior */

printf 的函式宣告採用不定參數,因此 compiler 省略了對 x 的型別檢查,其結果為 undefined behavior。另一個例子是 union:

1
2
3
4
5
6
7
8
9
10
/* C code */
union {
int x;
char c[2];
} u;
u.c[0] = 10;
u.c[1] = 20;
printf("%d\n", u.x); /* undefined in C89 */
/* implementation-defined in C99 */

C 並不會在 union 結構中記錄上一個存入的成員為何,因此也無法在 run time 發出錯誤以阻止使用者取用 u.x

相較於自動型別轉型,我認為藉由是否省略 type checking 來區分 weak/strong typing 是比較明確的定義方法。在這個定義下,C 與 C++ 屬於 weak typing,而 Java、C#、Perl、Python、Ruby 屬於 strong typing。


  1. 有人可能認為字串加法就是串接的意思,但實際上只是許多語言用加號來同時代表數字加法與字串串接,我們還是不能對字串進行數字的加法。

  2. 承上,個人認為使用加號來進行字串串接是錯誤設計,既然串接與加法是不同概念,就應該使用不同的運算元,但不知為何一堆語言還是這麼做。

  3. 比較自動型別轉換的數量也很奇怪,C# 或 Java 的 integer promotion 規則可多了,甚至 Java 還有 auto boxing/unboxing,在這定義下實在很難稱它們為 strong typing。

分享到 評論