目录

聊聊编程语言的类型系统

基础概念

类型(type):也叫数据类型,它代表了一个数据集合,并为数据赋上语义。说到整形,你就知道它代表一个整数集合,说到浮点数,你就知道它代表一个小数集合,它们都有各自的规则来决定它们如何表达、如何存储、如何计算。

类型系统(type system):一个逻辑意义上的系统,定义了一套规则,指定如何为程序中各种结构标上合适的类型(变量、表达式、函数、模块等),以及如何操作这些类型、这些类型如何互相作用。类型系统的主要目的是减少程序中出现 bugs 的可能性。类型系统把类型和计算的值关联起来,确保不会发生类型错误(type error)。类型系统通常是编程语言的一部分,构建在该语言的编译器或解释器中。无论在编译时还是运行时,或者无论通过手动注明还是自动推断,编程语言必须能够用类型系统中的规则做类型检查。

定型(typing,又称类型指派):通过类型系统的规则,为数据赋上了类型,称为定型。定型赋予一组比特某个特定的意义。在电脑中,任何数值都是以一组比特简单组成的,计算机硬件本身无法区分到底是一个内存地址还是一个指令码,或者到底是字符、整形还是浮点型,因为同一组比特可能代表不同的含义,计算机无法区分它们。

语法(Syntax):打个比方,你母语的语法,就是用一套规则来规定句子的结构,对于大部分语言来说,你至少需要一个主语和一个谓语来保证句子的正确性。编程语言有它自己不同的结构,诸如表达式、控制结构、语句等。

比如,Python 中的 if 语句,除了关键字 if 以外,至少还需要:

  • 一个条件语句
  • 冒号
  • 执行主体

就像这样:

1
2
if a == 1:
  print("hello world")

不同于其它语言,Python 中 if 语句的执行主体,通过缩进来识别,而不是大括号,这就是 Python 语法之一。

语义(Semantics):如果说语法表示句子的结构,那么语义就是句子结构背后的含义。比如 if 语句的语义可以描述为:

  1. if 的条件语句被执行。
  2. 如果条件语句为 True,执行主体被执行。
  3. 如果条件语句为 False,执行主体不被执行。
  4. 继续执行后面的语句。

 

为什么需要类型

内容和语义的表达

当你声明一个变量并给它赋予一个值的时候,这个值会以二进制的形式驻在内存中。我们平时用的数字是十进制,这意味着计算机内部的数字和我们使用的数字含义完全不同:

1
2
3
4
a = 'A'
b = 65
print(ord('A'), b)
print(bin(ord('A')), bin(b))

执行结果:

1
2
65 65
0b1000001 0b1000001

整数 65 和字符 ‘A’ 在内存中的二进制值完全相同。当我们使用这两个变量的时候,我们编程语言(Python)的类型系统会解释内存中的这两个值,并决定哪个是字符,哪个是整形。我们看到的数字 65 和字符 ’A‘ 只是相同的二进制的不同的表示。

另外,类型还决定如何在内存中存储不同的值。对于整形和字符,我们看到它们的存储方式是一样的,但对于浮点类型则完全不同。

类型系统帮助我们把内存的布局给抽象化了,在大多数情况下,开发人员不需要关心这些二进制数字。

一系列规则

如同上一节说的,类型系统定义了一系列规则,无论是否严格:

1
2
3
4
>>> 'my age is: ' + 20
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +: 'str' and 'int'

当你的代码违反了类型系统设定的规则时,结果一般有两种情况:

  1. 编译器/解释器尝试自己修复问题并继续执行。
  2. 编译器/解释器抛出错误并暂停执行。

在 Python 中,"‘my age is: ' + 20" 这句代码是毫无意义的,因为它尝试把一个整形和字符串相加。Python 解释器会抛出一个 TypeError 异常,告诉开发人员这个类型错误。由于不同语言的类型系统是不一样的,所以这句代码在有些语言中也许可以完全正常执行,结果可能是 “my age is: 20”。

 

类型声明

类型声明可以分为隐式类型声明、显示类型声明以及它们的混合方式。

隐式类型声明

有些语言,比如 Python,你声明一个变量的时候不需要指定类型,而在赋值给这个变量的时候决定了它的实际类型:

1
2
3
4
5
6
a = 10
print(type(a))
a = "hello world"
print(type(a))
a = [1, 2, 3]
print(type(a))

执行结果:

1
2
3
<class 'int'>
<class 'str'>
<class 'list'>

你不需要指定变量的类型,Python 解释器会在运行时检查变量 a 指向的对象的实际类型。这也称为类型推测(type inference)

显示类型声明

