对高级程序设计语言的类型系统的基本理解

类型系统

在我的《对高级程序设计语言的基本理解》一文中我曾经阐述了自己对高级程序设计语言的理解,其中就说道了数据类型的重要性,而一门高级程序设计语言的很多甚至大部分特性都是因为类型系统而决定的。这篇文章的目的就是聊一聊我对高级程序设计语言的类型系统的理解,通过类型系统就能够了解到一门高级程序设计语言的一些基本特性。

因此树立一种"类型思维"在学习一门高级程序设计语言中是非常有必要的。

我们在描述一门语言的类型系统的时候,通常有强类型弱类型静态类型动态类型这四个术语。对于这四个术语,在网上找到的解释都非常的杂乱,而且有很多的解释是明显错误的。比如有的网友在解释Python语言为什么被定义为强类型的语言的时候,使用了如下的例子:

a = 1
b = "2"
c = a + b //error
复制代码

但是同样作为强类型语言的Java中,字符串和数字相加并不会报错,这说明上述的解释并不准确。

在这篇文章中,我会谈一谈我对这些概念的理解,同时会引入一些我自己的概念来完善我的"类型思维"。

静态类型和动态类型

《对高级程序设计语言的基本理解》已经说过了数据类型在高级程序设计语言中的意义,一门高级程序设计语言通常会定义多种基本的数据类型,而这些数据类型其实是代表了一个固定的内存长度和对对应的内存中的数据的处理方式。

说到这里,我们还需要稍微了解一点程序的运行机制,程序在运行的时候需要有一个基地址,然后CPU通过基地址+偏移量的机制对数据和指令进行寻址,从而使得程序运行起来(当然这是一个比较简单的概括,实际情况非常复杂)。

而我们又知道,高级程序设计语言必须经过一定的转换才能在计算机中运行。一般来说高级程序设计语言可以大致分为两类——解释执行型和编译执行型(当然实际情况也会更加复杂)。

通过上面的分析之后,我们来明确两个概念,就是静态动态。一般我们称解释/编译阶段为静态阶段,而成运行阶段为动态阶段

静态类型

有了上面的认识,那么所谓的静态类型指的就是变量的类型(该变量的偏移量)在静态阶段就要确定下来,这就意味着,在静态阶段程序就能够确定每个变量的偏移量是多少。这就意味着静态类型语言需要具备下列的这些特征:

  • 变量必须先声明后使用。
  • 声明某个变量的时候要么显式指定一个类型,要么进行初始化依靠类型推断确定一个类型,并且变量的类型一旦确定之后将不能再进行更改。
  • 数据类型的定义粒度要比较细,比如数字类型不能只使用一个number类型,应该定义对应的整型,浮点型等,对不同偏移量的值进行支持

静态类型检查

这里我需要引入一个自定义的概念,即静态类型检查,什么是静态类型检查呢?意思是程序的编译器或者解释器在静态阶段会对每个变量的类型进行检测,并且不允许同一个变量先后保存不同类型的值,如果要存,要么类型兼容,要么把被存储的值转换成变量的类型。同时,对于操作符的操作数、函数的参数的类型都会做对应的类型检查[1]

为什么要引入静态类型检查这个术语呢,因为静态类型检查对于静态类型的语言来说是必须的,因为静态类型语言需要在静态阶段确定变量的偏移量,那么变量的类型就必须具有不变性,否则将无法在静态阶段就确定一个变量的偏移量

而某些语言虽然是动态类型的,但是却提供了静态类型检查的功能,比如具有工具属性的TypeScript语言。

动态类型

动态类型在编码上会比静态类型的语言更加灵活,因为在静态阶段不需要确定变量的类型,一个变量的类型(偏移量)是在运行阶段才确定的。这就意味着上面静态类型所具有的那些特征对动态类型来说并不是必须的。

动态类型检查(运行时类型检测(RTTI))

这里我需要引入一个概念,即动态类型检测,也可以成为运行时类型检测(RTTI),不管是动态类型语言,还是静态类型语言,RTTI都是必须的,因为有时候数据类型的转换是不可避免的。

但是需要注意的是,RTTI的类型检测更多的是检测值的类型,而不是变量的类型,同时对于动态强类型语言,对类型的检测全部发生在动态阶段

静态类型和动态类型的对比

1565520846800

以上的JavaScript是动态类型的语言,而C++是静态类型的语言,上述两段代码实现的功能是一样的,我们从内存分配的角度分析一下动态类型和静态类型的区别

1565521806470

可以看到,JavaScript需要在运行的时候动态计算各个属性的偏移量,并且每个对象都要维护自己的属性的偏移量信息,而C++在编译的时候就已经确定好了每个对象的属性的偏移量,偏移量信息只需要保存一份就可以了。

同时,静态类型能够实现子文档化,因为静态类型的语言肯定会有静态类型检查的特性,配合IDE,写代码会更加方便。

强类型和弱类型

对于强类型和弱类型这两个概念并没有一个明确的定义,而对于这两个定义,网上的解释也比较杂乱,这里我根据自己的理解给出一个自认为比较通俗的解释。

