PL真有意思(五):数据类型

前言

如今大多数程序设计语言中都有表达式和/或对象的类型概念。类型起着两种主要做用:html

  • 为许多操做提供了隐含的上下文信息,使程序员能够在许多状况下没必要显示的描述这种上下文。好比int类型的两个对象相加就是整数相加、两个字符串类型的对象相加就是拼接字符串、在Java和C#中new object()隐含在背后的就是要分配内存返回对象的引用等等。python

  • 类型描述了其对象上一些合法的能够执行的操做集合。类型系统将不容许程序员去作一个字符和一个记录的加法。编译器可使用这个合法的集合进行错误检查,好的类型系统可以在实践中捕获不少错误程序员

类型系统

从编译方面的知识咱们能够知道,计算机硬件能够按多种不一样的方式去解释寄存器里的一组二进制位。处理器的不一样功能单元可能把一组二进制位解释为指令、地址、字符、各类长度的整数或者浮点数等。固然,二进制位自己是无类型的,对存储器的哪些位置应该如何解释,大部分硬件也无任何保留信息。汇编语言因为仅仅是对一些二进制指令的“助记符号”翻译,它也是这种无类型状况。高级语言中则老是关联值与其类型,须要这种关联的一些缘由和用途就如前面说到的上下文信息和错误检测。编程

通常来讲,一个类型系统包含一种定义类型并将它们与特定的语言结构关联的机制;以及一些关于类型等价、类型相容、类型推理的规则。 必须具备类型的结构就是那些能够有值的,或者能够引用具备值得对象的结构。类型等价规则肯定两个值得类型什么时候相同;类型相容规则肯定特定类型的值是否能够用在特定的上下文环境里;类型推理规则基于一个表达式的各部分组成部分的类型以及其外围上下文来肯定这个表达式的类型。数组

在一些多态性变量或参数的语言中,区分表达式(如一个名字)的类型与它所引用的那个对象的类型很是重要,由于同一个名字在不一样时刻有可能引用不一样类型的对象。安全

在一些语言中,子程序也是有类型的,若是子程序是一级或者二级值,其值是动态肯定的子程序,这时语言就须要经过类型信息,根据特定的子程序接口(即参数的个数和类型)提供给这种结构的可接受的值集合,那么子程序就必须具备类型信息。在那些不能动态建立子程序引用的静态做用域语言(这种语言中子程序是三级值),编译器时就能肯定一个名字所引用的子程序,所以不须要子程序具备类型就能够保证子程序的正确调用。markdown

类型检查

类型检查时一个处理过程,其目的就是保证程序遵循了语言的类型相容规则,违背这种规则的状况称为类型冲突。说一个语言是强类型的,那么就表示这个语言的实现遵循一种禁止把任何操做应用到不支持这种操做的类型对象上的规则。说一个语言是静态类型化(statically type)的,那么它就是强类型的,且全部的类型检查都能在编译时进行(现实中不多有语言是真正的静态类型,一般这一术语是指大部分类型检查能够在编译器执行,其他一小部分在运行时检查)。如C#咱们一般都认为它是静态类型化的语言。数据结构

动态(运行时)类型检查是迟约束的一种形式,把大部分的检查操做都推迟到运行的时候进行。采用动态做用域规则的语言大部分都是动态类型语言,由于它的名字和对象的引用都是在运行时肯定的,而肯定引用对象的类型则更是要在引用肯定以后才能作出的。架构

类型检查是把双刃剑,严格的类型检查会使编译器更早的发现一些程序上的错误,可是也会损失一部分灵活性;动态类型检查灵活性大大的,可是运行时的代价、错误的推迟检查,各类语言的实现也都在这种利弊上进行权衡。函数

多态性