有些语言,比如 C/C++,你声明一个变量的时候必须指定类型,即使没有给这个变量赋值。编译器需要在编译的时候就知道变量的类型。

1
2
3
4
5
6
7
8
#include <iostream>

int main()
{
  a = "hello";
  std::cout << a << std::endl;
  return 0;
}

执行结果:

1
2
3
test.c: In function ‘int main()’:
test.c:5:3: error: ‘a’ was not declared in this scope
   a = "hello";

变量 a 声明的时候没有指定类型,程序编译出错。

混合类型声明

有些语言,比如 Golang,既可以显示地为变量指定类型,也可以不指定:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
	"fmt"
	"reflect"
)

func main() {
    beautifulInt := 65
    var explicitBeautifulInt int = 65
   
    fmt.Println(reflect.TypeOf(beautifulInt))

    fmt.Println(reflect.TypeOf(explicitBeautifulInt))
}

执行结果:

1
2
int
int

 

类型检查

如果说类型推动我们去遵守一些规则,那么编程语言就需要一套算法来检查我们是否遵守了这些规则。这个叫做类型检查(type checking)

虽然对于不同的编程语言,类型系统有很大的差别。但我们依然可以按照一些准则来对它们进行分类。编程语言按照类型检查的时机,有两个重要的分类:静态类型检查(static type checking)动态类型检查(dynamic type checking)。它们按照类型检查算法何时对你的代码进行检查来区分。

动态类型检查

如果对类型的检查发生在运行时,那么就是动态类型检查。如果编程语言只做动态类型检查,而没有静态类型检查,我们可以把它称为动态类型语言

如果你的代码违反了类型系统的规则,那么在运行时,编译器/解释器会找到错误、打印出来,并停止执行。也可能不停止执行,取决于对错误的处理方式。

实现动态类型检查的语言,一般会把每一个运行时的对象关联上一个类型标签(比如,通过一个引用,指向类型对象),类型标签包含了类型相关的信息,它们被称为运行时类型信息(RTTI)RTTI 也可用于动态绑定、延迟绑定、反射等技术。

大部分类型安全(type-safe)的语言,即使它们有静态类型检查器,也都或多或少会做动态类型检查。这是因为有一些语言特性很难在静态(编译)时检验。比如,假设程序中定义了两种类型 A 和 B,B 是 A 的子类。如果尝试把 A 类型引用的对象转换成 B 类型,这叫做向下转换(downcasting)。只有当被转换的对象实质上是一个 B 对象时,转换才是合法的。因此,动态类型检查在这种情况下就是必须的,用来确保 A 类型引用的对象实质是个 B 对象。这种需求也是很多人批评向下转换机制的理由。

Python、PHP 就是典型的动态类型语言。

静态类型检查

如果对类型的检查发生在运行之前、编译期间,那么就是静态类型检查。大部分类型检查是静态类型检查的语言,称为静态类型语言

静态类型检查,意味着编译器/解释器需要在运行之前,知道程序中定义个每个数据确切的数据类型。当数据类型需要在运行时决定时就会有问题。这就是为什么,即使是静态类型语言,或多或少也会做一些动态类型检查。一个典型的例子是 C++ 运行时类型检查(C++ RTTI)C++ 本身是静态类型语言,因为大部分类型的检查都是在编译环节做的。但是 C++ 的动态绑定功能需要在运行时检查指针指向具体什么类型,这种在运行时检查类型方式,就是 C++ RTTI。 除 C++ 外,其它面向对象语言也大多如此,基类类型的指针或引用,运行时指向的对象可以是基类对象本身,也可以子类的对象,而通过该指针或引用来访问方法的时候,会根据实际指向的对象类型来调用对应的方法,这个概念也称为运行时多态性,简称多态性

既然大部分类型检查是在运行前检查,这意味着静态类型语言的运行效率会更好一些,因为编译器会优化你的代码,使得大部分类型在运行时已经被决定了。

C++、Golang 就是典型的静态类型语言。

 

类型强度和类型安全

另一个把编程语言分类的角度是看这语言的类型系统是强类型还是弱类型(也叫松类型)然而事实上没有一个明确的定义告诉我们什么叫强类型、什么是弱类型。无论是学术上还是工程上,人们对这个分类术语的含义并没有达成一致。但很多时候,人们说的类型强度往往是和类型安全关联在一起。

为什么这么说呢?我们先来看看什么是类型安全。

关于什么是类型安全(type safe),有个技术委员会在 2000 年的时候为类型安全做过定义(引述自康纳尔大学的 计算机教程):

Safety: Any attempt to misinterpret data is caught at compile time or generates a well-specified error at runtime.

