对大多数计算模型而言,顺序都是基本的东西,它肯定了为完成所指望的某种工做,什么事情应该最早作,什么事应该随后作,咱们能够将语言规定顺序的机制分为几个类别:java
对于不一样类别的语言对不一样类别的控制流的重要性也不尽相同,好比顺序执行相比于函数式对于命令式则更加剧要。而命令式中更倾向用迭代,函数则更强调递归node
在讨论控制流以前先讨论下表达式的问题,先明确两个概念:运算符一般是指那些采用特殊语法形式的内部函数(好比+-*/等),运算对象指的是运算符的参数(如2+3,2和3就是运算对象),那么运算符和运算对象的组合就是表达式。通常根据运算符出现的位置(相对于运算对象而言),能够分为3类表示形式:前缀、中缀和后缀。好比Lisp就运用前缀语法:python
(+ 1 3 4 6) (* (+ 1 7) 8) 复制代码
大多数命令式语言对二元运算符都使用中缀记法,而对一元运算符和其它函数使用前缀激发。可是像Lisp就所有统一使用中缀记法程序员
优先级和结合性编程
大多数程序设计语言都提供丰富的内部算术。在用中缀方式(没有括号)写出就可能出现歧义。因此就须要优先级和结合性来解决歧义性,可是我以为安全
妈的你写括号就完事儿了bash
并且不一样语言的优先级和结合性也不尽相同markdown
在纯函数式语言中,程序的基本组成部分是表达式,计算也仅是对表达式求值。任何一个表达式对于整个计算的影响也仅限于这个表达式所处的上下文环境。数据结构
而命令式语言的状况与此大相径庭,计算一般是经过对内存中变量值的一系列修改操做来完成,赋值就是这种修改的最基本手段。每一次赋值都表示一个值被放入一个对应的变量中。闭包
通常来讲,若是语言中的一个结构除了返回一个值供其外围环境所使用,还能以其余方式影响后续计算(并最终影响程序输出),那么咱们就说这种结构有反作用。而反作用也是命令式语言里最核心的部分
而在纯函数语言中没有任何的反作用,表达式的值只依赖于输入
可是如今许多语言都是混合的,像Python和Ruby主要是命令式的,可是也提供了不少的函数式的特征,如今连Java都提供了对函数式的支持
考虑一下下面的C语言的赋值:
d = a;
a = b + c;
复制代码
第一个语句中,赋值语句右部引用了a的值,并但愿把这个值放入d。第二个语句左部引用了a的位置,但愿把b+c的结果放进去。这两种解释(值和位置)都是可行的,由于c语言中变量就是能保存值的命名容器,因此咱们会说相似的语言的变量是值模型。因为指示位置的表达式被放在赋值语句的左部,因此这种指示位置的表达式成为左值表达式。表示一个值的表达式称为右值。在变量的值模型下,同一表达式也多是左值或者右值,好比(a=a+1),左部的a是左值,用于表示存放结果的位置;右部的a是右值,用于表明a具体所指的值。
在采用了变量的引用模型的语言中,这种左值和右值的差别就更加明显了。
b = 2; c = b; a = b + c; 复制代码
在值模型语言中程序员会说:“把2放入b,而后复制到c,而后用它们两个的值相加,把结果4放入a。”。;
在引用模型语言中的程序员会说:“让b引用2,让c也引用2,而后把这两个引用送给+运算,并让a引用算出的结果,也是4“。
而在Java中,对于内部类型使用值模型,而类使用引用模型
对于内部类型使用值模型,就没法以统一的方式将它们传给要求类类型的参数的方法,因此这里就须要一个装箱过程
好比Java提供的Integer类
Integer i = new Integer(12); 复制代码
咱们知道赋值操做有右结合性,这使得咱们能够写出a=b=c的简练代码,在一些语言中(Ruby,Go,Python)咱们能够进一步这样写:
a, b = 1, 2; //上面的语句结果就是a等于1,b等于2。 a, b = b, a; //交换两个值,若是没有这种语言特性,那么就须要引入临时变量了。 a, b , c = funx(d, e, f); 复制代码
这种记法也消除了大多数程序设计语言中函数的非对称性,这些语言能够容许任意多个参数,但只能返回一个返回值。可是其实在Python中的返回多个值,就是将多个值封装为元组,在赋值的时候又拆箱而已
并非全部语言都提供声明变量时指定初始值的方式,可是至少有这几点能够证实提供初始值的机制是有益的
若是声明时没有明确的给定变量的初始值,语言也能够给定一个默认值。像C、Java和C#也都提供了相似的机制
动态检查
除了能够指定默认值以外,还能够采用另一种方式,将对为初始化的变量的使用做为动态语义错误,在运行时捕获这种错误。可是在运行时捕获全部使用到未初始化的状况的代价很是高
定义性赋值
在Java和C#中提供了一种定义性赋值的表示形式,意思就是由编译器来检查在达到一个表达式的全部可能控制路径上,都必须为这个表达式中的每一个变量赋过值
构造函数
许多面向对象语言都提供了为用户定义的类型的自动初始化方法,也就是构造函数
在C++中,还区分了初始化和赋值,它将初始化赋值解释为调用变量所属类型的构造函数,以初始值做为调用参数。在没有强制的状况下,赋值被解释为调用相关类型的赋值运算符,若是没有定义赋值运算符,就默认将赋值右部的值简单的按位复制过来
区分初始化和赋值的好处是,能够区分在赋值前是否是须要先释放空间
虽然优先级和结合性规则定义了表达式里二元中缀运算符的应用顺序,但却没有明确说明特定运算符的各运算对象的求值顺序。举例来讲,以下表达式:
a - f(b) - c * d
复制代码
根据结合性可知a-f(b)将在第二个减法前执行,根据优先级可知第二个减法的右运算对象是cd这个总体而不是c。可是若是没有进一步的规则描述,咱们没法得知a-f(b)是否在cd以前运行。诸如此类:对于f(a,g(b),c)这个子程序调用,咱们也不知这三个参数的求值顺序。
求值顺序之因此重要:
反作用:若是f(b)这个子程序可能会修改c的值,那么a-f(b)-cd的求值结果将依赖f(b)和cd哪个先执行;相似的,若是g(b)修改了a或者c的值,那么f(a,g(b),c)的结果也是依赖于参数的求值顺序。
代码改进:子表达式的求值顺序对于寄存器分配和指令调度都有重要的影响。好比(ab+f(c)),咱们可能会但愿在执行ab以前调用f(c)。由于若是先计算乘法,则在调用f(c)以前就要先保存起来乘积,由于f(c)可能会用光全部的寄存器。
对于布尔表达式,若是编译器能够对其执行短路求值,那么它生成的代码能够在表达式前一半的结果能够肯定整个表达式的值的状况下跳事后一半的计算。
好比(a<b) and(b<c),若是a>b,那么彻底不必去检查b是否小于c就能够肯定这个表达式必定为假。在一些特殊状况下,短路求值可节省大量时间,好比if(func&&func())。实际上这种状况下短路求值已经改变了布尔表达式的语义,若是非短路求值,那么在func不存在的状况下去执行func(),程序是会抛出错误的。
咱们常见的语法表现形式是&&和||这种布尔运算符身兼多职,既是布尔运算符又会触发短路求值,可是有一些语言针对短路求值是有单独的语法形式的,好比Clu语言中布尔运算符是and和or,短路运算符是cand和cor。这是为什么呢,由于有些代码逻辑是不须要这种短路求值的优化的。
汇编语言中的控制流经过有条件的或无条件的跳转(分支)指令来完成,早期的高级语言模仿这种方式(如Fortran),主要依赖goto来描述大部分非过程化控制流,好比下面代码:
if A < B goto label1; label1; 复制代码
可是现在goto像在Java、Clu和Eiffel里已经彻底被禁止了,在其它语言也是受限了或者只是为了向前兼容而已
对于goto被废弃,各类使用goto的地方也被结构的方案给代替了
break和contiune这两个关键字你们应该很熟悉了
return
上面的两个问题均可以有很好的替代品,可是对于多层返回就会比较麻烦一点。return或”局部的goto“只能在子程序中返回,若是遇到多层嵌套的子程序,想从内层的子程序返回来结束外围子程序的执行,那return和局部的goto就无能为力了。这种状况下,语言实现要保证能恰当的恢复栈上的子程序调用信息,这种修复工做称为"回卷",为完成此事,不只必须释放须要跳出的全部子程序的栈帧,还要执行一些信息管理工做,好比恢复寄存器内容。
Common Lisp提供了return-from语句来明确指定须要退出的词法外围函数或嵌套块,还能够提供一个返回值:
Common Lisp和另一个语言Ruby中还内置一个throw/catch语法来支持这种多层返回,注意这种结构并非所谓的异常处理,而是一种多层返回的语法结构,直白点说是一种功能强大的变相”goto“,看下面代码:
//定义一个方法
def search_file(filename,pattern)
file=File.Open(filename)
//遍历文件每一行
file.each{|line|
//根据parrern匹配模式查找,若是匹配就返回到定义found标签的位置
throw :found,line if line=~/#{pattern}/
}
end
//用catch定义一个found标签
math=catch:found do
serach_file("f1",key)
serach_file("f2",key) //若是f2文件找到了则就会返回line至math
serach_file("f3",key)
”not fount“ //找不到就执行到此处了
end
print match
复制代码
多层返回的概念假定了被调用方知道调用方期的是什么,而且能返回一个适当的值。还存在一种状况,其中深层嵌套的子程序中发生了一些状况,致使没法继续执行下去,并且由于没有足够的环境信息,甚至没法合适的结束本身的工做,这种状况下,惟一能作的就是”退回去“,一直回退到可以恢复执行的地方,这种要求程序退回去的条件一般称为叫作”异常“。常见的结构化的异常处理和多层返回有很大的类似性,二者都须要从某一个内层上下文回退到外层的上下文。具体的差别则是多层返回是内层的上下文正常的完成计算而后根据须要返回正确的值,而后转移到外层上下文,并不须要后续处理。而异常中的内层上下文已是没法进行正常的计算,必须以一种非正常的退出一直回卷,而后触发某个特殊的处理流程直到catch到它。
若是进一步推广上一小节中形成栈回卷的非局部goto概念,则能够定义一种称为继续(Continuations)的概念。从底层来看,一个继续是由一个代码地址与其关联的一个引用环境组成的,若是跳转到这个地址,就该恢复这个引用环境。从抽象层面看,它描述一个可能由此继续下去的执行上下文。在Scheme和Ruby中,继续是基本的一等公民,咱们能够利用这种机制有效的扩充流程控制结构集合。
Scheme中支持继续由一个一般称为call-with-current-continuation的函数实现,有时简称"call/cc"。该函数有一个参数f,f也是一个函数;"call/cc"调用函数f,把一个记录着当前程序计数器和引用环境的“继续(暂时称为是c)c”传递给f,这种"继续c"由一个闭包来表示(经过参数传递的子程序的表示的闭包并没有不一样)。在未来任什么时候候,f均可以调用c,而后能够用c来从新创建其保存的上下文。通常的应用状况是咱们把这个c赋值给一个变量,则可重复的调用它,甚至咱们能够在f中返回它,即便f已经执行完毕,仍然能够调用c。
如今大部分命令式语言中采用的选择语句,都是从Algol 60引进过的 if...then...else 的某种演进变形:
if condition then statement else if condition then statement else if condition then statement ... else statement 复制代码
虽然 if...then...else 语句的条件是一个布尔表达式,可是一般没有必要求出这个表达式的值放入寄存器。大部分机器都提供了条件分支指令(如上面提到的IL指令brtrue.s),由于这个表达式求值的目的并非为了值,而是为了跳转到合适的位置。这种见解使得能够对短路求值的表达式生成高效的代码(称为跳转码)。跳转码不但能够用于选择语句,也可用在“逻辑控制的循环”中。以下面代码:
if((A>B)&&(C<D)||(E!=F)){ then_clause } else{ else_clause } 复制代码
在不使用短路求值的Pascal中,生成的代码大体以下(它会计算每一个表达式的结果并放入寄存器r1...,而后再决定跳转):
r1=A r2=B r1=r1>r2 r2=C r3=D r2=r2>r3 r1=r1&r2 r2=E r3=F r2=r2!=r3 r1=r1|r2 if r1=0 goto L2 L1: then_clause goto L3 L2: else_clause L3: 复制代码
跳转码的状况于此不一样,它不会把表达式的值存入寄存器,而是直接跳转(只用到了r1和r2两个寄存器,明显也不会针对整个表达式进行求值,比上面的要高效一些):
r1=A r2=B if r1<=r2 goto L4 r1=C r2=D if r1>r2 goto L1 L4: r1=E r2=F if r1=r2 goto L2 L1: then_clause goto L3 L2: else_clause L3: 复制代码
对于if else结构来讲,若是嵌套的层数过多、或者是用于判断的条件表达式是基于一些有限的简单值(或编译时常量),那么出现了一种更为优雅的语法结构“case语句”,有不少ifelse均可以很轻松的改写成case/switch语句
对于case/switch的优点还不仅是语法上的优雅,有时还能够生成更高效的代码
T: &L1
&L2
&L3
&L4
&L5
&L6
L1: clause_A
goto L7
L2: clause_B
goto L7
L3: clause_C
goto L7
L4: clause_D
goto L7
L5: clause_E
goto L7
L6: clause_F
goto L7
L7:
复制代码
这样其实T就是一个地址跳转表
迭代和递归是计算机可以重复执行一些操做的两种机制;命令式语言倾向于使用迭代、函数式语言则更看重递归。大多数语言中的迭代都是以循环的形式出现的,和复合语句中的语句同样,迭代的执行一般也是为了反作用,也就是修改一些变量的值。根据用何种方式控制迭代的次数来看,循环有两个主要变种"枚举控制的循环"和“逻辑控制的循环”。前者是在给定的某个有限的集合中执行,后者则是不肯定要执行多少次(直到它所依赖的表达式结果被改变)。对于这两种结构,大多数的语言都提供了不一样的语法结构来表示。
枚举控制的循环最初来自Fortran的do循环,
do i = 1, 10, 2 ... enddo 复制代码
等号后面的表达式分别是i的初始值,边界值和步长
像这种枚举循环能够说的很少,可是若是前几回迭代的执行会致使迭代的次数或下标值的发生变化,那么咱们就须要一个更通用的实现
思考几个问题:
固然在以后出现了C的for循环
for (int i = first; i < last; i += step) { ... } 复制代码
这样有关结束条件、溢出和循环方向的问题全都交由程序员来掌控
上面描述的循环都是在算术值的序列上迭代。不过通常而言,咱们还但愿能够在任何定义的良好的集合的元素上迭代。在C++和Java里叫作迭代器
Clu,Ruby等语言容许任何容器对象提供一个枚举本身元素的迭代器,这种迭代器就像是容许包含yield语句的子程序,每次yield生成一个循环下标
在Python里就能够这样写
for i in range(first, last, step): ... 复制代码
在被调用时,这个迭代器算出循环的第一个下标值,而后经过yield语句返回给调用者,yield语句很像return,可是不一样的是再每次循环结束后控制权会再次的交给迭代器,从新开始下一次yield,直到迭代器没有元素可yield为止才结束for循环。从效果上看迭代器就像是另一个控制线程,有它本身的程序计数器,它的执行与为它提供下标值的for循环交替执行,这一类一般称为真迭代器。
在许多面向对象语言里,用了更加面向对象的方法来实现迭代器。它们的迭代器就是一个常规对象,它提供了一套方法,用来初始化、生成下一个下标值和检测结束条件
BinTree<Integer> myTree; for (Integer i : myTreee) { } 复制代码
上面的这段代码实际上是下面这段的一个语法糖
for(Iterator<Integer> it = myTree.iterator();it.hasNext();) { } 复制代码
实现是将循环的体写成一个函数,用循环的下标做为函数的参数,而后将这函数做为参数传递给一个迭代器
(define uptoby (lambda (low high step f) (if (<= low higt) (begin (f low) (uptoby (+ low step) high step f)) '()))) 复制代码
在那些没有真迭代器或者迭代器对象的语言中,仍是能够经过编程方式实现集合枚举和使用元素之间的解耦的,用C语言作例子:
tree_node *my_tree; tree_iter ti: ... for(ti_create(my_tree,&ti); !ti_done(ti); ti_next(&ti)){ tree_node *n=ti_val(ti); ... } ti_delete(&ti); 复制代码
和枚举循环相比,逻辑控制的循环关注点只在结束条件上
前置检测
由Algol W引进,后来被Pascal保留
while cond do stat 复制代码
后置检测
这种的循环体无论是否知足循环条件,都至少会执行一次循环体。如C语言的do while语句
do{ line=read_line(); //...代码 } while line[0]!='$'; 复制代码
中置检测
中置检测通常依赖if
for(;;){ line=read_line(); if line[0]!='$' break; } 复制代码
递归和上述讨论的其余控制流都不一样,它不依赖特殊的语法形式,只要语言容许函数直接或间接的调用自身,那么就是支持递归的。大部分状况下递归和迭代均可以互相用对方重写的。
早期的一些语言不支持递归(好比Fortan77之前的版本),也有一些函数式语言不容许迭代,然而大部分现代语言都是同时支持二者的。在命令式语言中,迭代在某种意义上显得更天然一些,由于它们的核心就是反复修改一些变量;对于函数式语言,递归更天然一些,由于它们并不修改变量。若是是要计算gcd(更相减损法),递归更天然一些:
int gcd(int a,int b){ if(a==b) return a; else if (a>b) return gcd(a-b,b); else return gcd(a,b-a); } 复制代码
用迭代则是这样:
int gcd(int a,int b){ while(a!=b){ if(a>b) a=a-b; else b=b-a; } return a; } 复制代码
常常有人说迭代比递归效率更高,其实更准确的说法应该是,迭代的朴素实现的(无优化)效率一般比递归的朴素实现的效率要高。如上面gcd的例子,若是递归的实现确实是实实在在的子程序调用,那么这种子程序调用所带来的栈的分配等的开销确实要比迭代要大。然而一个“优化”的编译器(一般是专门为函数式语言设计的编译器),经常能对递归函数生成优异的代码,如上面的gcd尾递归(尾递归函数是指在递归调用以后再无其余计算的函数,其返回值就是递归调用的返回值)。对这种函数彻底没必要要进行动态的栈分配,编译器在作递归调用时能够重复使用当前的栈空间,从效果上看,好的编译器能够把上面递归的gcd函数改造为:
int gcd(int a,int b){ start: if (a==b) return a; else if (a>b){ a=a-b; goto start; } else{ b=b-a; goto start; } } 复制代码
即便是那些非尾递归函数,经过简单的转换也可能产生出尾递归代码。
在上述的讨论中,咱们都假定全部参数在传入子程序以前已经完成了求值,可是实际中这并非必须的。彻底能够采用另一种方式,把为求值的之际参数传递给子程序,仅在须要某个值得时候再去求它。前一种在调用前求值的方案称为应用序求值;后一种到用时方求值的方式称为正则序求值。正则序求值在宏这个概念中是天然而然的方式,前面讨论的短路求值、以及后面要讨论的按名调用参数也是应用的正则序求值,一些函数式语言中偶尔也会出现这种方式。
可是咱们来看一个例子:
#define MAX(a,b) ((a)>(b)?(a):(b)) 复制代码
若是我这么调用MAX(i++,j++),致使i和j都执行两次++,产生了两次反作用,这是咱们不肯意看到的结果。总结来讲,只有在表达式求值不会产生反作用的状况下正则序才是安全的。
从清晰性和高效的角度看,应用序求值一般会比正则序合适一些,一次大部分语言都采用如此的方式。然而也确实有一些特殊状况下正则序更高效一些,而应用序会形成一些错误出现,这种状况的出现时由于一些参数的值实际上并不会被须要,可是仍是被求值了,应用序求值有时也成为非惰性求值,好比下面的JavaScript代码就会是一个死循环:
function while1() {
while (true) { console.log('死循环')}
}
function NullFunction() { }
console.log(NullFunction(1,2,3,while1()));
复制代码
Scheme经过内部函数delay和force提供可选的正则序求值功能,这两个函数提供的其实是惰性求值的一种实现
惰性求值最多见的一种用途就是用来建立无穷数据结构
(define naturals (letrec ((next (lambda (n) (cons n (delay (next (+ n 1))))))) (next 1))) 复制代码
这样就能够用Scheme表述全部的天然数
本篇首先从表达式开始,介绍了表达式(语句)中的一些基本概念;而后从讨论了从汇编时代到结构化程序设计时代语言中的控制流程的演进以及发展;有了前面两个基础,后面就详细的介绍了程序中的三大基本流程控制结构顺序、选择、循环(递归和迭代)。