多态性使得同一段代码体能够对多个类型的对象工做。它意味着可能须要运行时的动态检查,但也未必必定须要。在Lisp、Smalltalk以及一些脚本语言中,彻底的动态类型化容许程序员把任何操做应用于任何对象,只有到了运行时采起检查一个对象是否实现了具体的操做。因为对象的类型能够看做它们的一个隐式的(未明确声明的,一个不恰当的比喻就如C#中的this)参数,动态类型化也被说成是支持隐式的参数多态性。

虽然动态类型化具备强大的威力(灵活性),但却会带来很大的运行时开销,还会推迟错误报告。一些语言如ML采用了一种复杂的类型推理系统,设法经过静态类型化支持隐式的参数多态性。

在面向对象语言里,子类型多态性容许类型T的变量X引用了从T派生的任何类型的对象,因为派生类型一定支持基类型的全部操做,所以编译器彻底能够保证类型T的对象能接受的任何操做,X引用的对象也都能接受。对于简单的继承模型,子类型多态的类型检查就能彻底在编译时实现。采用了这种实现的大多数语言(如C++,JAVA和C#)都提供另外一种显示的参数化类型(泛型),容许程序员定义带有类型参数的类。泛型对于容器(集合)类型特别有用,如T的列表(List)和T的栈(Stack)等,其中T只是一个类型占位符,在初始化的这个容器对象时提供具体的类型来代替它。与子类型多态相似,泛型也能够在编译时完成类型检查。好比C++的模板彻底就是编译期间的东西,编译后就彻底没有了模板的痕迹;JAVA则是利用一种“擦除”的技术实现的泛型,须要在运行时作一些检查。

类型的含义

如今至少存在三种不一样的考虑类型问题的方式,分别称之为指称的、构造的和基于抽象的

  • 指称的

按照指称的观点,一个类型就是一组值,一个值具备某个类型的条件是他属于这个值集合,一个对象具备某个类型的条件是他的值保证属于这个值集合

  • 构造的

从构造的观点看,一个类型或者是以一小组内部类型,或者是经过对一个或几个更简单些的类型,应用某个类型的构造符构造出来的

  • 基于抽象的

从基于抽象的角度来看,一个类型就是一个接口,由一组定义良好并且具备相互协调的语义的操做组成。

类型的分类

在不一样语言里,有关类型的术语也不相同,这里说的一般都是经常使用的术语,大部分语言多提供的内部类型差很少就是大部分处理器所支持的类型:整数、字符、布尔和实数。

通常语言规范中都会规定数值类型的精度问题,以及一些字符的编码规定。一般特殊的一个数值类型是枚举类型,具体的语法在不一样的语言中略有差别,可是其也都是一个目的(用一个字符友好的表示一个数值)。

关于枚举类型,由一组命名元素组成。在C中能够这样写:

enum weekday { sun, mon, tue, wed, thu, fri, sat };
复制代码

在C中这样的写法和直接对里面的元素直接赋值除了语法上效果彻底同样。可是在以后的许多语言中,枚举类型是一个真正的类型

还有一些语言中提供一种称为子界的类型,它表示一种基于基本数值的一个连续的区间。好比Pascal中表示1到100:

type test_score = 0..100
复制代码

复合类型:由一些简单的基本类型组合成的一些类型称为复合类型,好比常见的记录、变体记录、数组、集合、指针、表等,具体的都会在后面详细介绍。

类型检查

大多数的静态类型语言中,定义一个对象都是须要描述清楚它的类型,进一步讲,这些对象出现的上下文也都是有类型的,也就是说语言中的一些规则限制了这种上下文中能够合法出现的对象类型。

类型相容肯定了一个特定类型的对象的可否用在一个特定上下文中。在最极端的状况下,对象可以使用的条件就是它的类型与上下文所指望的类型等价。可是在大多数语言中,相容关系都比等价更宽松一些,即便对象与上下文的类型不一样,它们也能够相容。

而类型推理想回答的是从一个简单的表达式出发构造另外一个表达式时,这整个的表达式的类型是什么

类型等价

在用户能够定义新类型的语言中,类型等价的定义通常基于两种形式。

type R2 = record
    a : integer
    b : integer
end;

type R2 = record
    b : integer
    a : integer
end;
复制代码
  • 结构等价

基于类型定义的内容,就是它们由一样的组成部分且按照一样的方式组合而成

它的准肯定义在不一样的语言中也不同,由于它们要决定类型之间的哪些潜在差别是重要的,哪些是能够接受的(好比上面的两个定义,是否还认为是等价的)。结构等价是一种很直接的认识类型的方式,早期的一些语言(Algol 6八、Modula-三、ML)有些事基于结构等价的,如今的大部分语言(Java、C#)大都是基于名字等价了,为什么呢?由于从某种意义上看,结构等价是由底层、由实现决定的,属于比较低级的思考方式。就如一个上下文,若是你传递了一个结构等价可是不是所期待对象,实施结构等价的编译器是不会拒绝这种状况的(假如这不是你但愿的,那么你也不会获得任何提示或者错误信息,很难排查的)。

  • 名字等价

基于类型的词法形式,能够认为是每个名字都引进一个新的类型;

它基于一种假设,就是说程序员花时间定义了两个类型,虽然它们的组成部分可能相同,可是程序员要表达的意思就是这是两个不一样的类型。名字等价的常规判断就很是简单了,看看声明两个对象的类型是不是一个就是了。可是也会有一些特殊的状况出现,好比类型别名(C、C++的程序员很熟悉这种东西吧),好比 typedef int Age; 就为int类型从新定义了一个别名"Age"。那些认为int不等价越Age的语言称为严格名字等价,认为等价的称为宽松名字等价。其实这两种也是很容易区分的,只要能区分声明和定义两个概念的差别就能够区分。在严格名字等价中看待typedef int Age是认为定义了一个新类型Age,在宽松名字等价看来这就是一个类型声明而已,int和Age共享同一个关于整数的定义。

类型变换和转换

在静态类型的语言中,若是“a=b”,那么咱们会指望b的类型和a的相同;如今假定所提供的类型和指望的类型和所提供的类型相同,那么咱们在要求某个类型的上下文中使用另一个类型时就须要显示的写出类型变换(或称为类型转换)。根据具体的变换的具体状况,在运行时执行这种变化会有如下三种主要的状况出现:

  • 所涉及的类型能够认为是结构等价的,这种状况里面由于涉及的类型采用了相同的底层的表示,则这种变换纯粹就是概念上的操做,不须要运行时执行任何代码。

  • 所涉及的类型具备不一样的值集合,但它们的值集合具备相同的表示形式。好比一个类型和它的子类型,一个整数和一个无符号的整数。拿无符号整数变换为整数来讲,因为无符号整数的最大值是整数类型所容纳不了的,则运行时就必须执行一些代码来保证这种变换的合法性,若是合法则继续下去,不然会产生一个动态语义错误。

  • 所涉及的类型具备不一样的底层表示,可是咱们能够在它们的值之间定义某种对应关系。好比32位整数能够变换到IEEE的双精度浮点数,且不会丢失精度。浮点数也能够经过舍入或割断的形式变换成整数,可是会丢失小数部分。

非变换的类型转换

有这么一种状况,咱们须要改变一个值,可是不须要改变它的二进制表示形式,更通俗点说就是咱们但愿按照另一个类型的方式去解释某个类型的二进制位,这种状况称为非变换类型转换。最简单的一个例子好比说,一个byte类型的数值65,按byte类型来解释它是65,若是按照char类型来解释它就是字符“A”。好比C++中的static_cast执行类型变换,reinterpret_cast执行非变换的类型转换。c中出现的union形式的结构,就能够认为是这种非变换的类型转换的合法的安全的语言结构。在好比下面C中通常性非变换类型转换代码:

r=*((float *) &n);
复制代码

任何非变换的类型转换都极其危险的颠覆了语言的类型系统。在弱类型系统的语言中,这种颠覆可能很难发现,在强类型系统的语言中显示的使用这种非变换的类型转换,起码从代码上能够看得出来它是这么一回事,或多或少的有利于排查问题。

类型相容

大多数语言的上下文中并不要求类型等价,相应的通常都是实施较为“宽松”的类型相容规则。好比赋值语句要求右值相容与左值、参数类型相容,实际返回类型与指定的返回类型相容。在语言中,只要容许把一个类型的值用到指望的另一个类型的上下文中,语言都必须执行一个到所指望类型的自动隐式变换,称为类型强制(好比int b;double a=b;)。就像前面说的显示的类型变换同样,隐式的类型变换也可能须要执行底层代码或者作一些动态类型检查。

重载

一个重载的名字可能引用不一样类型的对象,这种歧义性须要经过上下文信息进行解析。好比a+b这个表达式能够表示整数或者浮点数的加法运算,在没有强制的语言中,a和b必须都是整数或都是浮点数。若是是有强制的语言,那么在a或者b有一个是浮点数的状况下,编译器就必须使用浮点数的加法运算(另一个整数强制转换为浮点数)。若是语言中+只是进行浮点数运算,那么即便a和b都是整数,也会被所有转成浮点数进行运算(这代价就高了好多了)。

通用引用类型

通用引用类型:一些语言根据实习需求,设计有通用的引用类型,好比C中的void*、C#中的Object,任意的值均可以赋值给通用引用类型的对象。可是问题是存进去容易取出来难,当通用引用类型是右值的时候,左值的类型可能支持某些操做,然而这些操做右值对象是不具有的。为了保证通用类型到具体类型的赋值安全,一种解决办法是让对象能够自描述(也就是这个对象包含其真实类型的描述信息),C++,JAVA,C#都是这种方式,C#中若是赋值的类型不匹配则会抛出异常,而C++则是使用dynamic_cast作这种赋值操做,具体的后果呢,也是C++程序员负责。

类型推理

经过前面的类型检查咱们能够保证表达式的各各组成部分具备合适的类型,那么这整个表达式的类型是什么来着?其实在大多数的语言中也是比较简单的,算术表达式的类型与运算对象相同、比较表达式老是布尔类型、函数调用的结果在函数头声明、赋值结果就是其左值的类型。在一些特殊的数据类型中,这个问题并非那么清晰明了,好比子界类型、复合类型。好比下面的子界类型问题(Pascal):

type Atype=0..20;
type Btype=10..20;

var a: Atype;
var b: Btype;
复制代码

那么a+b什么类型呢???它确实是不能是Atype或者Btype类型,由于它可能的结果是10-40。有人以为那就新构造一个匿名的子界类型,边界时10到40。实际状况是Pascal给的答案是它的基础类型,也就是整数。

在Pascal中,字符串'abc'的类型是array[1..3] of char、而Ada则认为是一种未彻底肯定的类型,该类型与任何3个字符数组相容,好比在Ada中'abc' & 'defg'其结果是一个7字符的数组,那么这个7字符数组的类型是array[1..7] of cahr呢仍是某一个也是7个字符组成的类型array (weekday) of character呢,更或者是其余任意一个也是包含七个字符数组的另一个类型。这种状况就必须依赖表达式所处的上下文信息才能推到出来具体的类型来。

记录(结构)与变体(联合)

一些语言中称记录为结构(struct),好比C语言。C++把结构定义为class的一种特殊形式(成员默认全局可见),Java中没有struct的概念,而C#则对struct采用值模型,对class采用引用模型。

语法与运算

一个简单的结构体在C中能够这样定义:

struct element{
    char name[2];
    int number;
    double weight;
    Bool merallic;    
}; 
复制代码

等价于Pascal中的:

type two_chars=packed array [1..2] of char;
 type element - record
     name:two_chars;
     number:integer;
     weight:real;
     metallic:Boolean
 end
复制代码

记录里面的成员(如name,number...)称为域(field)。在须要引用记录中的域时,大部分语言使用“.”记法形式。好比Pascal中:

var copper:eement;
 copper.name=6.34;
复制代码

大部分语言中还容许记录的嵌套定义,好比在Pascal中:

type short_string=packed array[1..30] of char;
 type ore=record
      name:short_string;
      element_yielded:record /*嵌套的记录定义*/
          name:two_chars;
          number:integer;
          weight:real;
          metallic:Boolean
      end
 end
复制代码

存储布局及其影响

一个记录的各个域一般被放入内存中的相邻位置。编译器在符号表中保存每一个域的偏移量,装载和保存的时候经过基址寄存器和偏移量便可获得域的内存地址。类型element在32位的机器中可能的布局以下:

此处有图

(图在最后面,由于markdown的这个画表格不符合这个要求,又不想引图了,就直接用html写了,会被挤到最后去)

在对结构体的存储布局方案上,若是使用正常排序,结构中的空洞会浪费空间。可是若是经过压缩来节省空间,可是可能很带来很严重的访问时间的代价

数组

数组是最多见也是最重要的复合数据类型。记录用于组合一些不一样类型的域在一块儿;而数组则不一样,它们老是同质的。从语义上看,能够把数组想象成从一个下标类型到成员(元素)类型的映射。

有些语言要求下标类型必须是integer,也有许多语言容许任何离散类型做为下标;有些语言要求数组的元素类型只能是标量,而大多数语言则容许任意类型的元素类型。也有一些语言容许非离散类型的下标,这样产生的关联数组只能经过散列表的方式实现,而没法使用高效的连续位置方式存储,好比C++中的map,C#中的Dictionary。在本节中的讨论中咱们假定数组的下标是离散的。

语法和操做

大多数的语言都经过数组名后附加下标的方式(圆括号|方括号)来引用数组里的元素。因为圆括号()通常用于界定子程序调用的实际参数,方括号在区分这两种状况则有易读的优点。Fortran的数组用圆括号,是由于当时IBM的打卡片机器上没有方括号

维数、上下界和分配

对于数组的形状在声明中就已经描述,对于这种有静态形状的数组,能够用一般的方式来管理内存:生存期是整个程序的数组使用栈分配,具备更通常的生存期的动态生成数组使用堆分配。可是对于在加工以前不知道其形状的数组,或其形状在执行期间可能改变的数组,存储管理就会更复杂一点。

  • 内情向量

在编译期间,符号表维护者程序中的每一个数组的维度和边界信息。对于每一个记录,它还维护着每一个域的偏移量。若是数组维度的数目和边界是静态已知的,编译器就能够在符号表中找出它们,以便计算数组元素的地址。若是这些值不是静态已知的,则编译器就必须生成代码,在运行时从一个叫内情向量的数据结构来查找它

  • 栈分配

子程序参数是动态形状数组最简单的例子,其中数组的上下界在运行时才肯定,调用方都会传递数组的数据和一个适当的内情向量,可是若是一个数组的形状只能到加工时才知道,这种状况下仍能够在子程序的栈帧里为数组分配空间,可是须要多作一层操做

  • 堆分配

在任意时间均可以改变形状的数组,有时被称为是彻底动态的。由于大小的变化不会以先进先出的顺序进行,因此栈分配就不够用了。彻底动态的数组必须在堆中分配。好比Java中的ArrayList

内存布局

大多数语言的实现里,一个数组都存放在内存的一批连续地址中,好比第二个元素紧挨着第一个,第三个紧挨着第二个元素。对于多维数组而言,则是一个矩阵,会出现行优先和列优先的选择题,这种选择题对于语言使用者而言是透明的,而对语言的实现者则须要考虑底层方面的优化问题了。

在一些语言中,还有另一种方式,对于数组再也不用连续地址分配,也不要求各行连续存放,而是容许放置在内存的任何地方,再建立一个指向各元素的辅助指针数组,若是数组的维数多于两维,就再分配一个指向指针数组的指针数组。这种方式称为行指针布局,这种方式须要更多的内存空间,可是却有两个优势:

  • 首先,可能加快访问数组里单独元素的速度;
  • 其次,容许建立不用长度的行,并且不须要再各行的最后留下对齐所用的空洞空间,这样节省下来的空间有时候可能会超过指针占据的空间。C,C++和C#都支持连续方式或行指针方式组织多维数组,从技术上讲,连续布局才是真正的多维数组,而行指针方式则只是指向数组的指针数组。

字符串

许多语言中,字符串也就是字符的数组。而在另外一些语言中,字符串的状况特殊,容许对它们作一些其余数组不能用的操做,好比Icon以及一些脚本语言中就有强大的字符串操做功能。

字符串是编程中很是重要的一个数据类型,故而不少语言都对字符串有特殊的处理以便优化其性能以及存储(好比C#中的字符串不可变性保证了性能,字符串驻留技术照顾了存储方面的须要),因为这些特殊的处理,故而各各语言中为字符串提供的操做集合严重依赖语言设计者对于实现的考虑。

集合

程序设计语言中的一个集合,也就是具备某个公共类型的任意数目的一组值的一种无序聚集。集合的元素所具备的类型叫作元类型或者基类型。如今的大多数程序设计语言都对集合提供了很大的支持,为集合提供了不少相关的操做

指针和递归类型

所谓的递归类型,就是能够在其对象中包含一个或多个本类型对象的引用类型。递归类型用于构造各类各样的“连接”数据结构,好比树。在一些对变量采用引用模型的语言中,很容易在建立这种递归类型,由于每一个变量都是引用;在一些对变量采用值模型的语言中,定义递归类型就须要使用指针的概念,指针就是一种变量,其值是对其余对象的引用。

对于任何容许在堆里分配新对象的语言,都存在一个问题:若这种对象不在须要了,什么时候以及以何种方式收回对象占用的空间?对于那些活动时间很短的程序,让不用的存储留在那里,可能还能够接受,毕竟在它不活动时系统会负责回收它所使用的任何空间。可是大部分状况下,不用的对象都必须回收,以便腾出空间,若是一个程序不能把再也不使用的对象存储回收,咱们就认为它存在“内存泄漏”。若是这种程序运行很长一段时间,那么它可能就会用完全部的空间而崩溃。许多早期的语言要求程序员显示的回收空间,如C,C++等,另外一些语言则要求语言实现自动回收再也不使用的对象,如Java,C#以及全部的函数式语言和脚本语言。显示的存储回收能够简化语言的实现,但会增长程序员忘记回收再也不使用的对象(形成内存泄漏),或者不当的回收了不应回收的正在使用的对象(形成悬空引用)的可能性。自动回收能够大大简化程序员的工做,可是为语言的实现带来了复杂度。

语法和操做

对指针的操做包括堆中对象的分配和释放,对指针间接操做以访问被它们所指的对象,以及用一个指针给另外一个指针赋值。这些操做的行为高度依赖于语言是函数式仍是命令式,以及变量/名字使用的是引用模型仍是值模型。

函数式语言通常对名字采用某种引用模型(纯的函数式语言里根本没有变量和赋值)。函数式语言里的对象倾向于采起根据须要自动分配的方式。

命令式语言里的变量可能采用值模型或引用模型,有时是二者的某种组合。好比 A=B;

  • 值模型: 把B的值放入A。
  • 引用模型: 使A去引用B所引用的那个对象。

Java的实现方式区分了内部类型和用户定义的类型,对内部类型采用值模型,对用户定义的类型采用则采用引用模型,C#的默认方式与Java相似,另外还提供一些附加的语言特性,好比“unsafe”可让程序员在程序中使用指针。

悬空引用

在前两篇的名字、做用域和约束中咱们列举了对象的3种存储类别:静态、栈和堆。静态对象在程序的执行期间始终是活动的,栈对象在它们的声明所在的子程序执行期间是活动的,而堆对象则没有明肯定义活动时间。

在对象不在活动时,长时间运行的程序就须要回收该对象的空间,栈对象的回收将做为子程序调用序列的一部分被自动执行。而在堆中的对象,由程序员或者语言的自动回收机制负责建立或者释放,那么若是一个活动的指针并无引用合法的活动对象,这种状况就是悬空引用。好比程序员显示的释放了仍有指针引用着的对象,就会形成悬空指针,再进一步假设,这个悬空指针原来指向的位置被其余的数据存放进去了,可是实际却不是这个悬空指针该指向的数据,若是对此存储位置的数据进行操做,就会破坏正常的程序数据。

那么如何从语言层面应对这种问题呢?Algol 68的作法是禁止任何指针指向生存周期短于这个指针自己的对象,不幸的是这条规则很难贯彻执行。由于因为指针和被指对象均可能做为子程序的参数传递,只有在全部引用参数都带有隐含的生存周期信息的状况下,才有可能动态的去执行这种规则的检查。

废料收集

对程序员而已,显示释放堆对象是很沉重的负担,也是程序出错的主要根源之一,为了追踪对象的生存轨迹所需的代码,会致使程序更难设计、实现,也更难维护。一种颇有吸引力的方案就是让语言在实现层面去处理这个问题。随着时间的推移,自动废料收集回收都快成了大多数新生语言的标配了,虽然它的有很高的代价,但也消除了去检查悬空引用的必要性了。关于这方面的争执集中在两方:以方便和安全为主的一方,以性能为主的另外一方。这也说明了一件事,编程中的不少地方的设计,架构等等方面都是在现实中作出权衡。

废料收集通常有这两种思想,就不详细说了。

  • 引用计算
  • 追溯式收集

表具备递归定义的结构,它或者是空表,或者是一个有序对,有序对由一个对象和另外一个表组成。表对于函数式或者逻辑式语言程序设计很是适用,由于那里的大多数工做都是经过递归函数或高阶函数来完成的。

在Lisp中:

(cons 'a '(b))  => (a b)
(car '(a b))    => a
(cdr '(a b c))  => (b c)
复制代码

在Haskell和Python还由一个很是有用的功能,叫作列表推导。在Python中能够这样推导出一个列表

[i * i for i in range(1, 100) if i % 2 == 1]
复制代码

文件和输入/输出

输入/输出(I/O)功能使程序能够与外部世界通讯。在讨论这种通讯时,将交互式I/O和文件I/O分开可能有些帮助。交互式IO一般意味着与人或物理设备通讯,人或设备都与运行着的程序并行工做,送给程序的输入可能依赖程序在此以前的输出。文件一般对应于程序的地址空间以外的存储器,由操做系统实现。

有些语言提供了内置的File数据类型,另一些语言将IO工做彻底委托给库程序包,这些程序包导出一个file类型。因此IO也算做是一种数据类型

相等检测和赋值

对于简单的基本数据类型,如整数、浮点数和字符,相等检测和赋值相对来讲都是直截了当的操做。其语义和实现也很明确,能够直接按照二进制位方式比较或复制,可是,对于更加复杂或抽象的数据类型,就可能还须要其它的比较方式

  • 相互是别名?
  • 二进制位是否都相等?
  • 包含一样的字符序列?
  • 若是打印出来,看起来彻底同样?

就许多状况下,当存在引用的状况下,只有两个表达式引用相同的对象时它们才相等,这种称为浅比较。而对于引用的对象自己存在相等的含义时,这种比较称为深比较。对于复杂的数据结构,进行深比较可能要进行递归的遍历。因此相对来讲,赋值也有深浅之分。深赋值时是进行完整的拷贝。

大多数的语言都使用浅比较和浅赋值

小结

本文从语言为什么须要类型系统出发,解释了类型系统为语言提供了那些有价值的用途:1是为许多操做提供隐含的上下文,使程序员在许多状况下没必要显示的描述这种上下文;2是使得编译器能够捕捉更普遍的各类各样的程序错误。 而后介绍了类型系统的三个重要规则:类型等价、类型相容、类型推理。以此3个规则推导出的强类型(毫不容许把任何操做应用到不支持该操做的对象上)、弱类型以及静态类型化(在编译阶段贯彻实施强类型的性质)、动态类型化的性质以及在对语言的使用方面的影响。以及后续介绍了语言中常见的一些数据类型的用途以及语言在实现这种类型方面所遇到的问题以及其大体的实现方式。

4 byte/32bits
name(2个字节) 2个字节的空洞
number(4个字节)
weight
(8个字节)
metallic(1个字节) 3个字节的空洞
相关文章
相关标签/搜索