Data Abstraction and Object Orientation

Object-Oriented Programming

he abstraction provided by modules and module types has at least three important benefits:

  1. It reduces conceptual load by minimizing the amount of detail that the programmer must think about at one time.
  2. It provides fault containment by preventing the programmer from using a program component in inappropriate ways, and by limiting the portion of a program’s text in which a given component can be used, thereby limiting the portion that must be considered when searching for the cause of a bug.
  3. It provides a significant degree of independence among program components, making it easier to assign their construction to separate individuals, to modify their internal implementations without changing external code that uses them, or to install them in a library where they can be used by other programs.

Object-oriented programming can be seen as an attempt to enhance opportunities for code reuse by making it easy to define new abstractions as extensions or refinements of existing abstractions.

  • Public and Private Members
  • Tiny Subroutines(get set)
  • Derived Classes
  • General-Purpose Base Classes
  • Overloaded Constructors
  • Modifying Base Class Methods
  • Containers/Collections

Encapsulation and Inheritance(封装与继承)

Encapsulation mechanisms enable the programmer to group data and the subroutines that operate on them together in one place, and to hide irrelevant details from the users of an abstraction.

Modules

When the header and body of a module appear in separate files, a change to a module body never requires us to recompile any of the module’s users.

Classes

With the introduction of inheritance,object-oriented languages must supplement the scope rules of module-based languages to cover additional issues.

类之间的可见性讨论

Nesting (Inner Classes)

内部类的可见性?

Initialization and Finalization

Several important issues arise:

  • Choosing a constructor
  • References and values
  • Execution order
  • Garbage collection

Dynamic Method Binding

动态方法绑定会造成开销,动态和静态每个语言的选择不一样,Java选择可以用final来固定一些方法。

Unfortunately, as we shall see in Section 9.4.3, dynamic method binding imposes run-time overhead.While this overhead is generally modest,it is nonetheless a concern for small subroutines in performance-critical applications. Smalltalk, Objective-C, Modula-3, Python, and Ruby use dynamic method binding for all methods. Java and Eiffel use dynamic method binding by default, but allow individual methods and (in Java) classes to be labeled final (Java) or frozen (Eiffel), in which case they cannot be overridden by derived classes, and can therefore employ an optimized implementation.

  • Virtual and Nonvirtual Methods
  • Abstract Classes
  • Member Lookup
  • Polymorphism
  • Object Closures

Multiple Inheritance(多重继承)

Object-Oriented Programming Revisited

we characterized object-oriented programming in terms of three fundamental concepts: encapsulation, inheritance, and dynamic method binding.

  • Encapsulation allows the implementation details of an abstraction to be hidden behind a simple interface.
  • Inheritance allows a new abstraction to be defined as an extension or refinement of some existing abstraction,obtaining some or all of its characteristics automatically.
  • Dynamic method binding allows the new abstraction to display its new behavior even when used in a context that expects the old abstraction.

Summary and Concluding Remarks

这是我们关于语言设计的五个核心章节中的最后一个:名称(第3章)、控制流(第6章)、类型(第7章)、子程序(第8章)和对象(第9章)。

我们在第9.1节开始,通过确定面向对象编程的三个基本概念:封装、继承和动态方法绑定。我们还介绍了类、对象和方法的术语。我们已经在第3章的模块中看到了封装。封装允许复杂数据抽象的细节被隐藏在比较简单的接口后面。继承通过使程序员能够轻松定义新的抽象作为现有抽象的细化或扩展,扩展了封装的实用性。继承为多态子程序提供了一个自然的基础:如果一个子程序期望一个给定类的实例作为参数,那么可以使用从预期类派生的任何类的对象来代替(假设它保留了整个现有接口)。动态方法绑定通过安排对参数方法之一的调用在运行时使用与实际对象的类相关联的实现,而不是与参数的声明类相关联的实现,扩展了这种形式的多态性。我们注意到,包括Modula-3、Oberon、Ada 95和Fortran 2003在内的一些语言,通过一种类型扩展机制支持面向对象,其中封装与模块相关联,但继承和动态方法绑定与一种特殊形式的记录相关联。