首先,对于一门编程语言来说,最基础的部分就是类型系统,操作符,关键字和语句,有了这些最基础的部分,我们就可以实现任何复杂的程序。同时,我们最终操作数据的方式都是通过编程语言为我们定义的这些基础部分来进行的,这里我对强类型和弱类型的讨论主要是针对操作符来进行的。

我们都知道,编程语言在定义一个操作符的时候都会定义操作符的操作数数量以及操作符期望接收的操作数的数据类型,那么下面我们就展开讨论。

强类型

强类型所表现出来的特征是,如果操作符接收到的操作数的数据类型和定义操作符时所规定的数据类型不相符的时候,就会直接报告异常,静态类型的语言或者是具有静态类型检查的动态类型语言会在静态阶段报告错误,而不具有静态类型检查的动态类型语言会在运行时报告错误。这时候编程语言对数据类型的要求比较强制或者是对数据类型比较敏感,我们称之为强类型。典型的强类型语言有Java、Python等。下面给出几个Python中操作符定义的例子:

二元算术运算符遵循传统的优先级。 请注意某些此类运算符也作用于特定的非数字类型(对于支持非数字类型的运算符给出了专门的说明)。 除幂运算符以外只有两个优先级别,一个作用于乘法型运算符,另一个作用于加法型运算符:

运算符 * (乘) 将输出其参数的乘积。 两个参数或者必须都为数字,或者一个参数必须为整数而另一个参数必须为序列。 在前一种情况下,两个数字将被转换为相同类型然后相乘。 在后一种情况下,将执行序列的重复;重复因子为负数将输出空序列。

...

运算符 + (addition) 将输出其参数的和。 两个参数或者必须都为数字,或者都为相同类型的序列。 在前一种情况下,两个数字将被转换为相同类型然后相加。 在后一种情况下,将执行序列拼接操作。

运算符 - (减) 将输出其参数的差。 两个数字参数将先被转换为相同类型(因为对于数字类型来说,只有number一种,但是在运行时有可能会有整型和浮点型之分)。

这是Python官方文档对二元+操作符的定义,这解释了为什么1+"2"这样的表达式在Python中无法运行。同时注意如下代码:

a = "123"
b = 1
print(a/b)
复制代码
Traceback (most recent call last):
  File "H:/Python/Hello/src/Hello.py", line 3, in <module>
    print(a/b)
TypeError: unsupported operand type(s) for /: 'str' and 'int'
复制代码

可以看到在将一个str类型和一个number类型做整除操作的时候直接报告异常了。

弱类型

弱类型表现为对类型的容忍度较高,如果操作符接收到的操作数的数据类型和定义操作符是定义的数据类型不相符的时候,不会直接报出异常,而是首先尝试将接收到的值转换为操作符期望的数据类型的值,然后再使用操作符进行运算,如果数据类型转换失败,最终才会抛出异常。这时候我们称这门语言对类型的要求比较弱,或者是对类型不敏感。

这样的数据类型转换一般会在运行时进行,当然我们也可以在代码中对某个值进行强制类型转换。

比如JavaScript中,充斥着大量的隐式类型转换,而且在ES规范中定义了非常多的抽象操作来对这些隐式地类型转换提供操作,关于JavaScript这一部分的更多内容在JavaScript部分会有介绍。

强类型和弱类型的对比

  • 强类型语言的程序健壮性和可读性都比较好,而弱类型的语言操作符的使用更加灵活,但是需要具有较强的把控能力。

C语言的疑惑

C语言被归类为静态弱类型语言,按照我上面的论述来说也是可以说的通的,如下代码:

int main() {
    int a = 0;
    double b = 0;
    a = "123" - 3; //没有报错,但是产生的行为比较怪异,没有仔细研究过,但是足以说明C语言是一门弱类型语言
    printf("sizeof a: %d", sizeof(a)); //4 数据类型(偏移量)没有发生变化,所以是静态类型
    printf("\n");
    printf("valueof a: %d", a);
    printf("\n");
    printf("sizeof b: %d", sizeof(b)); //8
}
复制代码

真值和假值

布尔类型在任何一门高级程序设计语言中都是一个使用频率相当高的数据类型,因为任何的逻辑判断的结果都是由布尔类型的值表示的。为了编码方便,很多操作语言的类型系统都引入了真值假值的概念。

当一个不是bool类型的值转化成bool类型之后如果是true,我们就称其为真值,反之,如果一个不是bool类型的值转化为bool类型值后值为false,那么我们就称这个值为假值。这样就相当于在任何数据类型中都有跟true和false等价的值,这样我们在进行逻辑判断的时候就能够依赖真值和假值的特性。

同时,在动态类型的语言中,引入真假值会让逻辑操作符拥有更加强大的功能。

总结

以上就是我对高级程序设计语言的类型系统的基本认识,有了这些基本认识之后,再学习一门新的语言的时候先搞清楚语言的定位,能够节省很多时间。

以上纯属个人观点,有不同的观点欢迎进行讨论。


  1. 有很多人可能认为这是强类型所表现出来的特征,但是通过我对多门语言的分析发现,将这个特性冠在强类型上有失偏颇,比如Python语言是一门动态强类型的语言,但是一个变量可以先后存储不同类型的值。 ↩︎

猜你喜欢

转载自juejin.im/post/5d5050e6e51d4562165534c1