语言设计之数据类型

  大多数程序设计语言中都有表达式和/或对象的类型概念。类型起着两种主要的作用:1)类型为许多操作提供了隐含的上下文环境,使程序员不必显示地描述这种环境。例如在c++、java和c#中,操作new my_type()不仅为类型my_type的对象分配大小合适的存储块,还自动的调用与此类型相关的用户定义的初始化函数。2)类型限制了语义上合法的程序中可以执行的操作集合。例如,类型将不允许程序员去做一个字符和一个记录的加法。

  1 类型系统

  计算机硬件可以按多种不同的方式来解释存储器中的一组二进制位:解释为指令、地址、字符以及各种长度的整数或浮点数。机器语言和汇编语言是无类型的,任何种类的操作都可以应用于任何位置的值。但在另一方面,高级语言却几乎总是将值与类型联系在一起,以提供上面提到过的上下文信息和错误检查。

  非形式的说,一个类型系统包括1)一种定义类型并将它们与特定的语言结构相关联的一种机制;2)一组有关类型等价类型相容类型推理的规则。必须具有类型的结构也就是那些幼稚的或者可以引用具有值的对象的结构。这样的结构包括:命名常量、变量、记录与、参数,还有在某些情况下的子程序;具有字面意义的常量;以及包含这些结构的更复杂的表达式(类?)。类型等价规则确定两个值的类型何时相同;类型相容规则确定给定类型的值是否可以用在特定的上下文环境中。类型推理规则基于一个表达式的各组成部分的类型及其(有时)上下文,确定这个表达式的类型。在一些多态性变量或者参数语言中,区分引用或指针的类型与它所引用的那个对象的类型是非常重要的,因为同一给定名字在不同时刻可能引用着不同类型的对象。

  在某些语言中子程序也被认为是有类型的。

  1.1 类型检查

  类型检查是一个处理过程,其目标是保证程序遵守了语言的类型相容规则,违背这种规则的情况称为类型冲突。如果一个语言的实现能贯彻一套规则,禁止将任何操作应用到并不准备支持这个操作的对象上,就称这个语言是强类型的。如果一个语言是强类型的,而且所有的类型检查都能在编译时执行,就称它为静态类型化的从严格意义上说很少有语言是静态类型化的。在实践中,人们常用这个术语指那些大多数类型检查可以再编译时执行,其余检查则在运行中执行的语言。动态(运行时)类型检查是迟约束的一种形式,在那些将其他事项也推迟到运行时进行的语言中常常能看到这种检查方式。

  1.2 多态性

  多态性使得同一段代码体可以用于多个类型的对象。它意味着可能需要运行时的动态检查,但也未必一定需要。由于对象的类型可以看做它们的一个隐式参数,动态类型化也被说成是支持yinshi

  虽然动态类型化威力强大而且直接了当,但却会造成很大的运行时开销,还会推迟错误报告。ML及其后续语言使用了一种复杂的类型推理系统,设法通过静态类型化来支持隐式的参数多态性。动态类型化当然会带来一些运行时开销,会推迟错误报告,但越来越多的人觉得,与人的生产率潜在提高相比,这些并不那么重要。在静态类型化与动态类型化之间的选择,会是一个有趣的语言争论中的一个话题。

  1.3 "类型“的含义

  在大多数语言中使用者都必须明确声明每个对象的类型,还需要声明每个费内部类型的特征。至少存在三种考虑类型的方式:1)指称的:一个类型就是一组值。一个值具有某种类型的条件是它属于这个值集合,一个对象具有某个类型的条件是它的值保证属于这个值集合。2)构造的:一个类型或者是一小组内部类型(基本类型)之一;或者是通过一个或几个更简单些的类型,应用某个类型构造符(record、array、set等)创建出来的复合类型。3)基于抽象:(这是现代面向对象语言的标志性特征)一个类型就是一个接口,由一组定义良好而且具有相互协调的语义操作组成。对于大多数程序员来说,类型概念通常反映的是这几种观点的混合。

  1.4 类型的分类

  c语言没有bool类型,在大多数语言要求使用布尔值的地方,c语言用0代表false,其它非0值代表true。

  数值类型:有些语言区分了不同长度的整数和实数,但大部分语言没有都这么做,而是将选择精度的问题留给了实现。不幸的是,各语言实现使用的精度不同会破坏可移植性:在一个系统上能正确运行的程序,在另一个系统就可能产生运行时错误或者错误结果。java 和c#的不同寻常之处在于提供了几种长度的数值类型,而且规定了每种类型的长度。也有少数语言(c、c++、c#、Modula-2)同时提供了带符号数和无符号整数。还有几种语言提供了内部复数类型。另一些语言则用标准库支持复数。整数、布尔和字符都属于离散类型,他们对应的论域是可数的,除了最后一个元素和第一个元素之外,每个元素都有一个前驱和后继元素。用户定义类型中的枚举和子界也是离散的。离散类型有理数类型实数复数类型统称为标量类型,标量类型有时被称为简单类型

  枚举类型:一个枚举类型由一组命名元素组成。枚举类型的值是排序的,因此可以合法的作比较。代替枚举类型的一种方式就是简单的声明一组常量。在c中,这两种方式的区别完全是语法上的。然而在pascal和它的大多数后继语言中:枚举是一个真正的类型,与整数类型并不相容。在期望整数或枚举的上下文中使用另一个东西,编译时就会出现类型冲突。

  子界类型:一个子界类型是一个类型,其值有某离散的基类型(也称父类型)的某个连续子集组成。

  复合类型非标量类型通常称为复合类型或者结构类型。常见复合类型包括:记录(结构)、变体记录(联合)、数组、集合、指针、表和文件

  2 类型检查

  2.1 类型等价

  在用户可以定义新类型的语言中,类型等价性的定义存在两种基本方式。结构等价基于类型定义的内容,粗略的说,两个类型相同的条件是他们由相同的成分,按照相同的方式组合而成。名字等价则基于类型定义的词法出现方式:粗略地说,每个定义都引入一个新的类型。结构等价是一种很直接的认识类型的方式,但在某种意义上是低级的、由实现决定的方式。其主要问题在于,有可能无法区分程序员认为应该是不同的,但恰好又具有相同内部结构的类型。使用结构等价的编译器可能轻率的接受结构相同,名字不同的类型的变量之间的赋值。名字等价是基于另一种假设:如果程序员花时间写出两个类型定义,那么这两个定义可能就是想表示两个不同的类型。

  那些认为别名类型互不相同的语言被称为使用了严格名字等价,而那些认为别名类型等价的语言被称为使用的是宽松名字等价(将type  a = b视为一种声明。如果有分别编译存在, 结构等价和名字等价的实现就会出现一些很难处理的问题。

  现在假定语言中所期望的和所提供的类型完全相同。这样一来,如果程序员想在要求某个类型的上下文中使用另一个类型的值,就需要显式地写出类型变换(类型转换)。根据所涉及变换的不同,这种变换在运行时可能需要也可能不需要实际执行代码。主要有三种情况:1)所涉及的类型可以认为是结构等价,但语言使用的是名字等价。这种情况下,所涉及的类型使用了相同的底层表示,具有同样的一组值。这种变换纯粹是概念上的操作,运行时完全不需要执行代码。2)所涉及的类型具有不同的值集合,但他们所共有的值具有相同的表示方式。例如一个类型可能是另一个类型的子界,如果所提供的类型具有某些期望类型中没有的值,那么运行时就必须执行一些代码,以保证当前的值确实是期望类型的合法值。3)所涉及的类型具有不同的底层表示,但是我们可以在它们的值之间定义某种对应关系。

  c及其后续语言对任何运算都不检查算术溢出。

  改变类型但并不修改基础二进制位的情况称为非变换的类型转换

  2.2 类型相容性

  在大多数语言中,并不是每个上下文都要求类型等价,相反,它们只是要求一个值的类型必须与它所出现的上下文相容。在不同语言中,类型相容的定义也很不一样。

  只要一种语言允许将一个类型的值用到期望另一个类型的上下文中,该语言的实现都必须执行一个到期望类型的自动隐式转换,这种变换称为类型强制。强制的存在将允许类型的混合使用,不需要程序员明确表明自己的意图,这实际上将极大地削弱类型安全性。

  为了系统程序设计的需要,或者为了写出通用的容器对象,使其中可以保存任意类型的引用,一些语言提供了”通用引用“类型。在c和c++中,这种类型是void *。在面向对象的语言中,有关如何保证从通用类型到专门类型的赋值合法性的问题,成为一个如何保证任意赋值的合法性的普遍问题。一种方式是让对象具有自我描述,也就是说在每个对象的表示中包含关于其类型的指示符。这种方法在面向对象语言中使用的很普遍,这些语言通常都需要它来做动态方法约束。对象中的类型标识可能耗费客观的空间,但它们使实现能够防止将一种类型的对象赋给另一种类型的变量。在Java和c#中,从通用类型到专门类型的赋值需要做类型强制,如果通用引用所引用的不是强制的目标类型对象,就会产生一个异常。

  2.2 类型推理

  对简单的算术类型,当一个或更多的运算对象具有子界类型时,基本类型系统就出现了一个很难解决的问题。在一些语言中,对子界类型算术运算的结果是该子界的基类型(父类型)。

  如果将一个运算应用于复合值产生出的结果具有不同于运算对象的类型,类型推理就可能遇到问题。

  3 记录(结构)和变体(联合)

  3.1 语法和运算

  c语言:struct element {

     ...

      };

  3.2 存储布局及其影响

  一个记录的各个域通常被存入内存中的一些相邻位置。编译器在其符号表中保存着每个记录类型中各个域的偏移量。如果需要访问一个域,编译器通常会生成一条使用位移寻址的加载或存储指令。对于局部对象,基址寄存器中是帧指针,所用的位移值都是被访问的记录相对于寄存器的偏移量与这个域在记录内部的偏移量之和。在RISC机器上,全局记录访问方式类似。在CISC机器上,编译器可以用绝对地址值直接访问域,或者余姚更短时间内访问许多域,编译器可以加载一个具有记录的绝对地址的临时寄存器,然后使用域的位移作为偏移量。

  在大多数机器中,记录的成员所占的空间会进行长字节对齐。在内存空间上会出现空洞。在一些语言中,允许程序员将记录类型描述为压缩的。优化空间但是访问速度会下降。

  在实践中, 联合的主要用于两个目的。1)第一个目的来自于系统程序,联合使得同一组字节在不同时刻可以按不同的方式解释。2)为了表示一个记录中可选择的域集。

  4 数组

  7指针和递归类型

  递归类型:在其对象中间包含了一个或多个对于本类型对象的引用的类型。大多数递归类型都是记录,因为在递归类型中还需要包含除对于本类的引用之外的其他东西,而这实际上隐含这需要有不同类型的域。递归类型被用于构造各种各样的”链接“数据结构,包括表和树等。

  在使用变量的引用模型的语言中,很容易在类型foo的记录中包含对另一个类型为foo的记录的引用,因为每个变量都是引用。在使用变量的值模型的语言中,定义递归类型就需要有指针概念,指针就是一种变量,其值是对其他对象的引用。

  

  

 

  

posted @ 2021-04-25 16:31  应是良辰好景虚设  阅读(237)  评论(0)    收藏  举报