在后续章节中,我们详细讨论了对象的初始化和终结、动态方法绑定以及(在PLP CD上的)多重继承。在许多情况下,我们发现功能性与一方面的简洁性和执行速度之间存在权衡。将变量视为引用而非值,通常会导致更简单的语义,但需要额外的间接性。垃圾收集,如第7.7.3节前所述,极大地简化了软件的创建和维护,但带来了运行时的成本。动态方法绑定通常要求(在一般情况下)通过vtables或其他查找机制来分派方法。多重继承的简单实现即使未使用也会带来开销。

在多个案例中,我们也看到了时间/空间的权衡。如第8.2.4节先前提到的,内联子程序可以显著提高代码性能,这不仅是通过消除子程序调用本身的开销,还允许寄存器分配、公共子表达式分析以及其他“全局”代码改进在调用间应用。同时,内联扩展通常会增加对象代码的大小。练习9.28和9.30探讨了在实现多重继承中的类似权衡。

尽管Smalltalk缺乏多重继承,但它仍被广泛认为是最纯粹和最灵活的面向对象语言之一。然而,它缺乏编译时类型检查,加上其“基于消息”的计算模型以及对动态方法查找的需求,使得其实现相对较慢。C++,具有对象值变量、默认的静态绑定、最小的动态检查和高质量的编译器,很大程度上负责面向对象编程日益增长的普及性。可靠性、可维护性和代码重用的改进,可能会也可能不会证明Smalltalk的高性能开销是合理的。它们几乎肯定证明了C++的相对适度开销,可能也证明了Eiffel略高的开销。随着软件系统规模的不断增加,互联网上分布式计算的爆炸性增长,以及高度可移植的面向对象语言(Java)、面向对象的脚本语言(Python、Ruby、PHP、JavaScript)和二进制对象标准(.NET [WHA03]、CORBA [Sie96]、JavaBeans [Sun97])的开发,面向对象编程将在21世纪的计算中显然扮演核心角色。

Subroutines and Control Abstraction

Subroutines are the principal mechanism for control abstraction in most programming languages.

  • A subroutine that returns a value is usually called a function.
  • A subroutine that does not return a value is usually called a procedure.

Review of Stack Layout(栈内存布局)

the stack pointer register contains the address of either the last used location at the top of the stack, or the first unused location

The frame pointer register contains an address within the frame.

Calling Sequences

Maintenance of the subroutine call stack is the responsibility of the calling sequence

Tasks that must be accomplished on the way into a subroutine include:

  • passing parameters
  • saving the return address
  • changing the program counter
  • changing the stack pointer to allocate space
  • saving registers (including the frame pointer) that contain important values and that may be overwritten by the callee
  • changing the frame pointer to refer to the new frame
  • executing initialization code for any objects in the new frame that require it

Tasks that must be accomplished on the way out include:

  • passing return parameters or function values
  • executing finalization code for any local objects that require it
  • deallocating the stack frame (restoring the stack pointer)
  • restoring other saved registers (including the frame pointer)
  • restoring the program counter
Saving and Restoring Registers

函数调用最棘手的部分就是保存和恢复寄存器。

Perhaps the trickiest division-of-labor issue pertains to saving registers.

A simpler solution is for the caller to save all registers that are in use, or for the callee to save all registers that it will overwrite.

调用约定:

strike something of a compromise: registers not reserved for special purposes are divided into two sets of approximately equal size. One set is the caller’s responsibility, the other is the callee’s responsibility. A callee can assume that there is nothing of value in any of the registers in the caller-saves set; a caller can assume that no callee will destroy the contents of any registers in the callee-saves set.

Maintaining the Static Chain

In languages with nested subroutines,at least part of the work required to maintain the static chain must be performed by the caller,rather than the callee,because this work depends on the lexical nesting depth of the caller.

A Typical Calling Sequence

The caller:

  1. saves any caller-saves registers whose values will be needed after the call
  2. computes the values of arguments and moves them into the stack or registers
  3. computes the static link (if this is a language with nested subroutines), and passes it as an extra, hidden argument
  4. uses a special subroutine call instruction to jump to the subroutine, simultaneously passing the return address on the stack or in a register

