谈变量的类型
为什么需要类型
我们先从类型系统谈起,其实在计算机运算时,并不会关心类型。比如 'C' + 25
,从类型上来看,字符串无法和整数进行相加,因为这么做没有意义。但是如果抛开类型的话,其实在计算机底层运算的时候,'C' 是 0x43 这个数字,25 是0x19 这个数字,所以从底层来看它们进行相加并没有什么问题。所以类型是我们自己约定出来的一套规则。因为我们写出来的程序需要在日常生活中能够使用。所以需要一个框架的约定来理解这个数字。比如我们一起约定一个框架叫 "uint8",这个框架的作用是状态的数字一对一到整数集合 0 到 255 之间的数字。在这个计算框架下,计算 0x02 + 0x03 得到了 5,就是有意义的。如果计算字符串 'C' 与 25 的和,得到的结果就是没有意义的,因为无法通过这个约定的框架来解释。之后为了工程上的完备,我们又约定了浮点数,定点数,不同的编码(UTF-8、UTF-16),来把自动机接受的状态映射到不同的内容上。这就是类型做的事情。我们在编写程序的时候不会面向内存思考它到底在内存里是怎么表示的(况且部分语言还不是一一对应内存的,包括绝大部分脚本语言,并且部分语言禁止你直接操控内存,必须约定的类型来间接修改内存),通过类型这个抽象层来减少我们的思考,把重心放在编写逻辑上是很有必须要的,这么做就需要类型系统的介入。如果没有了类型系统,计算机运行既不直观也不容易预测结果,很容易出现 untrapped error。在强类型类型系统可以在编译时(静态语言)和运行时(动态语言)提前发现以减少 untrapped error 问题的出现,提高程序的健壮性。
数据的位置和长度:针对类型设计的类型系统是从语法层面定义了一组操作,而从底层来看,类型也提供了数据的位置和长度。也就说,数据在哪就是地址,数据长度其实某方面就是取决于类型。指针的本质就是存储别的变量的地址,那么指针的类型是干什么用的呢?是的,就是去访问指针所指向的地址的时候,指针的类型和指针的值(变量地址),提供了一条汇编指令最关键的两点:数据的长度和数据地址。
编译型和解释型
为什么 C/C++、Java 这类编译型语言就需要显示声明类型,而 Python 这类解释型语言就不需要呢?所以是所有的编译型都需要申明类型?所有的解释型都不需要?
这个理解是不对的。首先,"编译"和"解释"是语言实现的特征,语言本身可以以任何一种方式实现。可能我们大多只了解过C编译器(如GCC、Clang),却很少听过 C 解释器,如 Ch。但是,即使使用 Ch 解释器,我们仍然需要在 Ch 解释的 C 代码中申明类型。Python 的对象模型本身在运行时自带了"类型"这个信息。在 Python 中所有变量的本质上都是一个对象的"指针",也正因为如此,Python 中的各种变量直接才能随意赋值,因为赋值的本质其实就是改变一下变量所指向的对象。而真正到了解释执行的时候,Python 解释器就会取出这个类型字段,来判断操作是否符合语法,不符合的话就会运行时报错。
如何看待变量
值得注意的是静态编译和动态编译与强类型和弱类型之间没有必然的联系。强类型与弱类型区分的根源是变量与数据类型的关系,在所有的计算机语言中,大体有两种看待变量的方式:变量是保存数据的容器和变量是访问数据的入口。
-
变量是保存数据的容器:采用第一种方式的计算机语言,因为变量需要保存数据,所以变量受其保存的数据的类型制约。将数据保存到同类型变量的过程,称为赋值。在我们声明变量时,计算机要给这个变量分配内存,这时,计算机至少需要知道这个变量将来的赋值数据的物理大小,于是,计算机语言,不得不要求,声明变量时,必须指出其赋值数据的类型(包含物理大小信息)。另一方面,根据数据类型对变量被分配的内存将伴随它的整个声明周期,同时,在访问变量中数据时还会使用数据类型,这就意味着,声明时指定的那个本意是赋值数据的类型变成了该变量的类型,这就是所谓的变量类型。这种变量有类型的语言,称为强类型语言。
-
变量是访问数据的入口:采用第二种方式的计算机语言,其变量仅仅保存数据在对象池中的入口地址,而不保存数据。将数据入口地址保存到变量的过程,称为绑定。我们在声明变量时,计算机只需要分配一个保存绑定地址的内存块就可以了,无需知道将要绑定的什么数据,于是变量也就没有了类型。这种变量没有类型的语言,称为弱类型语言。
强类型和弱类型
最后,我们来比较一下强弱两种类型的语言。早期,强类型语言,都是编译(静态)语言,例如:C/C++,Fortran,Pascal 等,这类语言,没有强大的运行时来支持对象池,因此只能采用第一种方式。而早期,弱类型语言,都是解释(动态)语言,例如:Lisp,Scheme 等,它们有强大的解释器,其中包括对象池,因此可以采用第二种方式。
强类型语言,有一个非常大的优势,那就是:编译器知道变量的类型,可以提前检查赋值错误,再加上,编译语言的运行性能优势,这使得,强类型语言,在上世纪中叶很快成了主流。但是,强类型的优势也是缺陷,这就是:由于变量带有类型,所以代码和类型强关联,很难写出同时适用于多种类型的代码,为了修补这个缺陷,几乎同时出现了两种解决方案:从宏(模板)发展出来的泛型和面向对象(OOP)。泛型使得类型可以成为某个代码块的参数,在使用该代码块时被具体制定。面向对象利用继承让子类对象复用父类对象的代码块。在经过,泛型和 OOP 改造后,强类型语言在千禧年后,到达了顶峰,以至于这时,出现的 Java 和 C# 这样的动态编译语言,也采用强类型。
但是弱类型语言并非一无是处:没有变量类型是天然的泛型、OOP 也可以引入和适用于脚本代码。因此,才有 JavaScript 和 Python 这样的弱类型语言,随着计算机性能的飞速发生使得强类型语言的性能优势慢慢削弱,而弱类型语言的简单灵活慢慢凸显,这使得,如今的它们也是如日中天。
计算机源于数学,早在第一台计算机出现之前,数据家就对可行性计算问题进行了深入的研究,先后出来了:递归函数、lambda演算、图灵机,之后图灵机称为了计算机体系结构的数学原理,而 lambda演算正是函数式编程的本质。因此,我们可以从数学角度来稍微看一下变量(常量)类型。
变量是否有类型,仅仅是计算机语言的类型系统的一部分,即便是同为强或弱类型语言,其类型系统也差距较大,以下是一些类型系统具有代表性的语言:
- C 语言,代表命令式编程,其类型系统以过程为核心进行设计
- C++ 语言,代表传统多继承面向对象,其类型系统多继承类-对象为核心
- Java 语言,代表传统单继承面向对象,其类型系统单继承-对象为核心,以接口弥补单继承的不足
- JavaScript 语言,代表原型链单继承面向对象,其类型系统构造函数-原型链为核心
- Scala 语言,代表加入特性的面向对象,特性的加入弥补的单继承的不足
- Lisp 语言,代表传统函数式编程,其类型系统以符号表达式为核心
- Haskell 语言,代表加入范畴的函数式编程,数学中的类型,就本质而言,就是基于集合(或者比集合更大的类)之上的各种数学系统,目前最大的代数系统是范畴,Haskell 采用的就是以范畴为核心的类型系统;当然,类型系统还包括,宏和泛型,Scheme 的卫生宏、C# 的泛型,都是典型代表