聊聊编程语言的类型系统
基础概念
类型(type)
:也叫数据类型,它代表了一个数据集合,并为数据赋上语义。说到整形,你就知道它代表一个整数集合,说到浮点数,你就知道它代表一个小数集合,它们都有各自的规则来决定它们如何表达、如何存储、如何计算。
类型系统(type system)
:一个逻辑意义上的系统,定义了一套规则,指定如何为程序中各种结构标上合适的类型(变量、表达式、函数、模块等),以及如何操作这些类型、这些类型如何互相作用。类型系统的主要目的是减少程序中出现 bugs 的可能性。类型系统把类型和计算的值关联起来,确保不会发生类型错误(type error)
。类型系统通常是编程语言的一部分,构建在该语言的编译器或解释器中。无论在编译时还是运行时,或者无论通过手动注明还是自动推断,编程语言必须能够用类型系统中的规则做类型检查。
定型(typing,又称类型指派)
:通过类型系统的规则,为数据赋上了类型,称为定型。定型赋予一组比特某个特定的意义。在电脑中,任何数值都是以一组比特简单组成的,计算机硬件本身无法区分到底是一个内存地址还是一个指令码,或者到底是字符、整形还是浮点型,因为同一组比特可能代表不同的含义,计算机无法区分它们。
语法(Syntax)
:打个比方,你母语的语法,就是用一套规则来规定句子的结构,对于大部分语言来说,你至少需要一个主语和一个谓语来保证句子的正确性。编程语言有它自己不同的结构,诸如表达式、控制结构、语句等。
比如,Python 中的 if 语句,除了关键字 if 以外,至少还需要:
- 一个条件语句
- 冒号
- 执行主体
就像这样:
|
|
不同于其它语言,Python 中 if 语句的执行主体,通过缩进来识别,而不是大括号,这就是 Python 语法之一。
语义(Semantics)
:如果说语法表示句子的结构,那么语义就是句子结构背后的含义。比如 if 语句的语义可以描述为:
- if 的条件语句被执行。
- 如果条件语句为 True,执行主体被执行。
- 如果条件语句为 False,执行主体不被执行。
- 继续执行后面的语句。
为什么需要类型
内容和语义的表达
当你声明一个变量并给它赋予一个值的时候,这个值会以二进制的形式驻在内存中。我们平时用的数字是十进制,这意味着计算机内部的数字和我们使用的数字含义完全不同:
|
|
执行结果:
|
|
整数 65 和字符 ‘A’ 在内存中的二进制值完全相同。当我们使用这两个变量的时候,我们编程语言(Python)的类型系统会解释内存中的这两个值,并决定哪个是字符,哪个是整形。我们看到的数字 65 和字符 ’A‘ 只是相同的二进制的不同的表示。
另外,类型还决定如何在内存中存储不同的值。对于整形和字符,我们看到它们的存储方式是一样的,但对于浮点类型则完全不同。
类型系统帮助我们把内存的布局给抽象化了,在大多数情况下,开发人员不需要关心这些二进制数字。
一系列规则
如同上一节说的,类型系统定义了一系列规则,无论是否严格:
|
|
当你的代码违反了类型系统设定的规则时,结果一般有两种情况:
- 编译器/解释器尝试自己修复问题并继续执行。
- 编译器/解释器抛出错误并暂停执行。
在 Python 中,"‘my age is: ' + 20" 这句代码是毫无意义的,因为它尝试把一个整形和字符串相加。Python 解释器会抛出一个 TypeError 异常,告诉开发人员这个类型错误。由于不同语言的类型系统是不一样的,所以这句代码在有些语言中也许可以完全正常执行,结果可能是 “my age is: 20”。
类型声明
类型声明可以分为隐式类型声明、显示类型声明以及它们的混合方式。
隐式类型声明
有些语言,比如 Python,你声明一个变量的时候不需要指定类型,而在赋值给这个变量的时候决定了它的实际类型:
|
|
执行结果:
|
|
你不需要指定变量的类型,Python 解释器会在运行时检查变量 a 指向的对象的实际类型。这也称为类型推测(type inference)
。
显示类型声明
有些语言,比如 C/C++,你声明一个变量的时候必须指定类型,即使没有给这个变量赋值。编译器需要在编译的时候就知道变量的类型。
|
|
执行结果:
|
|
变量 a 声明的时候没有指定类型,程序编译出错。
混合类型声明
有些语言,比如 Golang,既可以显示地为变量指定类型,也可以不指定:
|
|
执行结果:
|
|
类型检查
如果说类型推动我们去遵守一些规则,那么编程语言就需要一套算法来检查我们是否遵守了这些规则。这个叫做类型检查(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 来实现会如何呢:
|
|
同样把一个字符串和一个整形拼在一起,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++ 等。
如果用一个坐标来表示,就是这样子:
总结
从不同的编程语言来观察不同的类型系统是个有趣的实践,它会帮助你提高解决相关问题的能力。类型系统用来表达语义,而语义就是编程语言的哲学。不要把自己禁锢在一种语言内,尝试不同的语言可以帮助你理解不同类型系统的差异。
总结一下本文的知识点:
- 类型代表了一个数据集合,并为数据赋上语义。
- 为了强制执行类型系统的规则,通常在编译时做类型检查(静态类型检查),或者在运行时做类型检查(动态类型检查)。
- 申明一个变量可以是隐式的(类型推测),也可以是显示的。
- 类型的强弱没有正式的定义。但很多场合,类型强弱和类型安全关联在一起。
- 类型安全是指:任何尝试曲解数据含义的企图都会在编译时被捕获,或者在运行时产生一个明确的错误。
- 转换类型可以是隐式的(type coercion),也可以是显示的(type casting)。
- 你需要了解你正在使用的语言的类型系统,它可以帮助你减少错误。
参考
https://en.wikipedia.org/wiki/Type_system
https://thevaluable.dev/type-system-software-explained-example/
https://en.wikipedia.org/wiki/Strong_and_weak_typing
https://www.zhihu.com/question/19918532
「 您的赞赏是激励我创作和分享的最大动力! 」
- 原文链接:https://zhuyinjun.me/2019/type_system_in_programming_languages/
- 版权声明:本创作采用 CC BY-NC 4.0 国际许可协议,非商业性使用可以转载,但请注明出处(作者、链接),商业性使用请联系作者获得授权。