the callee:

  1. allocates a frame by subtracting an appropriate constant from the sp
  2. saves the old frame pointer into the stack, and assigns it an appropriate new value
  3. saves any callee-saves registers that may be overwritten by the current routine (including the static link and return address, if they were passed in registers)

After the subroutine has completed, the epilogue:

  1. moves the return value (if any) into a register or a reserved location in the stack
  2. restores callee-saves registers if needed
  3. restores the fp and the sp
  4. jumps back to the return address

Finally, the caller:

  1. moves the return value to wherever it is needed
  2. restores caller-saves registers if needed

Displays

One disadvantage of static chains is that access to an object in a scope k levels out requires that the static chain be dereferenced k times.

This number can be reduced to a constant by use of a display.

为了优化这一过程,可以引入一个叫做 display 的数据结构。display 是一个数组,其中的每个元素都是一个指针,指向不同嵌套层级的活动记录(activation record)。当进入一个函数时,编译器会更新 display 来反映当前的调用环境。具体来说,display[i] 会指向第 i 层嵌套的最近活动记录。

使用 display 可以直接通过数组索引快速定位到任何层级的活动记录,从而让访问外层变量的操作更加高效。这种方法减少了通过多个静态链指针进行跳转的需要,因此可以显著提高程序的运行速度,尤其是在函数嵌套层次较深的情况下。

Case Studies: C on the MIPS; Pascal on the x86

Calling sequences differ significantly from machine to machine and even compiler tocompiler

  • Compilers for CISC machines tend to pass arguments on the stack; compilers for RISC machines tend to pass arguments in registers.
  • Compilers for CISC machines usually dedicate a register to the frame pointer; compilers for RISC machines often do not.
  • Compilers for CISC machines often rely on special-purpose instructions to implement parts of the calling sequence; available instructions on a RISC machine are typically much simpler.

Register Windows

As an alternative to saving and restoring registers on subroutine calls and returns, the original Berkeley RISC machines introduced a hardware mechanism known as register windows.

The basic idea is to map the ISA’s limited set of register names onto some subset (window) of a much larger collection of physical registers, and to change the mapping when making subroutine calls.

In-Line Expansion

many language implementations allow certain subroutines to be expanded in-line at the point of call:

A copy of the “called” routine becomes a part of the “caller”; no actual subroutine calloccurs.

In-line expansion avoids a variety of overheads,including:

  • space allocation,
  • branch delays from the call and return,
  • maintaining the static chain or display,
  • and (often) saving and restoring registers.

It also allows the compiler to perform code improvements such as:

  • global register allocation
  • instruction scheduling
  • common subexpression elimination across the boundaries between subroutines

Parameter Passing

Most subroutines are parameterized: they take arguments that control certain aspects of their behavior, or specify the data on which they are to operate.

Parameter names that appear in the declaration of a subroutine are known as formal parameters.

Variables and expressions that are passed to a subroutine in a particular call are known as actual parameters.

Parameter Modes

The two most common parameter-passing modes, called:

  • call-by-value
  • call-by-reference

call-by-value只要在函数返回时把参数的值写回到调用方,就可以实现和call-by-reference类似的效果

Call-by-sharing

不是值传递。因为:

if we modify the object to which the formal parameter refers, the program will be able to see those changes through the actual parameter after the subroutine returns

也不是引用传递,因为:

although the called routine can change the value of the object to which the actual parameter refers, it cannot change the identity of that object.

Call-by-sharing is thus commonly implemented the same as call-by-value for objects of immutable type.

The Purpose of Call-by-Reference
  • 需要修改参数
  • 传递地址比复制参数节约时间

Call-by-Name

Explicit subroutine parameters are not the only language feature that requires a closure to be passed as a parameter.

In general, a language implementation must pass a closure whenever the eventual use of the parameter requires the restoration of a previous referencing environment.

Special-Purpose Parameters

  • Conformant Arrays
  • Default (Optional) Parameters
  • Named Parameters: A(name=’xxx’, age=24)
  • Variable Numbers of Arguments: fun(string…)

