The Ten Commandments of Egoless Programming

无我编程的十诫

  1. Understand and accept that you will make mistakes. The point is to find them early before they make it into production. Fortunately, except for the few of us developing rocket guidance software at JPL, mistakes are rarely fatal in our industry. We can, and should, learn, laugh, and move on.

    理解并接受你会犯错误。关键是要在错误进入生产之前尽早发现它们。幸运的是,除了我们在 JPL 开发火箭制导软件的少数人之外,错误在我们的行业中很少是致命的。我们可以,也应该,学习、欢笑并继续前进。

  2. You are not your code. Remember that the entire point of a review is to find problems, and problems will be found. Don’t take it personally when one is uncovered. (Allspaw note – related: see below, number #10, and the points Theo made above.)

    你不是你的代码。请记住,审查的整个目的就是发现问题,而问题会被发现。当发现问题时,不要把它当作个人问题。(Allspaw 注 – 相关内容:见下文,第 #10 条,以及 Theo 上面提到的要点。)

  3. No matter how much “karate” you know, someone else will always know more. Such an individual can teach you some new moves if you ask. Seek and accept input from others, especially when you think it’s not needed.

    无论你知道多少“空手道”,总会有人知道更多。如果你请求,这样的人可以教你一些新招式。寻求并接受他人的意见,尤其是在你认为不需要的时候。

  4. Don’t rewrite code without consultation. There’s a fine line between “fixing code” and “rewriting code.” Know the difference, and pursue stylistic changes within the framework of a code review, not as a lone enforcer.

    在没有咨询的情况下不要重写代码。“修复代码”和“重写代码”之间有一条细微的界限。了解区别,并在代码审查的框架内追求风格上的变化,而不是作为孤独的执行者。

  5. Treat people who know less than you with respect, deference, and patience. Non-technical people who deal with developers on a regular basis almost universally hold the opinion that we are prima donnas at best and crybabies at worst. Don’t reinforce this stereotype with anger and impatience.

    尊重、谦恭和耐心地对待那些知识不如你的人。与开发人员定期打交道的非技术人员几乎普遍认为我们充其量是自命不凡,最糟糕的是像小孩一样哭闹。不要用愤怒和不耐烦来强化这种刻板印象。

  6. The only constant in the world is change. Be open to it and accept it with a smile. Look at each change to your requirements, platform, or tool as a new challenge, rather than some serious inconvenience to be fought.

    世界上唯一不变的就是变化。要对变化保持开放态度,并以微笑接受它。将每一次对您的需求、平台或工具的变化视为一个新的挑战,而不是需要抗拒的严重不便。

  7. The only true authority stems from knowledge, not from position. Knowledge engenders authority, and authority engenders respect — so if you want respect in an egoless environment, cultivate knowledge.

    唯一真正的权威源于知识,而非地位。知识产生权威,权威产生尊重——因此,如果你想在一个无自我的环境中获得尊重,就要培养知识。

  8. Fight for what you believe, but gracefully accept defeat. Understand that sometimes your ideas will be overruled. Even if you are right, don’t take revenge or say “I told you so.” Never make your dearly departed idea a martyr or rallying cry.

    为你所信仰的而奋斗,但优雅地接受失败。要明白,有时你的想法会被否决。即使你是对的,也不要报复或说“我早就说过了。”永远不要让你心爱的想法成为殉道者或号召口号。

  9. Don’t be “the coder in the corner.” Don’t be the person in the dark office emerging only for soda. The coder in the corner is out of sight, out of touch, and out of control. This person has no voice in an open, collaborative environment. Get involved in conversations, and be a participant in your office community.

    不要成为“角落里的程序员”。不要成为那个在黑暗办公室里只为喝汽水而出现的人。角落里的程序员不在视线范围内,失去联系,失去控制。这个人在开放、协作的环境中没有发言权。参与对话,成为你办公室社区的参与者。

  10. Critique code instead of people — be kind to the coder, not to the code. As much as possible, make all of your comments positive and oriented to improving the code. Relate comments to local standards, program specs, increased performance, etc.

    批评代码而不是人 — 对编码者要友善,而不是对代码。尽可能使您的所有评论都是积极的,并旨在改善代码。将评论与本地标准、程序规范、性能提升等相关联。

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的情况,语义上令人困惑的特性也难以实现,放弃它们使编译器变得更简单。在其他情况下,一些有用但难以实现的语言特性仍然在一些语言中出现,但在其他语言中则不出现。这一类别的例子包括一等子程序、协程、迭代器、续延和具有无限范围的局部对象。

0%