代码之髓读后感——语法&流程&函数&错误处理

Perl的设计者:Larry Wall在《Programming Perl》中提出,优秀的程序员有三大美德:懒惰,急躁和傲慢。

懒惰是因为为减少总的能量支出而不遗余力的努力。

急躁是因为无法忍受程序执行的低效。

傲慢是因为容不下错误。


语言的诞生

程序设计语言的出现就是为了解决各种各样的问题,而这些问题以现在的语言是无法更合适的处理。

于是乎,语言之于语言,就是目标问题的差异。


语法的诞生

语法是程序设计者制定的规则。因语言而异。

比如运算符的优先顺序,计算流程的规定等等。

对于式子(2+1)*3

书中提到了比较有特点的FORTH语言,书写成:12+3*

即后缀表达式的形式。利用的是栈来实现。

而LISP语言则是前缀表达式:*+123

从语法树的角度来看,连着实际上就是对于相同的树结构进行了不同的遍历方式。

LISP语言语法简单,代码与语法树容易理解并且对应比较直观,此外它还具有宏这样的语法树替换机制,这两个特点催生了结构化编程等一系列现象。

而语言FORTRAN则是引入了运算符优先级和结合性等复杂语法,使得程序员们编写数学表达式更为习惯。编译程序时,它的语法分析器(把原代码作为字符串,读入解析并建立语法树的程序)会将源代码的字符串转换为语法树。

现代语言大多崇尚FORTRAN的语言风格,追求简单便利的编写规则。

但是因为不存在任何解析矛盾的语法体系的设计是十分困难的,又要不断地融入新语法时又要避免与现有的发生冲突,这样更难,正是因此,现代程序设计语言中仍然保留着不少别扭复杂的编写规则。


流程控制

源自结构化程序设计的诞生。

他们的功能原本都可以借助goto来实现。

if... & if...else...

很早以前存在满足条件后跳的命令。

1949年发明的EDSAC就有“特定内存值大于零时跳转”和“特定内存值为负时跳转”这两条命令。

这使得本来要表达 如果满足条件就执行某事的逻辑,不得不变成了 若是满足条件则跳转到某处执行某事

而导入if语句则使得逻辑更为清楚,理解更为直观。

while... & break

主要是用来做那些只要有goto语句就能做的事。带来的附加值是程序的易读性和易写性。

for...

让数值渐增的while更加简洁。for主要通过循环次数来控制循环操作。

升级版的foreach,则根据处理的对象来控制循环操作。

但是并非所有的for语句都可以写成foreach。

这些流程控制语句主要是为了实现程序的 简洁易懂


函数

随着程序的庞大,把握全局愈发困难,同时有可能多次要用到相同的操作。函数因此诞生。把一整块代码切分出来而命名。同时伴随着函数的出现,也产生了递归调用这一编程技巧,非常适合处理嵌套形式的数据。

函数的使用使得程序便于理解和重复使用。

从冗长的程序中切分出反复使用的代码将其封装成一个整体。

遇到问题:一个程序中有几处执行相同的代码若是封装,那怎么返回到原来的地方?

我们希望的是,执行跳转时,记住该位置,之后返回时,又能跳转到该位置后面。

EDSAC的方法是通过修改程序中跳转命令的跳转目的地而实现调用后返回原来的位置。

遇到问题:函数调用者必须知道跳转目的地和返回命令所在地。

创建用来事先记录返回目的地的内存空间,并设计能跳转到该内存空间所记录的地址的命令。

遇到问题:当调用函数X时又调用了Y,返回目的地的内存被覆写,X执行后的返回目的地发生错误。

人们开始使用栈。

这一切都是因为栈这种结构的特性决定的。

由于函数的实际上是代码段的封闭环境,有起始就得有结束,而对于嵌套函数调用而言,必然是越深的,越晚调用的越要及早结束,及早返回,正好符合后入先出的特点。使用栈来实现函数的调用,是极为合适的。

将数据存储的地址依次压入栈中,而栈顶元素内容就是最后被存入数据的地址。

关于递归调用

关键在于如何退出。


错误处理

主要有两种方法:

  1. 使用返回值

  2. 使用异常

使用返回值

通过判断错误的返回值来判断错误并进行相应的处理。

传递错误信息既可以使用返回值,也可以使用事先定义好的全局变量,还可以传递引用形参等方法。

这种方法可以在C语言中见到。

遇到问题:

  • 遗漏错误——忘记做返回值检查;出错条件难以确保完全...

  • 错误处理导致代码可读性下降——为了避免错误导致出现大量错误处理代码,影响可读性...

对于第二个问题,可以在错误处理相同时,使用goto进行集中处理。从代码形式来看,实现了代码和错误处理的分离。

Linus在《Linux内核编码风格》中推荐使用goto语句把函数的结尾处理集中起来。

小栗子:

int main() {
    if(!fun(A)) goto ERROR;
    if(!fun(B)) goto ERROR;
    if(!fun(C)) goto ERROR;
    return 0;
ERROR:
    /* 失败处理 */
}

使用异常(异常处理),出错后跳转

调用函数前设定好错误处理的代码,错误发生时能跳转至相应的错误处理代码。

C之前出现过事先定义好错误发生时跳转的位置,后来演变为了现在的异常处理。

计算机UNIVACI:在计算中出现溢出时,它会执行000处编写的命令。(即 中断(interrupt),键盘按键按下,CPU可以接收信号,传达信息的就是中断)

语言COBOL:只有两种针对性的错误处理。见下例:

READ <文件名> AT END <错误处理语句>
ADD <函数名> ON SIZE ERROR <错误处理语句>

语言PL/I: 引入ON语句。可以自定义增加新的错误类型,也可以主动触发新定义的错误类型。

相较于PL/I的错误处理,现代的JAVA,C++,PYTHON语言的方式有所不同,前者先定义好出错的处理操作,在编写可能出错的代码,而后者则是先用try{...}编写可能出错的代码,在编写出错时的处理。

一些历史

这样的设计的改变源自当初John Goodenough的论文中所提出的方法。

命令有可能会抛出异常,而程序员有可能忘记这种可能性,也可能在不正确的地方编写异常处理或者编写不正确类型的异常处理。为使编译器能够对程序员的错误发出警告,减少这种可能性,需要做到两点。一是明确声明命令可能抛出何种异常,二是需要有将可能出错的操作括起来的语句结构。

这里提议的括起来的语句为基础,现代大部分语言采用了先括起来可能出错的操作,再编写错误处理的语句结构。明确声明命令可能抛出何种异常,这个设计方针在 Java语言的异常检查中得以继承。

1977年语言CLU11引入了异常处理的机制,追加了置于命令后面的错误处理语句结构except。CLU语言从最初就具有用begin…end将代码括成块状的功能,这一功能和except相结合,就实现了将可能出错的操作括起来再补充错误处理的代码编写方式。

1983年C++诞生。针对异常处理的语句结构问题从1984年到1989年间经历了多次讨论,C++语言最终确认追加一种语句结构,把关键字try放在那些被括起来的可能出错代码的前面,把关键字catch放在捕捉并处理错误的代码块前面。按照C++语言设计者斯特劳斯特卢普(Bjarne Stroustrup)的说法,try只是一个为了方便理解的修饰符。 另外还使用throw这一关键字作为触发异常的命令,是因为更易理解的raisesingal两个关键字已经在标准库作为函数名字占用了。

1993年发布的Windows NT 3.1在操作系统和C语言编译器导入了结构化异常处理(Structured Exception Handling,SEH)的概念。结构化异常处理中,除了将可能出错的代码括起来的__try和将错误处理的代码括起来的__except之外,还有将即使出错也要执行的代码括起来的__finally

为什么要引入finally

程序在意料之外结束时,也可以正常的释放锁定的内存和文件等资源,无遗漏的执行成对操作。对于错误处理,要能够不使用返回值检查和goto语句,简洁的实现。

后来Java,Python,Ruby等语言都使用了finally

C++语言中没有finally。C++语言中使用了一种名叫RAII(Resource Acquisition Is Initialization,资源获取即初始化)的技术。比如,在操作打开了就要关闭的文件对象时,定义来操作该对象的类,用构造函数打开,用析构函数关闭。函数结束时,针对函数局部变量,程序可以自动调用析构函数。

2001年出现的D语言以改良C++语言为目标,反对 RAII是优雅的这一意见。打开了就要关闭这样紧密关联的操作,反映在代码上时,如果能放在相近的位置就容易理解多了。基于这一考虑,D语言中引入了作用域守护(scope guard)的概念。通过使用作用域守护,可以事先定义从某一 作用域(如函数)跳出时执行的操作。

何时抛出异常

发生错误应该停止操作立刻报告,这一设计思想被称为错误优先(fail first)。

异常传递

包括Java在内的很多现代语言的异常处理机制中,异常可以传递到调用方。这一设计有一个很大的问题。那就是,即使看到了函数f的代码也不知道函数f可能会抛出什么异常。有可能是函数f调用的另外的函数g中抛出的异常传递过来的,也有可能是函数g调用的函数h抛出的异常。也就是说,如果不看见函数f调用的所有的函数代码,就无从得知函数f抛出何种异常。万一没有察觉到抛出某种异常的可能性,程序就有可能异常终止。

Goodenough主张为了避免这一问题,需要明确地声明可能抛出的异常。Java语言就采用了这一方针。

其他语言中所谓的异常,Java语言中的throw语句也能抛出,并进一步分为三类:

  • 不应该做异常处理的重大问题

  • 可做异常处理的运行时异常

  • 可做异常处理的其他异常

这里的其他异常叫做 检查型异常,如果在方法之外抛出,就需要在定义方法时声明,throws就是为这个目的准备的。

实现异常处理的小栗子:

class Foo {
    // shippai跑出MyException异常
    void shippai() throws MyException {
        throw new MyException();
    }

    // 1. 使用shippai方法声明`throws MyException`
    void foo() throws MyException {
        shippai();
    }
    // 2. 使用catch捕获MyException异常,进行错误处理
    void bar() {
        try {
            shippai();
        }catch(MyException e) {
            ...
        }
    }
}
class MyException extends Exception();

异常处理的问题

一个是当函数有不只一个出口时,必须成对处理的操作很难正确地成对处理。另一个是即便看了代码也不知道函数将抛出何种异常。Java语言的开发者为了解决第二个问题导入了检查型异常,但是这种方法并不太被接受。C#语言的开发者一方面承认检查型异常的优势,另一方面希望有更好的方法出现。

检查型异常的问题:可以说检查型异常是一种非常好的机制。但是这种机制并没有很好地普及到其他语言中。因为它太麻烦。一旦throwstry/catch中异常的数目增多,或者某一方法需要追加一种异常,就不得不修改调用了该方法的所有方法,特别麻烦。

你可能感兴趣的:(语言)