Generic Subroutines and Modules

需要泛型的原因:

In a language like Pascal or Fortran, this static declaration of item type means that the programmer must create separate copies of enqueue and dequeue for every type of item, even though the entire text of these copies (other than the type names in the procedure headers) is the same.

Implementation Options

Generics can be implemented several ways.

  • the compiler creates a separate copy of the code for every instance
  • guarantees that all instances of a given generic will share the same code at run time.

Generic Parameter Constraints(泛型约束)

避免使用隐式泛型参数:

To avoid surprises, it is best to avoid implicit use of the operations of a generic parameter type.

Exception Handling

exception handling generally requires the language implementation to “unwind” the subroutine call stack.

try catch语法:

all provide exception-handling facilities in which handlers are lexically bound to blocks of code, and in which the execution of the handler replaces the yet-to-be-completed portion of the block.

In practice, exception handlers tend to perform three kinds of operations:

  • First, ideally, a handler will compensate for the exception in a way that allows the program to recover and continue execution.
  • Second, when an exception occurs in a given block of code but cannot be handled locally, it is often important to declare a local handler that cleans up any resources allocated in the local block, and then “reraises”the exception, so that it will continue to propagate back to a handler that can (hopefully) recover.
  • Third, if recovery is not possible, a handler can at least print a helpful error message before the program terminates.

Defining Exceptions

In many languages, dynamic semantic errors automatically result in exceptions, which the program can then catch. The programmer can also define additional, application-specific exceptions.

Most languages use a throw or raise statement,embedded in an if statement, to raise an exception at run time.

已知和未知异常:

If a subroutine raises an exception but does not catch it internally, it may “return” in an unexpected way.

include in each subroutine header a list of the exceptions that may propagate out of the routine.

Unchecked exceptions are typically run-time errors that most programs will want to be fatal

Exception Propagation(异常传播)

When an exception arises, the handlers are examined in order; control is transferred to the first one that matches the exception.

Implementation of Exceptions

The most obvious implementation for exceptions maintains a linked-list stack of handlers. When control enters a protected block, the handler for that block is added to the head of the list.

Coroutines

vs continuation:

a continuation is a constant—it does not change once created—while a coroutine changes every time it runs.

coroutines are execution contexts that exist concurrently, but that execute one at a time, and that transfer control to each other explicitly, by name. Coroutines can be used to implement iterators and threads.

Events

An event is something to which a running program (a process) needs to respond, but which occurs outside the program, at an unpredictable time.

事件和回调:

Instead, the programmer usually wants a handler—a special subroutine—to be invoked when a given event occurs. Handlers are sometimes known as callback functions,because the run-time system calls back into the main program instead of being called from it.

Summary and Concluding Remarks

这一章主要关注了控制抽象的主题,特别是子程序。子程序允许程序员将代码封装在一个狭窄的接口后面,然后可以不考虑其实现方式进行使用。控制抽象对于任何大型软件系统的设计和维护都至关重要。从审美的角度来看,像Lisp和Smalltalk这样的语言中,内置和用户定义的控制结构使用相同的语法,这使得控制抽象特别有效。

我们在8.1节开始研究子程序,首先回顾了子程序调用堆栈的管理。然后我们考虑了用于维护堆栈的调用序列,PLP CD的额外部分专门讨论了展示;MIPSpro C编译器和GNU x86 Pascal编译器(gpc)的案例研究;以及SPARC的寄存器窗口。在简要考虑内联扩展之后,我们在8.3节转向了参数的主题。我们首先考虑了参数传递模式,所有这些模式都是通过传递值、引用或闭包来实现的。我们注意到,语义清晰和实现速度的目标有时会有冲突:通常通过引用传递大参数最有效,但是由此产生的别名可能会导致程序错误。在8.3.3节,我们考虑了特殊的参数传递机制,包括一致的数组、默认(可选)参数、命名参数和可变长度的参数列表。我们注意到,默认和命名参数提供了一种对动态范围的有吸引力的替代方案。在8.4节,我们考虑了泛型子程序和模块的设计和实现。泛型允许在编译时将控制抽象参数化,以参数的类型而不仅仅是它们的值为基础。

