Do more! Do better!

编程语言的类型系统

Posted on By zjk

《计算机科学导论期末读书报告》

大一什么都不懂的时候写的东西

1.引言

类型系统被采用并被作为类型检查的一种手段,从二十世纪五十年代的FORTRAN语言编译器就已开始。采用类型论(type theory)观点的编程语言类型系统的研究,在软件工程、编程语言设计、高性能编译器和网络安全等方面都有重要应用.通过学习计算机科学导论这门课程以及课下查阅资料,作者加深了对编程语言中的类型系统的概念的理解,并对其作用和存在的问题有了一定了解。现将所得整理在此读书报告中。
S

2.类型系统(type system)

2.1定义

类型,通常认为是一组可能的值的集合。比如整型,其可能的值是整数的集合;深究系统一词的内涵具有一定的困难。在我们讨论的编程语言这一领域内,当提到“类型系统”时,一个较合理的解释为:一组基本类型构成的“基本类型集合”;及“基本类型集合”上定义的一系列组合、运算、转换方法。
我们可以得到如下的类型系统的定义:
编程语言的组成部分,它由一组定型规则(typing rule)构成,这组规则用来给各种程序构造(变量、表达式和函数等)指派类型
例1: “若M和N都是long类型的表达式, 则M+N也是long类型的表达式”是非形式描述的定型规则

例2:若函数f的某个形参是long类型,则对应的实参也应是long类型。若对应实参是char、short和int类型,则系统会自动把它们提升为long类型。[1]

关于类型系统的定义亦可参见:

A type system is a collection of rules that assign a property called type to various constructs a computer program consists of, such as variables, expressions, functions or modules.[2]

2.2作用

我们知道计算机存储是以二进制方式进行的,并以连续的八个二进制位为一个基本单元——“字节”。以此来看,计算机存储方式是通用的,存储文字、图像、声音或其他别的媒介都没有内在的本质差异。
而实际情况是,我们在编程语言概念上人为引入一系列的迥异的“基本类型”,如C语言中的int和double类型。某种意义上,int和double本身并没有什么差别,都只不过是若干个字节构成的存储单元而已。
那我们为何要设置“复杂”的类型系统?其实就“存储”概念而言,我们用二进制方式,以字节为单位来实现信息的存储,已经足够。但是我们注意到,计算机或者进一步说,编程语言,其作用主要有二,即存储信息和处理信息。事实证明,研究类型理论(type theory),划分类型系统,将在对信息处理方面带来极大便利。

A type system is a tractable syntactic method for proving the absence of certain program behaviors by classifying phrases according to the kinds of values they compute.[3]

The fundamental purpose of a type system is to prevent the occurrence of execution errors during the running of a program.[4]

换句话说,设计类型系统的根本目的是利用类型检查(将在下文中介绍)的方式来保证合法程序运行时侯的行为是良好的。
类型化的编程语言除了有助于进行类型检查,较早的发现错误,还另程序模块可以互相独立地编译,并且可得到更有效的空间安排与访问方式。我们也看到由于不同类型数据运算规则的差异,类型系统的存在也是必要的。

并且,结合类型的实际意义,可知,“类型理论解决了使程序具有意义的基本问题,同时引发了一个问题,即一个有意义的程序没有相应的类型与之对应,这也导致了对更加丰富的类型系统的需求。” [5]

2.3相关概念

我们先引入一些下文论述中会出现的概念
trapped errors导致程序终止执行的错误,如除0,Java中数组越界访问
untrapped errors 出错后继续执行,但可能出现任意行为。如C里的缓冲区溢出、Jump到错误地址
Forbidden Behaviors 语言设计时,可以定义一组forbidden behaviors. 它必须包括所有untrapped errors, 但可能包含trapped errors.
Well/ill behaved: 如果程序执行不可能出现forbidden behaviors, 则为well behaved,否则为ill behaved。
了解了以上概念,我们就可以对编程语言进行区分。
1 一种语言的所有程序都是well behaved——即不可能出现forbidden behaviors,则该语言为强类型(strongly typed)。否则为弱类型(weakly typed),比如C语言的缓冲区溢出,属于trapped errors,即属于forbidden behaviors。
2静态类型(statically typed): 在编译时拒绝ill behaved的程序。
动态类型(dynamiclly typed): 在运行时拒绝ill behaviors,
3.如果类型是语言语法的一部分,则是显式类型(explicitly typed);如果类型通过编译时推导,是隐式类型(implicity typed), 比如ML和Haskell4.

由此我们可以对常见的编程语言进行分类,分类如下

无类型: 汇编
弱类型、静态类型 : C/C++
弱类型、动态类型检查: Perl/PHP
强类型、静态类型检查 :Java/C#
强类型、动态类型检查 :Python, Scheme
静态显式类型 :Java/C
静态隐式类型 :Ocaml, Haskell

4.类型系统的形式化

类型表达式,如:
int, intint, pointer(int)
定型规则,如:

x:int|-x+1:int

其中|-左侧的被称为定型环境 定型断言:

 |– M : int    |– N : int
 |– M + N : int 

其中形如

a < b    b < c
a < c

的式子我们称之为推理规则。
根据个人理解,定型断言可以看作建立在定型规则之上的一种推理规则。

3.类型检查(Type checking)

编译器对程序进行的检查包括语法检查、类型名变量名及函数名等先声明后引用的检查、类型检查以及其它检查。
我们在此讨论的为类型检查,其实现依赖类型系统。如,若a是long类型的数组,m是long类型,则编译器会发现m + “123”和a + 3.5都有类型错误。
关于类型系统,此处借用来自Wikipedia的定义如下[6]:

The process of verifying and enforcing the constraints of types—type checking—may occur either at compile-time (a static check) or at run-time.

其中提到的两种检查方式为:
Static type checking is the process of verifying the type safety of a program based on analysis of a program’s text (source code).
具体而言,Static type checking不运行被测程序本身,仅通过分析或检查源代码的语法、结构、过程、接口等来检查程序的正确性。静态方法通过程序静态特性的分析,找出欠缺和可疑之处,例如不匹配的参数、不适当的循环嵌套和分支嵌套、不允许的递归、未使用过的变量、空指针的引用和可疑的计算等
Dynamic type checking is the process of verifying the type safety of a program at runtime.
大部分类型安全(type-safe)的语言包含有一些形式的动态类型检测,即使他们同时具有a static type checker,原因在于,许多有用的动能对于静态检查实行起来非常困难。例如,假设有一个程序定义了两个类型A和B,并B是A的一个子类型。此时如果我们要将一个变量从A类型转换为B类型,即对此变量进行向下类型转换,此操作仅在此变量原来即为B类型时才是合法的。而,变量名指向的地址存储的内容只有在程序运行时才可以确定,因此,动态类型检测在判断此类操作是否安全时是必须的。
由此引申第三种类型——Combining static and dynamic type checking。即存在一些编程语言同时允许以上两种类型的检查。

4.c语言的类型缺陷

C语言为笔者唯一较为熟悉的语言,故以此为例。
C语言具有较为完备的静态类型系统,类型可以通过组合产生新的类型 。C的类型包括基本类型和构造类型两种(指针和函数可以看作构造类型)。但是C语言在类型系统上存在着一些不可忽视的缺陷。根据上文中我们介绍类型系统的时候给出的定义, C语言是一种弱类型语言,这会导致在编译过程中的检查是有一定缺陷的,也就有可能会导致程序运行时的安全问题。其中比较常见的不安全运算就是指针运算和类型强制转换。
我们在此处再次举出上课时用到的例子[7]:

typedef struct{int m;float a[];} record;
	record p={2,{1.0,2.0}},q={3,{1.0,2.0,3.0}};
	int main(){
		p=q;
		printf(“%d %f %f %f\n”,p.n,p.a[0],p.a[1],p.a[2]);
		printf(“%d %f %f %f\n”,q.n,q.a[0],q.a[1],q.a[2]);
      return 0;
	}

对于这样一段程序,经GCC: (Ubuntu/Linaro 4.6.3-1 ubuntu5)4.6.3编译后会出现以下结果:
3 1.000000 2.000000 3.000000
3432433534 1.000000 2.000000 3.000000
q.n被破坏的原因在于编译器分配连续的内存来存放结构体p和q的数据,导致p.a[1]和q.n的地址相邻,在执行过p=q之后, p.a[2]占用了q.n的地址,q.n的中存储的值被破坏。
又,下面是一个类型转换的例子
/一个初学C会遇到的一个摄氏度转换的算法
f为输入的温度,c为转化后的温度
/

main()
{
     float c,f;
     scanf("%f",&f);
     c=5*(f-32)/9;
}
//根据优先级问题,先算括号里面的,即f-32,由于f是float型,则f-32也是float型
//5和9都是int型,5*(f-32)就是int型与float的乘积,此时的int会自动转换为
//float型
//同理,再除9,最终c为float型。

但是实际中经常会出现的问题是,有些同学会把 c=5*(f-32)/9的公式改写成

c=5/9*(f-32)

即先用5/9再乘括号里的东西,这样的话输出结果会直接为0,因为5和9均为int型,由于int/int=in,所以5/9=0 ,0乘任何数都为0。这样就导致了错误。
弱类型语言与强类型语言并无法判别优劣。在实际工作中,使用哪种语言按需而定。对类型的弱化很多时候提高了语言的灵活性,在编写简单小应用时,使用弱类型语言可,节省很多代码量,有更高的开发效率。例如,C语言的类型系统存在缺陷,这种缺陷造成了C安全上的漏洞,但我们出于效率上的考虑允许了这种不足,所以C语言至今为仍最流行的语言之一。而对于构建大型项目,使用强类型语言可能会比使用弱类型更加规范可靠。