我们经常会听到这样的讨论,“动态语言不适合大型项目”,“xx 语言不是类型安全的”,诸如此类。但对于这些概念我们真的清楚吗?

一般来说,从类型系统的角度,我们有三个相互独立的概念:

  • 强类型/弱类型
  • 静态定型/动态定型
  • 类型安全/非类型安全

下面我们非形式化地讨论一下这几个概念。

强类型/弱类型

强类型语言,即在该编程语言中,没有隐性的数据类型转换。

一个典型的例子如下:

a = '2'
b = a - 1

在 JavaScript,b 的值为 1;而在 Python 中,则会产生错误 TypeError: unsupported operand type(s) for -: 'str' and 'int'

JavaScript 的顺利执行,归因于编译器(解释器)隐性地完成了类型转换的操作,对应的 Python 代码应该像下面这样:

a = '2'
b = int(a) - 1

但是一个语言是强类型还是弱类型有时是有争议的,因为哪些转换是允许的并没有一个很严格的界定。考虑下面的 Python 代码:

a = 2
b = a - 1.0

其中也存在 int 到 float 的隐性类型转换,但是可以顺利执行。

这样的隐性类型转换往往就是允许的,因为也是符合数学直觉的(减法运算的操作数是数值,而非字符串)。

静态定型/动态定型

静态定型,在编译时进行类型检查;动态定型,在运行时进行类型检查。这一概念往往比较清楚,因为静态定型依赖于类型标注,比如前面的例子在 C 语言中就必须写成:

int a = 2;
int b = a - 1;

类型安全/非类型安全

类型安全,即不会出现逃逸出该编程语言类型规则的情况。这个概念比较抽象,有时会被混淆。所谓逃逸出类型规则,典型的例子是 C 语言的类型双关(type punning)。

bool is_negative(float x) {
    unsigned int *ui = (unsigned int *)&x;
    return *ui & 0x80000000;
}

在使用 IEEE 754 浮点数标准的条件下,利用指针类型转换实现类型双关来获取浮点数的符号位做整型计算,可以比以下简单的浮点计算得到更优的性能。

bool is_negative(float x) {
    return x < 0.0;
}

注意负零(10000000)时的区别。

类型双关也可以用 union 来实现:

bool is_negative(float x) {
    union {
        unsigned int ui;
        float d;
    } my_union = { .d = x };
    return my_union.ui & 0x80000000;
}

非类型安全往往还和未定义行为(undefined behavior)联系在一起,常常出现在没有显式错误处理的语言中。对应的典型漏洞利用(security exploits)如 stack smashing attack (stack buffer overflow) 和 format string attack (uncontrolled format string)。

同样需要注意的是,设计上的类型安全不等于真正的类型安全,主要出于以下原因:

  • 对于绝大多数语言,很难做到完整的形式化证明(参考 Featherweight Java
  • 语言在实现上可能存在 bug
  • 链接库可能使用其他语言

现在,基本清楚这三个概念在说些什么了,我们就来看看一些常见的编程语言分别属于哪一类。

Programming Language Strong/Weak Type Static/Dynamic Typing Type Safety/Type Unsafety
C Strong (?) Static Unsafety
Java Strong (?) Static Safety
Python Strong (?) Dynamic Safety
JavaScript Weak Dynamic Unsafety
Haskell Strong Static Safety

? : 对于哪些隐性类型转换是可接受的存在争议