在最后的三个主要部分,我们考虑了异常处理机制,这些机制允许程序以良好的结构方式从嵌套的子程序调用序列中“解开”;协程,它允许程序维护(并在两个或更多执行上下文之间切换);以及事件,它允许程序响应异步外部活动。在PLP CD上,我们解释了协程如何用于离散事件模拟。我们还注意到,它们可以用来实现迭代器,但在这里存在更简单的替代方案。在第12章,我们将基于协程来实现线程,这些线程并行运行(或看起来并行运行)。

在几个情况下,我们可以看出关于语言应该提供哪些类型的控制抽象的观点正在形成共识。像Fortran和Algol 60这样的语言的有限参数传递模式已被更广泛或灵活的选项取代。Ada和C++等语言中,标准的位置记号法已被默认参数和命名参数所增强。较少结构化的错误处理机制,如标签参数、非局部goto和动态绑定处理器,已被结构化的异常处理器取代,这些异常处理器在子程序内部进行词法范围处理,并且在常见(无异常)情况下可以零成本实现。传统的信号处理机制中的自发子程序调用已被专用线程中的回调取代。在许多情况下,实现这些新特性需要编译器和运行时系统变得更复杂。偶尔,如call-by-name参数、标签参数或非局部goto的情况,语义上令人困惑的特性也难以实现,放弃它们使编译器变得更简单。在其他情况下,一些有用但难以实现的语言特性仍然在一些语言中出现,但在其他语言中则不出现。这一类别的例子包括一等子程序、协程、迭代器、续延和具有无限范围的局部对象。

DataTypes

Types serve two principal purposes:

  • Types provide implicit context for many operations, so that the programmer does not have to specify that context explicitly. 让编译器知道是整数还是浮点数加减、自定义类型申请正确的堆空间、执行用户自定义的类型构造器(constructor)
  • Types limit the set of operations that may be performed in a semantically valid program.

Type Systems

硬件层面所有bit没有区别:

Computer hardware can interpret bits in memory in several different ways: as instructions, addresses, characters, and integer and floating-point numbers of various lengths.

高级语言必须使用类型系统来提供某一块bit的类型上下文信息和错误检查(整数加浮点数。。。)

High-level languages, on the other hand, almost always associate types with values, to provide the contextual information and error checking alluded to above.

a type system consists:

  • a mechanism to define types and associate them with certain language constructs
  • a set of rules for type equivalence, type compatibility, and type inference.

Type Checking

Type checking is the process of ensuring that a program obeys the language’s type compatibility rules.

几种分类:

  • 强类型
  • 弱类型
  • 静态类型
  • 动态类型
  • 编译时绑定
  • 运行时绑定

Polymorphism(多态)

Polymorphism allows a single body of code to work with objects of multiple types.

多态、动态类型会造成运行时消耗并且延迟暴露类型错误

explicit parametric polymorphism(泛型)

The Meaning of “Type”

There are at least three ways to think about types, which we may call the denotational, constructive, and abstraction-based points of view.

  • denotational(表示意义):A value has a given type if it belongs to the set
  • constructive(构造):a type is either one of a small collection of built-in types, or a composite type created by applying a type constructor
  • abstraction-based(基于抽象):a type is an interface consisting of a set of operations with well-defined and mutually consistent semantics

Types are domains, and the meaning of an expression is a value from the domain that represents the expression’s type.

One of the nice things about the denotational view of types is that it allows us in many cases to describe user-defined composite types.

Classification of Types

Most languages provide built-in types similar to those supported in hardware by most processors: integers, characters, Booleans, and real (floating-point) numbers.

  • Numeric Types
  • Enumeration Types
  • Subrange Types: 0..100
  • Composite Types
    • Records (structures)
    • Variant records (unions)
    • Arrays
    • Sets
    • Pointers
    • Lists
    • Files

Orthogonality(正交性)