翻译:任何尝试曲解数据含义的企图都会在编译时被捕获,或者在运行时产生一个明确的错误,那我们称它是类型安全的。

C 和 C++ 并不是类型安全的。比如即使数组的下标越界,编译时也不会出错,运行时也完全可能正常而不被察觉。另外,也没有保护措施,来防止使用指针访问任何内存区域。

为什么很多时候,类型强度是和类型安全关联在一起的呢?因为一个语言如果是强类型,那就意味着它会强制要求类型安全性,所以当人们说强类型或弱类型的时候,他们往往指的是类型安全。

从这个角度来说,一个强类型语言,也是类型安全的,它往往在编译时有较严格的类型规则,从而意味着大部分的错误和异常更有可能在编译时被捕获,或者在运行时会抛出明确的类型错误。类型安全是指这个语言的编译器/解释器为了发现类型错误,会做多大程度的检查,以及抛出“类型错误”异常的频率。这里“多大程度”的定义并不是很明确。通常来说,它取决于用什么编程语言,甚至取决于同一个语言的不同版本。

同理,一个弱类型语言,也是类型不安全的,它有较宽松的类型规则,在编译时不会察觉到,所以可能会在运行时产生难以预料的、错误的结果,也有可能在运行时进行隐式类型转换。

为什么需要类型 一节中本人为了说明类型系统定义了一系列规则,举了一个 Python 的 TypeError 的例子。如果同样的功能用 PHP 来实现会如何呢:

1
2
<?php
echo "<p>my age is: " . 20 . "</p>";

同样把一个字符串和一个整形拼在一起,PHP 并不会报错,而是正常执行。正是因为 PHP 解释器能理解你想把整数 20 转换成字符串并和另一个字符串拼在一起,所以它暗地里自动帮你转换类型了。

编译器/解释器自动地、隐式地帮你转换类型而不通知你,这种行为叫做隐式类型转换(implicit type change)或者叫 type coercion

与隐式类型转换相对应的是显示类型转换(explicit type change)或者叫type casting

隐式类型转换,是弱类型语言的一个特征。 像 PHP 这种经常做隐式类型转换的语言,做类型检查的程度较轻,也较少抛出类型错误异常,所以可以被认定是弱类型的。

而在 Python 的例子中,运行时抛出了类型错误异常,意味着 Python 做类型检查较严格,所以可以被认定为是强类型。

另一个判断是否类型安全的角度是,该语言是否会阻止你访问你不应该访问的内存 。从这个角度来看,C 和 C++ 显然不满足。所以把类型强弱和类型安全关联在一起话,C 和 C++ 就是典型的弱类型。

 

主流语言分类

如果从类型检查和类型强度两个维度出发,并且用上一节讲的类型安全来定义类型强度,那么我们可以把主流语言分成四大类:

  • 动态类型、强类型:Python,Erlang,Ruby 等。
  • 动态类型、弱类型:PHP,Perl,JavaScript 等。
  • 静态类型、强类型:Java,C# 等。
  • 静态类型、弱类型:C,C++ 等。

如果用一个坐标来表示,就是这样子:

/2019/type_system_in_programming_languages/classify_languages.png
主流语言分类(引自 https://www.zhihu.com/question/19918532)

 

总结

从不同的编程语言来观察不同的类型系统是个有趣的实践,它会帮助你提高解决相关问题的能力。类型系统用来表达语义,而语义就是编程语言的哲学。不要把自己禁锢在一种语言内,尝试不同的语言可以帮助你理解不同类型系统的差异。

总结一下本文的知识点:

  • 类型代表了一个数据集合,并为数据赋上语义。
  • 为了强制执行类型系统的规则,通常在编译时做类型检查(静态类型检查),或者在运行时做类型检查(动态类型检查)。
  • 申明一个变量可以是隐式的(类型推测),也可以是显示的。
  • 类型的强弱没有正式的定义。但很多场合,类型强弱和类型安全关联在一起。
  • 类型安全是指:任何尝试曲解数据含义的企图都会在编译时被捕获,或者在运行时产生一个明确的错误。
  • 转换类型可以是隐式的(type coercion),也可以是显示的(type casting)。
  • 你需要了解你正在使用的语言的类型系统,它可以帮助你减少错误。

 

参考

https://en.wikipedia.org/wiki/Type_system

https://www.cs.cornell.edu/courses/cs1130/2012sp/1130selfpaced/module1/module1part4/strongtyping.html

https://thevaluable.dev/type-system-software-explained-example/

https://en.wikipedia.org/wiki/Strong_and_weak_typing

https://www.zhihu.com/question/19918532


- 全文完 -

相关文章

「 您的赞赏是激励我创作和分享的最大动力! 」