Orthogonality is equally important in type system design. A highly orthogonal language tends to be easier to understand, to use, and to reason about in a formal way.

Type Checking

Type Equivalence

there are two principal ways of defining type equivalence.

  • Structural equivalence is based on the content of type definitions(结构一样就一样)
  • Name equivalence is based on the lexical occurrence of type definitions(结构一样换个名字也不一样)

Type Compatibility

Most languages do not require equivalence of types in every context. Instead, they merely say that a value’s type must be compatible with that of the context in which it appears.

In general, modern compiled languages display a trend toward static typing and away from type coercion.

For systems programming,or to facilitate the writing of general-purpose container (collection) objects (lists, stacks, queues, sets, etc.) that hold references to other objects, several languages provide a universal reference type.

Records (Structures) and Variants (Unions)

Record types allow related data of heterogeneous types to be stored and manipulated together.

Memory Layout and Its Impact

The fields of a record are usually stored in adjacent locations in memory. In its symbol table,the compiler keeps track of the offset of each field within each record type.

Variant Records (Unions)

Many allowed the programmer to specify that certain variables (presumably ones that would never be used at the same time) should be allocated “on top of” one another, sharing the same bytes in memory.

Arrays

Arrays are the most common and important composite data types. They have been a fundamental part of almost every high-level language.

接下来的章节包含下面的内容:

  • Syntax and Operations
  • Dimensions, Bounds, and Allocation
    • Stack Allocation
    • Heap Allocation
  • Memory Layout
    • row-major
    • column-major
    • Row-Pointer Layout
    • Address Calculations

Strings

Sets

Pointers and RecursiveTypes

  • Dangling References
  • Garbage Collection
    • Reference Counts
    • Tracing Collection
    • Mark-and-Sweep
    • Pointer Reversal
    • Stop-and-Copy
    • Generational Collection
    • Conservative Collection

Lists

Files and Input/Output

EqualityTesting and Assignment

Summary and Concluding Remarks

这一节结束了我们关于语言设计五个核心章节中的第三章(第一部分的名称、控制流、类型、子程序和类)。在前两节中,我们讨论了类型系统和类型检查的一般问题。在剩余的章节中,我们检查了最重要的复合类型:记录和变体、数组和字符串、集合、指针和递归类型、列表和文件。我们注意到类型有两个主要目的:它们为许多操作提供了隐式上下文,使程序员无需明确指定该上下文,并且它们允许编译器捕捉各种常见的编程错误。类型系统包括一组内置类型、定义新类型的机制以及类型等价、类型兼容性和类型推断的规则。类型等价确定两个名称或值何时具有相同的类型。类型兼容性确定何时可以在“期望”另一类型的上下文中使用某一类型的值。类型推断基于其组件的类型或(有时)周围上下文来确定表达式的类型。如果一个语言从不允许对不支持它的对象进行操作,则称该语言为强类型语言;如果一个语言在编译时强制执行强类型,则称该语言为静态类型语言。

在我们关于类型的一般讨论中,我们区分了表示性、构造性和基于抽象的观点,分别从它们的值、它们的子结构以及它们支持的操作来看待类型。我们为常见的内置类型、枚举、子范围以及常见的类型构造器引入了术语。我们讨论了几种不同的类型等价、兼容性和推断方法,包括(在PLP CD上)对ML的推断规则的详细检查。我们还检查了类型转换、强制和非转换类型转换。在类型等价的领域内,我们对比了结构性和基于名称的方法,指出虽然名称等价似乎越来越受欢迎,但结构等价仍有其拥护者。

在我们对复合类型的调查中,我们花了最多的时间在记录、数组和递归类型上。记录的关键问题包括变体记录的语法和语义、整个记录的操作、类型安全以及每个与内存布局的交互。内存布局对数组也很重要,在其中它与形状的绑定时间相互作用;静态、栈和基于堆的分配策略;数值应用中高效的数组遍历;C中指针和数组的互操作性;以及可用的整个数组和基于切片的操作集。

对于递归数据类型,很多都取决于变量/名称的值模型和引用模型之间的选择。递归类型是引用模型的自然结果;使用值模型时,它们需要指针的概念:其值为引用的变量。从实现角度来看,值与引用之间的区别很重要:将内置类型实现为引用是浪费的,因此具有引用模型的语言通常会以不同方式实现内置和用户定义的类型。Java在语言语义中反映了这种区别,要求内置类型采用值模型,用户定义的类类型的对象采用引用模型。

递归类型通常用于创建链接数据结构。在大多数情况下,这些结构必须从堆中分配。在一些语言中,程序员负责释放不再需要的堆对象。在其他语言中,语言运行时系统自动识别并回收这种垃圾。显式释放是对程序员的一种负担,并会导致内存泄漏和悬挂引用的问题。尽管语言实现几乎从不尝试捕捉内存泄漏(参见探索3.32和练习7.36,不过,有一些关于这个主题的想法),但有时会使用墓碑或锁和钥匙来捕捉悬挂引用。自动垃圾回收可能代价高昂,但已被证明越来越受欢迎。大多数垃圾回收技术要么依赖于引用计数,要么依赖于某种形式的递归探索(追踪)当前可访问的结构。这一类别中的技术包括标记-清扫、停止-复制和分代收集。

语言设计的几个领域中,I/O显示出了极大的变化。我们的讨论(主要在PLP CD上)区分了交互式I/O,这往往非常具有平台特定性,以及基于文件的I/O,后者又细分为临时文件,用于单次程序运行中的大量数据,以及用于离线存储的持久文件。文件还细分为那些以二进制形式表示信息的文件,这些文件模仿内存中的布局,以及那些转换为字符基础文本和从字符基础文本转换回来的文件。与二进制文件相比,文本文件通常会产生时间和空间的开销,但它们具有可移植性和人类可读性的重要优势。

在我们对类型的检查中,我们看到了许多语言创新的例子,这些创新有助于提高程序的清晰度和可维护性,而且通常几乎没有或根本没有性能开销。例子包括用户定义类型的原始想法(Algol 68)、枚举和子范围类型(Pascal)、记录和变体的整合(Pascal)以及Ada中子类型和派生类型之间的区别。在第9章中,我们将检查许多人认为过去30年中最重要的创新,即面向对象。

在某些情况下,语言之间的区别不太是进化的问题,而是哲学上的根本差异。我们已经提到了变量/名称的值模型和引用模型之间的选择。同样地,大多数语言采用了静态类型,但Smalltalk、Lisp和许多脚本语言则与动态类型配合得很好。大多数静态类型语言采用了名称等价,但ML和Modula-3则与结构等价配合得很好。大多数语言已经远离了类型强制转换,但C++却接受了它们:结合运算符重载,它们使得在语言本身之外定义简洁、类型安全的I/O例程成为可能。

正如上一章中所看到的,为了简化编译器,或使编译后的程序更小或更快,一种语言的便利性、正交性或类型安全似乎已经受到了妥协。例子包括大多数语言中缺乏对记录的相等性测试,Pascal和Ada要求记录的变体部分位于末尾的要求,许多语言对集合最大尺寸的限制,C语言中缺乏对I/O的类型检查,以及许多语言实现中通常缺乏动态语义检查。我们还看到了几个至少部分是为了高效实现而引入的语言特性的例子。这些包括打包类型、多长度数值类型、with语句、十进制算术和C风格的指针算术。

与此同时,可以看出语言设计者和用户越来越愿意接受语言实现中的复杂性和成本,以改善语义。这里的例子包括Ada的类型安全变体记录;Java和C#的标准长度数值类型;Icon、Java和C#的变长字符串和字符串操作符;Ada中数组边界的后期绑定;以及Fortran 90中丰富的整个数组和基于切片的数组操作。也可以包括ML的多态类型推断。当然,还应该包括自动垃圾回收的趋势。曾经被认为对于生产质量的命令式语言来说太昂贵的垃圾回收,现在不仅在Clu和Cedar等实验性语言中是标准配置,而且在Ada、Modula-3、Java和C#中也是如此。许多这样的特性,包括变长字符串、切片和垃圾回收,已被脚本语言所采纳。

0%