首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

13.4  新特征

Innovative Features

在13.1.1节里,我们列举了脚本语言的一些共有特点。

1.     同时支持批处理使用和交互式使用。

2.     简短的表达形式。

3.     缺少声明,简单的作用域规则。

4.     灵活的动态类型化。

5.     很容易访问其他程序。

图13.21  图13.20的HTML绘制的页面。

6.     丰富的模式匹配和串操作。

7.     高层数据结构。

这其中一些问题的细节将在下面几小节里讨论。特别的,13.4.1节将考虑脚本语言的命名和作用域问题,13.4.2节讨论字符串和模式操作,13.4.3节考察数据类型。上表里的项目(1)、(2)和(5)虽然也很重要,但不是特别困难或特别微妙,因此这里就不进一步考虑了。

13.4.1

13.4.1  名字和作用域

Names and Scopes

大多数脚本语言并不要求变量都有声明。有几个语言(特别是Perl和JavaScript)允许可选的声明,主要是作为供编译检查的文档。Perl可以在一种要求声明的模式 (use strict 'vars') 下运行。

无论有声明或没有声明,大多数脚本语言都采用动态类型化。各种值都是自描述的,因此解释器可以在运行时执行类型检查,必要时做值的强制。Tcl有些特殊,其中所有的值(包括列表)都用字符串表示,通过适当的语法分析支持算术、下标和其他操作。

嵌套和作用域方面的约定也有些不同。Scheme、Python、JavaScript和R提供的是嵌套子程序和静态(词法)作用域的经典组合;Tcl允许子程序嵌套,但采用动态作用域(后面还会讨论这一问题)。在PHP和Ruby里没有嵌套的命名子程序(或方法),但命名子程序是Perl中唯一允许嵌套的东西。另一方面,Perl和Ruby在提供一级匿名局部子程序方面却加入了Scheme、Python、JavaScript和R的行列。嵌套块在Perl里具有静态作用域;在Ruby里,嵌套块是其出现所在的命名作用域的一部分。Scheme、Perl、Python和R都为闭包里捕捉的变量提供了非受限的生存期,Ruby和JavaScript没有这种东西。PHP、R和主要的粘接语言(Perl、Tcl、Python和Ruby)都为信息隐藏提供了复杂的名字空间机制,以及从分离的模块里选择性地导入的机制。

未声明的变量的作用域

在采用静态作用域规则的语言里,没有声明将引出了一个很有意思的问题:当访问一个变量x的时候,怎么知道它是局部的、全局的,或者(如果作用域有嵌套)出自其间的什么地方。现存语言采取了几种不同的规定。在Perl里除非有明确声明,否则所有变量都是全局的。在PHP里它们都是局部的,除非明确导入(因为在这里没有作用域嵌套,所有导入都是全局的)。Ruby也只有两层实际的作用域,但如我们在13.2.4节已经看到的,该语言用名字的前缀区分它们,foo是局部变量,$foo是全局变量,@foo是当前对象(它的方法当时正在执行)的实例变量,而 @@foo是当前对象的类实例变量(由所有的兄弟实例共享)。(注意,如我们将在13.4.3节看到的,Perl用类型前缀字符表示类型。这方面的巨大差异是困扰程序员的潜在问题,尤其是那些需要在两种语言之间转来转去换的程序员。)

例13.43

Python的作用域规则

可能最有趣的作用域解析规则是Python和R的规则。在这两个语言里,被写的变量总被假定为局部的,除非明确地导出。对于在一个给定作用域里只读的变量,我们需要去找最接近的用写方式定义了这一变量的外围作用域。看一个例子,考虑图13.22里的Python程序,这里有一组嵌套的子程序,采用一系列缩进表示。主程序调用outer,outer调用middle,middle又进一步调用inner。在调用之前主程序写了变量i和j。outer读j(把它传给middle)但没有写,然而它确实写了i。因此outer读的是全局的j,但拥有自己的i,这个i与全局的i不同。middle读了i和j但都没写,因此必须到外围作用域去确定它们。它在outer里找到i,在全局的层次上找到j。函数inner也在自己里面写了全局的i。执行时程序将打印:

请注意,虽然从middle返回的三元组(由outer进一步外传,主程序打印)以2作为自己的第一个元素,全局的i仍以4为值,这是inner写的结果。还请注意,虽然outer里对i的写按正文出现在middle读i之后,但其作用域延伸到整个outer,包括middle的体。

图13.22  展示作用域规则的Python程序。这里的j和k各有一个实例,而i有两个实例,一个是全局的,另一个是outer里局部的。后者的作用域就是整个outer,而不是相应赋值之后的那个部分。global语句使inner可以访问最外层的i,可以向它写入而不会定义一个新实例。

例13.44

R的超赋值

有趣的是,在Python里,一个嵌套的子程序没办法去写一个属于外层但又非全局作用域的变量。在图13.22里,我们无法修改inner使之去写outer的那个i。R提供了另一种机制,因此可以有这种能力。在那里不是把i声明为global,而是提供了一种特殊的“超级赋值”运算符。正常运算符i <-4把值4赋给局部变量i,超级赋值i <<- 4把值赋给按照正常静态(词法)作用域规则找到的那个i。

例13.45

Tcl的作用域规则

按照另一种完全不同的考虑,Tcl做出了很不寻常的选择,它不仅采用了动态作用域,而且以一种异乎寻常的方式实现它。位于调用所在的作用域里的变量并不能自动访问,程序员必须明确地提出要求,如图13.23所示。这里的upvar和uplevel命令取一个可选的首参数,它指定位于动态链上的一个帧,或者用一个以井号(#)开头的绝对的值,或者采用例子里调用uplevel的形式,用一个值表示与当前帧的距离。如果忽略(像例子里upvar的情况),这一参数就默认为1。命令upvar访问指定帧里的一个变量,并给它指定一个局部名字。命令uplevel提供一个嵌套的Tcl脚本,让它在指定帧的上下文里执行,其方式就像是换名调用参数。在图中所给的例子里,我们用upvar获得了foo的i的一个局部名,用uplevel去执行一个使用全局a和b的命令。这一程序将打印出一个5和一个3。请注意,动态作用域的通常行为(自动获得最近创建的那个具有给定名字的变量,无论创建它的作用域是什么),在Tcl里并不可用。

图13.23  展示Tcl作用域规则的程序。upvar命令使bar可以访问位于其调用者的作用域里的变量i。uplevel命令使bar可以在其调用者的作用域里执行一个嵌套的Tcl脚本(这里的puts命令)。

Perl的作用域

Perl已经发展了许多年。开始时语言里只有全局变量。为了模块化,人们很快就把局部变量加了进来,这样,如果一个子程序里有变量i,就不必担心会不会修改了别的代码需要用的全局的i。不幸的是,由于向后兼容的需要,局部变量一开始就是按动态作用域定义的,这种行为方式一直持续到Perl加入静态作用域为止。所以该语言提供了两种作用域。

例13.46

Perl的动态和静态作用域

在Perl里,任何没有声明的变量都默认为全局的,用local运算符声明的变量具有动态作用域,用my声明的变量具有静态作用域。这一差异可以从图13.24里看到,其中的子程序outer里声明了两个局部变量lex和dyn,前一个是静态作用域的,后一个是动态作用域的。两者都用foo的第一个参数的副本初始化。(参数通过伪变量 @_ 传递,这个数组中的第一个元素是 $_[0]。)

在outer里面嵌套着两个词法上等同的匿名子程序,分别在 $lex和 $dyn的重新定义之前和之后。对它们的引用保存在局部变量sub_A和sub_B里。由于Perl的静态作用域从声明的位置开始直到所在块的结束,sub_A将看到全局的 $lex,而sub_B看到的是outer的 $lex。与此对应的是,由于声明 local $dyn出现在sub_A和sub_B的两个调用之前,它们看到的都是这一局部变量。这个程序将打印:

例13.47

在Perl里访问全局变量

图13.24  一个展示Perl作用域规则的程序。my运算符创建静态作用域的局部变量,local运算符创建同名全局变量的一个新的具有动态作用域的局部版本。静态作用域从声明那一点开始直到该块的词法结束,动态作用域从加工开始持续到该块的执行结束。

如果在某种情况下静态作用域规则导致访问一个位于嵌套的中间层次的变量,Perl允许程序员用our运算符去访问相应的全局变量。这里用名字our就是为了与my对应:

设计和实现

有关动态作用域的思考

我们在3.3.6节描述了动态作用域规则,说它引进了另一种新机制,其作用就是一直保持相应的名字为可见的,直到控制离开了创建这一新意义的作用域,无论其间程序执行到哪里。这一概念模型反映的是3.4.2节描述的关联表实现,而且如第133页的旁白所言,这可能就是Lisp的早期版本采用了动态作用域规则的根源。

Perl文档提出了一种语义上等价但在概念上却不同的模型。在那里没有说local声明引进了新的变量,其名字正好掩盖了以前的声明。Perl的说法是:只是在全局的层次上存在一个变量,在遇到新声明时它以前的值被保存起来了,直到控制离开新声明的作用域时自动恢复。这一模式反映了Perl下面的实现方式,其中采用的是一个中心控制表格(如3.4.2节描述)。为了与这一模型和实现一致,Perl不允许用local运算符去创建非全局变量的动态实例。

这里有z的一个词法实例,x和y各有两个实例,其中一个实例是全局的,另一个位于嵌套的中间层次。在中间层也有z的一个实例。内层作用域在执行其print语句时,将找到中间层的那个y。但另一方面,由于第6行的our运算符,它将找到全局的x。那么z的情况怎么样?规则要求我们从静态作用域开始,忽略local运算符。这样,由于内层作用域里的our,用到的就是全局的z。看到这些以后,我们再看看动态(local)重新声明的z是否起了作用。如果它确实起了作用,这个程序就会打印出1, 2, 3。实际情况是内层作用域的our声明对于这个程序没有影响。如果只有x被声明为our,我们或许还想使用全局的z,但随后就会发现位于中间作用域里z的动态实例。

13.4.2

13.4.2  串和模式匹配

String and Pattern Manipulation

当我们在2.1.1节第一次考虑正则表达式时,就提到许多脚本语言和相关工具使用了这种记法的许多扩展版本。有些扩展仅仅是为了方便,另外一些则提升了记法的表达能力,使我们能生成(匹配)非正则集的串。还有一些扩展不过是为了把这一记法与语言的其他特征联系起来。

我们已经看到了sed(图13.1)、awk(图13.2和图13.3)、Perl(图13.4和图13.5)、Tcl(图13.6)、Python(图13.7)和Ruby(图13.8)里的扩展正则表达式的一些例子,还给出了有关grep的注记(这是Unix下独立的模式匹配工具,见第729页旁白。)

虽然扩展的正则表达式(简称“RE”)有许多不同实现,采用的语法各不相同,但大多数实现都可以归入主要的两组。第一组包括awk、egrep(grep的几个不同版本中使用最广泛的一个),以及老版本的Tcl。它们实现的都是POSIX标准 [Int03b] 中定义的RE。属于第二组的语言都是追随Perl引领的潮流。Perl提供了很大的一集扩展,有时被称为“高级RE”。与Perl类似的“高级RE”也出现在PHP、Python、Ruby、JavaScript、Emacs Lisp、Java、C# 和最新版本的Tcl里。这种东西也可以在C++和其他语言的第三方程序包里看到。还有几种工具提供的是所谓的“基本RE”,包括sed、经典的grep和老的Unix编辑器等,其功能还不如egrep等的RE。

一些语言和工具里的正则表达式与语言其他部分紧密联系在一起,采用特殊的语法形式和内部运算符,值得提出的包括sed、awk、Perl、PHP、Ruby和JavaScript。这些语言里的RE通常用斜线符界定,虽然在有些情况下也可以接受其他界定符(事实上Perl为另外几个界定符提供了稍微不同的语义)。在大部分其他语言里,正则表达式用普通的字符串表示,操作的方式是把它们送给库函数。下面我们将考虑POSIX和高级RE的一些细节。我们也按Perl的形式,用斜线符作为界定符。这一讨论不可能完全,在Perl全书里有关正则表达式的内容有近80页 [WCO00,第5章]。对应的Unix的man页的内容也超过了20页。

设计和实现

正则表达式的自动机

POSIX正则表达式通常用2.2.1节描述的构造方式实现。首先把RE转换为NFA而后再转换到DFA。像Perl提供的那类高级RE通常采用在明显的NFA上回溯的方式实现,一般不用NFA-DFA转换,因为这一转换不能维护某些高级RE扩展(特别是例13.62—13.65描述的捕捉机制)[WCO00,第197—202页]。有些实现首先用DFA确定确实存在匹配,而后通过NFA或回溯去进行实际匹配。这一策略可以只在认为值得做的时候才付出使用较慢自动机的代价。

设计和实现

grep命令和Unix工具的诞生

从历史上看,正则表达式工具的根是在ed编辑器里的模式匹配机制,其时间应回溯到Unix开发的早期。1973年Doug McIlroy是Unix诞生的那个部门的头,当时他在做一个计算机语音合成项目。作为该项目的一部分,他需要用编辑器在一个在线词典里检索某些具有挑战性的单词。这一过程既冗长又很无聊。在McIlroy的请求下,Ken Thompson把ed里的模式匹配器提取出来做成了一个独立工具。他把自己的造物称为grep,源自编辑器的命令序列g/re/p。这里的g表示全局,/ / 是去搜索正则表达式(re),p是打印 [HH97a,第9章]。

Thompson的这一创造是Unix的一大批基于流的工具集里最早的成员之一。正如我们在13.2.1节(第680页)所描述的,这种工具很快就和管道的概念组合在一起,用于执行各种各样的过滤、变换和格式化操作。

例13.48

POSIX RE的基本操作

POSIX正则表达式

就像形式语言理论里的“真正的”正则表达式一样,扩展的RE也支持并置、选择和Kleene星号。括号用于分组:

例13.49

POSIX RE的更多量词

  

能用的还有另外几个量词(是Kleene闭包的推广):? 表示0或1个出现,+ 表示一次或多次重复,{n} 表示恰好n次重复,{n,} 表示至少n次重复,{n,m} 表示nm次重复。

例13.50

零长度断言

例13.51

字符组

两个零长度断言 ^ 和 $ 分别与目标串的开始和结束匹配。这样,虽然 /abe/ 将与abe、abet、babe和label匹配,/^abe/ 只与其中前两个匹配,/abe$/ 只与第一个和第三个匹配,而 /^abe$/ 只与第一个匹配。

作为 /a|b|c|d/ 的简写,扩展RE允许用方括号刻画字符组:

也允许写区间:

例13.52

圆点(.)字符

在字符组之外,圆点(.)与任何非换行符的字符匹配。以表达式 /b.d/ 为例,它不但与bad、bbd、bcd等匹配,也与b:d、b7d及许许多多其他东西匹配,包括那些中间字符不可打印的字符序列。在Perl的接受Unicode的版本里,存在着成千成万的选项。

例13.53

字符组里的否定和引号(反斜线)

写在字符组开头的 ^ 表示否定,这样的组只与不在其中出现的那些字符匹配。这样 /b[^aq]d/ 将与 /b.d/ 匹配的所有字符串匹配,只是要除掉bad和bqd。在字符组里也可以写 ^、\ 或-,为此需要在它们前面写一个反斜线符。在字符组外也需要用反斜线符保护所有的特殊字符,包括 | ( ) [ ] { } $ . * + ? 10。要匹配一个反斜线符,就需要把它双写:

例13.54

POSIX的预定义字符组

POSIX标准里有一些预定义字符组表达式。如在图13.18里看到的,表达式 [:space:] 可用于匹配空格,针对标点符号有 [:punct:]。这些字符组的确切定义依赖于局部字符集和语言。还请注意,这些表达式只能用在构造字符组的内部,它们本身并不是字符组。举例说,C变量名可能用 /[[:alpha:]_][[:alpha:] [:digit:]_]/ 匹配,或者写得稍简单些,/[:alpha:]_][[:alnum:]_]/。另外有语法(这里不讨论了)使字符组可用于捕捉Unicode的整编序列(collating序列,多字节序列,例如一个字符和与之关联的调号),它们是整编在一起的,就像是一个字符。Perl还允许使用这些特殊字符组的稍微简单些的形式。

例13.55

Perl的RE匹配

Perl扩充

对RE的扩充是Perl的核心部分。内部运算符 =~ 用于检查匹配:

被匹配的串也可以不描述,此时Perl提供了 $_ 作为默认匹配的伪变量:

例13.56

Perl匹配的否定

回忆一下(如在13.2.2节 [第687页] 提到的),在对一个文件里的各行迭代时将自动设置 $_,它还被作为for循环的默认下标变量。

当没有匹配时 !~ 运算符返回真:

例13.57

Perl的RE代换

二元“混合缀”运算符s///用于表示代换,它把位于第一和第二个斜线符之间的串代换为位于第二和第三和线性符之间的串。

还有,如果没给出被代换的左部,s///就匹配和修改 $_。

修改符和换意序列

例13.58

RE匹配的尾随修改符

通过在闭界定符之后加一个或几个字符,就可以修改匹配或代码的行为。例如,在匹配后边加一个i使之对大小写不敏感:

图13.25  Perl正则表达式里的换意序列。表格最上面部分的序列表示单个字符,中间部分表示0长度断言,下面部分是内部定义的字符类。

在代换后加一个g使之代换掉正则表达式的所有出现:

要在一个多行的字符串里匹配,尾随的s使圆点(.)能匹配其中的换行符(通常情况下不能)。尾随的m使 $ 或 ^ 分别匹配正好在换行符之前或之后的位置。尾随的x使Perl忽略模式里的注释和空白。这样,如果需要,就可以把特别复杂的表达式分开写在几行里,再加上注释等。

按照C及其相关语言的传统(第366页例7.73),Perl页允许在RE里用反斜线开头的换意序列写非打印字符。图13.25最上面总结了这些序列。除了 ^ 和 $,Perl还提供了另外几个0长度断言,在图的中部列出了这些断言。其中的 \A 和 \Z 换意序列与 ^ 和 $ 不同,即使是在多行匹配中使用了m修改符,它们也只与串的开头和结尾匹配。最后,Perl还提供了一些内部的字符组,在图的下部给出。

它们既可以用在用户定义(方括号括起)的字符组之外,也可以用在其内部。还请注意 \b 用在字符组内外的不同意义。

贪心匹配和最小匹配

例13.59

贪心匹配和最小匹配

RE匹配的常用规则有时被称为“最左最长”规则:如果一个模式可以在一个串里的多处匹配,所选的匹配将是在这个串里最早遇到的可能位置开始的那个匹配,而且一直延伸得尽可能远。举例说,对于串abcbcbcde,模式 /(bc)+/ 有6个可能的匹配方式:

例13.60

HTML标题的最小匹配

其中第三个是“最左最长”匹配,也称为贪心匹配。然而在有些情况下,我们需要的可能是得到“最左最短”的匹配或最小匹配。这对应于上面的第一种情况。

我们在例13.22(图13.4)中看到过更实际的例子,其中包含下面代换:

假定HTML输入的形式是良形式的,其中的标题没有嵌套,那么这一代换就会删除从串(隐含地是 $_)开头到第一个内嵌标题结束位置之间的所有内容。它做这件事的方式是用 *? 限定符而不是通常的 *。如果没有这个问号,这一模式将匹配(因此代换也将删除)直至串中最后一个标题结束的所有内容。再说一次,尾随的修改符s使这里的标题可以跨过多行。

一般而言,*? 将匹配能使最终匹配成功的尽可能少的前部子表达式的实例。与它类似,+? 至少匹配一个实例,但也仅匹配使整个匹配成功所必须匹配的那么多实例。?? 将匹配0个或一个实例,但优先匹配0个。

变量内插和捕获

例13.61

扩充RE的变量内插

就像双引号括起的字符串一样,Perl还支持变量内插。任何不是正好位于竖线、闭括号或串结束之前的美元符号,都被作为一个Perl变量名的开始。在把这种模式作为正常的正则表达式传给求值器之前,先要把作为这种变量的值的串展开。这就使我们可以写出代码,使之可以在运行时生成所需的模式:

请注意这里 $ 扮演的两种不同角色。

例13.62

扩充RE的变量获取

也可以让信息流向另一方向,用这种方式取出正则表达式中变量的值。在前面图13.1的sed脚本里我们已经看到过一个简单的例子:

下面是Perl的一种等价语句形式:

在Perl 的正则表达式里,每个括号括起的片段都能捕获与之匹配的正文,这样捕获的正文可以在替换的右部通过 \1、\2等去引用。在表达式之外也可以通过 $1、$2等使用它们(直至执行下一个代换):

例13.63

扩充RE的逆向引用

我们甚至可以在当前正则表达式的后面引用已经捕获的串,这样的串称为逆向引用:

例13.64

解剖浮点数文字量

这里用 /2来强调HTML闭标签必须与相应的开标签匹配。

当然,完全可以同时捕获多个串:

就像在前面的例子里一样,这里的编号对应于左括号的出现,从左到右读。对于输入 –123.45e-6,我们就有:

例13.65

前缀、匹配的后缀的隐式获取

请注意,由于 $3和 $4 是相互替代的,因此可以保证两者之中只有一个被设置。还应注意,虽然为了结组需要第6对括号(它带有 ? 限定符),这里并不需要它所捕获的串。

对于简单匹配,Perl还提供了名字为 $`、$& 和 $' 的伪变量,用它们分别指称最近匹配的串之前、之中和之后的部分:

对于输入“now is the time”,这段代码就输出:

检查你的理解

44.   使用最广泛的采用动态作用域规则的脚本语言是什么?

45.   请总结一下Perl、PHP、Ruby和Python中用于确定未声明变量的作用域的策略。

46.   请描述Perl关于动态作用域的变量的概念模型。

47.   请列出在POSIX正则表达式中可以找到的,但在形式语言理论的正则表达式(2.1.1节)里并没有的主要特征。

48.   请列出Perl的正则表达式里有,但POSIX正则表达式里没有的主要特征。

设计和实现

编译正则表达式

在被用作匹配的基础之前,必须先把正则表达式编译为一个确定性的或者非确定性(回溯式)的自动机。明显是常量的模式只需编译一次,或者是在程序装入的时候,或者是在第一次遇到它的时候。当然,包含内插串的模式通常需要在每次遇到时重新编译,因此具有潜在的显著运行开销。如果程序员知道内插变量绝不改变,因此可以禁止重新编译,那么就可以在正则表达式的最后附上一个o修改符。在这种情况下,该表达式将在第一次遇到时编译一次,以后再也不重新编译了。对那些必须在某些时候重新编译,但不必每次都做的表达式,程序员可以用qr运算符强迫对模式做重新编译,而后就可以重复地高效地使用这样产生的结果了:

49.   请解释在Perl一类的正则表达式里的查询修改符(跟随在正则表达式的结束界定符之后的字符)的作用。

50.   请描述Perl一类的正则表达式里三类不同的换意序列。

51.   请解释贪心匹配和最小匹配之间的差异。

52.   请描述正则表达式中捕获的概念。

13.4.3

13.4.3  数据类型

Data Types

例13.66

Ruby和Perl的强制

正如我们已经看到的,脚本语言通常并不要求(甚至不允许)声明变量的类型。大多数语言都要执行广泛的运行时检查,以保证不会以不合适的方式去使用各种值。有些语言(例如Scheme、Python和Ruby)在这种检查方面更加严格,如果程序员希望从一种类型转换到另一类型,就必须明确写出来。如果在Ruby里输入下面语句:

运行时会得到一条错误信息:“In '+': failed to convert Fixnum into String (TypeError)”。Perl在这方面宽松得多。如前面例13.2的程序:

将打印出43和7。

例13.67

Perl的上下文和强制

一般而言,Perl(Rexx和Tcl与之类似)采取的立场是:程序员应该检查他们所关心的各种错误,在没有相关的检查时程序应该做某种合理的事情。譬如说,Perl愿意接受下面的程序(虽然在用 –w编译开关进行编译时它会打印出一个警告):

例13.68

Ruby的显式类型转换

这里的 $a[4] 没有初始化,因此具有值undef。在算术的上下文中(作为 + 的运算对象),串 "1" 求出值1,undef求出值0。相加后得到1,再转换到字符串之后打印输出。

要写出与之对应的Ruby代码片段,就需要关心更多的东西。在对a做下标操作前必须保证它引用的是数组:

如果没有第一行(也没用其他方式初始化),第二行就会生成一个“无定义变量”错误。在这一赋值之后a[3] 是个字符串,a的其他元素则都是nil。我们不能做一个串和nil的拼接,也不能做它们之间的加法(在Ruby里这两个运算都用 + 表示)。如果希望做字符串拼接,而a[4] 可能是nil,那么就必须写:

如果希望做加法,那么就必须写:

从这些例子可以看出,Perl(Tcl也类似)采用变量的值模型,Scheme、Python和Ruby采用变量的引用模型。PHP和JavaScript、Java一样,对基本类型的变量采用值模型,对对象类型的变量采用引用模型。这一差异对于PHP和JavaScript的影响并不像在Java里那么大,因为同一个变量完全可以在某个时刻保存一个基本值,在另一时刻保存一个对象引用。

数值类型

如我们在13.4.2节看到的,脚本语言通常都为字符串和模式操作提供功能丰富的各种机制。这方面的语法和内部记法的约定方面可能差别很大,但作为其基础的功能却是相当统一的,都受到Perl的广泛影响。不同语言之间对数值类型的基础支持的差异更大一些,但相关的编程模型也是相当统一的。作为最粗略的近似,它们都鼓励用户把数值看作“简单的数”,不要求过多关心定点数或浮点数,或者可用精度限制方面的问题。

在系统内部,JavaScript的数都是双精度浮点数;Tcl的数都是字符串,只是在需要做算术运算时才转换为浮点数(并转换回来);PHP用整数(保证至少有32位)和双精度浮点数。Perl和Ruby在此之上增加了任意精度(多字)整数,有时被称为大数。Python也有大数,它还支持复数。Scheme有上述所有的东西,再加上精确的有理数,用一对 <分子, 分母> 表示。对于所有语言,如果要求做不同表示之间的算术运算或者将要出现溢出,解释器都会做必要的“向上转换”。

Perl特别注意隐藏不同数值表示之间的差异,而大多数其他语言都允许用户去确定实际使用的是什么,虽然这件事很少需要做。Ruby在有关数的不同表示形式的存在性方面表现得最明确,它的类Fixnum、Bignum和Float(双精度浮点数)的内部方法集合相互有重叠,但却不完全一样。特别的,整数有迭代器方法,但是浮点数没有;浮点数有舍入和误差检查方法而整数没有。Fixnum和Bignum都是Integer的后裔。

复合类型

在C、Fortran和Ada一类编译语言里,选择类型构造符时在很大程度上考虑的是高效实现。特别是数组和记录都有直截了当的高效实现方式,在第7章里已经研究过这一问题。然而在脚本语言里效率并不是那么重要。语言设计师在选择类型构造符的问题上更多考虑的是容易理解,而不是纯粹的运行时性能。特别的,大多数脚本语言里都特别强调映射,也常称为字典、散列表或关联数组。从第三个名字可以猜到,映射通常采用散列表实现。散列表的访问时间是O(1),但与编译的数组和记录相比,其常量因子大得多。

例13.69

Perl数组

在Perl这一最老的广泛使用的脚本语言里,主要的复合类型(数组和散列表)是从awk继承来的。它也同样采用变量名的前缀指明类型,$foo是标量(数、布尔、串或指针 [Perl称为“引用”]),foo表示数组,%foo是散列表,&foo是子程序,简单的foo是文件句柄或I/O格式,具体是什么依赖于上下文。

Perl的常规数组用方括号写下标,整数下标从0开始:

例13.70

Perl散列表

请注意这里引用整个数组时用 @ 前缀,在引用其中一个(标量)元素时要用 $ 前缀。数组能够自动扩展,如果给超出范围的元素赋值,就会自动建立一个更大的数组(代价是动态存储分配和元素复制)。未初始化的元素以undef为默认值。

这里采用在花括号里写字符串的方式为散列表描述下标:

散列表也是自动扩展的。

例13.71

Python和Ruby的数组和散列表

记录和对象一般都是从散列表构造起来的。在C程序员写fred.age = 19的地方,Perl程序员要写 $fred{"age"} = 19。在面向对象的代码里,$fred更像是一个引用,此时需要写 $fred->{"age"} = 19。

Python和Ruby都像Perl一样提供了常规的数组和散列表,但对这两种情况都用方括号写下标,区分数组和散列表的方式是分别用方括号或花括号界定符的形式写初始式:

例13.72

Ruby的数组访问方法

(这是Ruby的语法,Python里用 : 而不是 =>)。

作为纯的面向对象语言,Ruby把下标操作定义为调用方法 [](get)和 []=(put)的语法包装:

例13.73

Python的类型

Python除提供了数组(它称为列表)和散列表(它称为字典)之外,还提供了另外两种复合类型:元组和集合。元组在本质上是一种不变化的列表(数组),其初始式用圆括号而不是方括号:

元组的访问效率可能比数组更高,因为其不变化的性质可以消除大多数有关越界和重新确定大小的检查。这种结构还作为多路赋值的基础:

这个例子里的括号可以不写,因为逗号的结组能力强于赋值运算符。

设计和实现

Perl的类型组(typeglob)

Perl的全局名字可以有多个相互独立的意义。例如,完全可以在一个程序里同时使用 $foo、@foo、%foo、&foo,以及foo的两个不同意义。为了维持这样的多重意义,Perl在foo的符号表项和foo可能具有的多重值之间放入了一层间接操作。这种中间结构称为类型组,对foo的每个意义有一个槽位,它自己还有一个名字 *foo。通过对类型组的操作,Perl高级程序员可以实际地修改解释器在运行时查看名字的符号表。最简单的使用是创建别名:

执行这个语句之后,a和b就变成不可区分的,它们都引用同一个类型组,而且对它们任何一个的(任何意义的)修改都能通过另一个看到。Perl还支持选择性的别名,也就是让一个类型组的某个槽位指向另一类型组的某个值:

Perl的反斜线运算符(\)创建指针。在执行了这个语句之后,&a(a作为一个函数的意义)就与 &b 一样了,但a的其他意义保持不变。除了其他用途外,选择性别名还用于实现从Perl程序库里导入名字的机制。

Python的集合很像字典,但它并不映射到任何有趣的东西,只是简单地用于确定元素的存在与否。与字典不同的是它们还支持并集、交集和差集运算:

例13.74

Python的集合

例13.75

PHP、Tcl和JavaScript的合成类型

PHP和Tcl的复合类型更简单,它们都不区分数组和散列表,其数组也就是散列表,只是程序员选择用数值作为关键码而已。JavaScript采用了类似简化,统一了数组、散列表和对象。访问对象成员(JavaScript称为性质)的常见形式obj.attr也就是obj["attr"] 的语法包装。因此对象也就是散列表,数组就是以整数作为属性名的散列表。

例13.76

Python和其他脚本语言的多维数组

在大多数脚本语言里,创建高维类型也是直截了当的。完全可以定义(引用)散列表的数组、(引用)数组的散列表,等等。换个方式,也可以创建采用组合对象作为关键码的方式实现“平坦化”的散列表。Python的元组特别适合用在这里:

这种写法从观感和功能上都和多维数组一样,效率当然低一些。现在有Python的扩充库提供了更高效的同质数组,只是所使用时的语法形式难看一点。在数值和统计脚本语言里,例如Maple、Mathematica、Matlab和R,都为多维数组提供了更广泛的支持。

上下文

我们在7.2.2节定义了类型相容性的概念,它确定在静态类型的语言里,哪个类型可以用在哪个上下文中。在这一定义里,术语“上下文”指的是有关一个值将如何使用的信息。以C为例,我们可能说在声明

里,右边的3出现在一个期望浮点数值的上下文中。C编译器将对3做强制转换,把它做成一个double而不是int。

在7.2.3节里,我们进一步定义了类型推理的概念,它使编译器可以基于一个表达式的组成部分的类型,在某些情况下基于其出现的上下文,确定这个表达式

的类型。ML及其后裔是这方面最极端的例子,其中采用了一种非常复杂的机制去确定大部分对象的类型,不需要声明。

例13.77

Perl的标量和列表上下文

在这两种情况下(类型相容性和推理),上下文信息都仅仅在编译的时候使用。Perl扩展了上下文的概念,将其用于驱动运行时的决策。说得更具体些,在编译时,对于每个运算对象,Perl里的每个运算符都要去确定应该将其解释为一个标量还是一个列表。与之对应的,每个参数(它本身可能又是嵌套的运算符)在运行时也能知道自己处于什么样的上下文中,并因此确定应该展示什么样的行为。

作为一个简单的例子,赋值运算符(=)基于其左部的类型为右部提供标量或者列表的上下文。这个类型在编译时总是已知的,而且通常对关心它的读者也很明确,因为左部也就是一个名字,其前缀字符或者是美元符号($)表示标量上下文,或者是 $ 或 % 符号标准列表上下文。如果我们写:

Perl标准的gmtime() 库函数将以字符串形式返回一个时间,写出来大致就是 "Tue Mar 15 21:09:39 2005"。而在一方面,如果我们写:

例13.78

用wantarray确定调用上下文

同一个函数将返回 (39, 09, 21, 15, 2, 105, 2, 73),这是一个包含8个元素的数组,表示秒、分、时、一月中的日期、一年中的月份(一月 = 0)、年份(从1900年起算),一周中的日期和一年中的日序数。

那么gmtime怎么知道应该做什么呢?它是通过调用内部函数wantarray。如果当前函数调用位于列表上下文中,对wantarray的调用就会返回true,在一个标量上下文中就会返回false。按照习惯,如果函数在列表上下文里的调用出了错,它们通常返回一个空数组来表示这一情况,如果是在标量上下文里的调用出错,它就返回无定义值(undef):

13.4.4

13.4.4  面向对象

Object Orientation

虽然Perl 5不是面向对象的语言,但它提供了一些特征,使人可以采用面向对象的风格写程序11。PHP和JavaScript有着更清晰、看起来也更习惯的面向对象特征,但两者也都允许程序员采用更传统的命令式风格。Python和Ruby则是明确而统一的面向对象语言。

Perl采用变量的值模型,对象都是通过指针访问的。在PHP和JavaScript里,变量可以保存一个基本类型的值,也可以保存一个复合类型的对象的引用。与Perl不同,在这些语言里都没有提供去说一个引用本身的方法,只能去说它所引用的对象。Python和Ruby都采用了统一的引用模型。

Python和Ruby里的类本身也是对象,就像在Smalltalk里那样。在PHP里类也就是类型,就像C++、Java和C# 里的情况。Perl的类只不过是另一种看程序包(名字空间)的方式。JavaScript很值得注意,它只有对象而没有类,其继承是基于所谓原型的概念。

Perl 5

例13.79

Perl里的简单类

Perl 5对于对象的支持也就是两样东西:(1) 将一个引用与一个程序包关联的一种称为赐福(blessing)的机制;(2)一种方法调用的特殊语法形式,它自动把一个对象引用或包的名字作为函数的第一个参数。虽然任何引用都可以赐福,常见的习惯就是像例13.70那样用一个散列表,使其元素可以命名。

作为一个非常简单的例子,我们考虑图13.26里的Perl代码。在这里定义了一个包Integer,它扮演着类的角色。这个包里有三个函数,其中一个(new)的意图是用作构造函数,另外两个(set和get)用作访问函数。有了这个定义后,我们就可以写:

例13.80

Perl的方法调用

这里的Integer->new和new Integer都不过是调用Integer::new的语法包装,而且以包(类)的名字字符串作为该调用附加的第一个参数。在函数new的第一行里,我们把这个字符串赋给变量 $class。(shift操作返回伪变量@ [函数的实在参数表] 里的第一个元素,并将其余参数移位,使之可以被以后的shift看到。)然后创建了一个新的散列表,将其保存到局部变量 $self,并调用bless操作将它关联于适当的类。第二个shift操作提取出所创建的整数的初始值(如果有,这里的“或”运算符 || 使得在没提供实参的情况下给定值0)。我们把这个初值赋给 $self的val域,用的是Perl对指针间接操作后用下标访问散列表内部的语言形式。最后返回对这个新创建对象的引用。

一旦为一个引用赐福后,Perl就允许对它使用方法调用的语法,直接写c1->get() 或get c1() 作为Integer::get($c1) 的语法包装形式。请注意,这一调用将把一个引用(而不是包的名字)作为附加的第一个参数。有了上面 $c1、$c2、$c3的声明之后,下面代码

图13.26  在Perl里做面向对象的程序设计。bless一个引用(对象)到包Integer里之后,Integer的函数就像是对象的方法了。

将打印

例13.81

Perl的继承

与Perl的平常情况一样,如果参数表为空,括号也可以忽略不写。

Perl里的继承通过数组 @ISA的方式获得,这一数组在包的全局层次上初始化。扩充前面的例子,我们定义一个继承Integer的Tally类:

例13.82

用use base继承

t1的inc方法将能如我们所期望的那样工作。然而,当Perl看到调用Tally::new或Tally::get时(这两个东西在这个包中都没有),就会利用 @ISA数组到其他可能找到所需方法的包里去找。Perl支持多重继承。有可能通过Tally调用new而不是通过Integer调用,这一情况也解释了为什么在图13.26里要用shift操作去获取类名。如果在那里明确地用 "Integer",在创建Tally时就会得不到所需的行为。

Perl程序里,最常见情况是在独立的模块(文件)里写包(也就是类)的声明。在这种情况下,除了修改 @ISA外还需要导入对应于超类的模块。标准的base为这种组合操作提供了一种方便的语法形式,最好是用这种形式来写继承关系:

PHP和JavaScript

虽然Perl提供的机制足以用于进行面向对象的程序设计,但它所采用的动态查找方式使这种程序比等价的命令式程序慢一些,而且其描述方式也不够优美。PHP和JavaScript都是更面向对象的。

PHP 4提供了许多面向对象特征,PHP 5对这方面又做了广泛修改。该语言的新版本提供了(类类型)变量的引用模型、接口和混入式继承、抽象方法和抽象类、静态和常量成员,以及类似于Java、C# 和C++的访问控制(public、protected和private)。与本小节讨论的其他语言都不同,PHP里的类声明必须包含其中所有成员(域和方法)的声明,因此给定类的成员集合在定义之后就不能改变了(当然可以定义包含额外成员的派生类)。

JavaScript在提供对象(以及继承和动态方法指派)方面采用了一种很不寻常的方式,在这里没有类的概念。事实上,JavaScript对象的函数是第一级的实体,一个方法也就是一个函数,只是被一个对象引用作为它的性质(成员)。在调用o.m时,在m所引用的函数的执行过程中,关键字this一直引用o。与此类似,在调用new f时,在f的执行期间this一直引用一个新创建的(初始化为空的)对象。这样,JavaScript的构造函数也就是一个函数,其作用就是把值(域和方法)赋给新创建的对象。

与每个构造函数f相关联的有一个对象f.prototype。如果对象o是f创建的,只要我们企图使用一个o没提供的属性,JavaScript就会去查看f.prototype。从效果上看,o从f.prototype继承了所有它没有覆盖的东西。这种原型属性通

图13.27  JavaScript的面向对象程序设计。函数Integer被用作构造函数,给它的原型对象赋成员也就是建立方法。在用Integer创建对象,而该对象没有自己对应的方法时,就会使用这些方法。

例13.83

JavaScript里的原型

常被用于维持有关的方法集合。当然也可以用于维持一些常量或在其他语言里称为“类变量”的东西。

图13.27展示了这种原型的使用,这里的代码大致上等价于图13.26里的Perl代码。函数Integer被作为一个构造函数,给Integer的属性赋值也就是为Integer构造的对象建立方法。使用这个图里的代码,我们可以写:

这一代码将打印出

例13.84

在JavaScript里覆盖实例方法

有趣的是,在这里没有正式的类概念,也意味着我们可以对一个个对象做方法和域的覆盖:

如果前面例子里的代码都没变,这段代码将打印:

要得到继承的效果,我们可以写:

例13.85

JavaScript的继承

这一代码将打印出5。

Python和Ruby

例13.86

Python和Ruby的构造函数

正如前面提过的,Python和Ruby都明确是面向对象的语言,它们都采用变量的引用模型。与Smalltalk类似,这两个语言都有一个类层次结构,其中的类本身也用对象表示。Python的根类称为object,而Ruby的根类是Object。

在Python和Ruby里,每个类都有唯一一个明确的构造函数,它不能重载。Python里的这个函数是 _ _init_ _,Ruby里是initialize。要创建一个新对象,Python的写法是my_object = My_class(args),Ruby的写法是my_object = My_class.new(args)。两种情况里args都被传递给构造函数。要得到重载的效果,采用不同成员或参数类型进行初始化,就必须做出一种安排,使构造函数可以明确地检查自己的实在参数。我们可以采用与Perl(图13.26里的new例程)和JavaScript(图13.27里的Integer例程)类似的编程方式。

例13.87

在Python和Ruby里命名成员的方式

在有关类的内容(成员)方面,Python和Ruby都比PHP或各种传统面向对象语言更灵活。可以给Python对象增加新的域,为此只需直接给它们赋值my_object.new_field = value。然而这里的方法集合则是在类的第一次定义时就固定下来的。在Ruby里,只有方法是类外可见的(要访问域就必须用“put”和“get”方法),所有的方法都是显式声明的。但是允许修改现存方法的定义,添加或覆盖方法。程序员也可以针对一个个对象做这件事。由于这些情况,同一个类的两个对象完全可能有不同的行为。

Python和Ruby在许多其他方面都是不同的。在Python里方法的初始参数必须明确写出,按照常规其名字就是self。而在Ruby里self是个关键字,它所代表的参数是不可见的。在Ruby里,任何一个以 @ 符号开头的变量都是当前对象的域,而在Python的方法里,使用对象的成员都必须明确写出对象的名字。作为一个例子,在Python里必须写self.print(),只写print() 是不够的。

Ruby的方法可以是public、protected或private12。Python的访问控制纯粹只一种约定,这里的方法和域都是全局可见的。最后,Python还有多重继承,Ruby只有混入式继承,一个类不能从多于一个前辈得到数据。然而,在另一方面Ruby又与大多数其他语言不同,在这里一个接口(混入物)不仅可以定义方法的签名,也可以定义它们的实现(代码)。

检查你的理解

53.   请对比Perl和Ruby在错误检查和报告方面的想法。

54.   请比较常用脚本语言里的数值类型与C或Fortran一类语言里的情况。

55.   什么是大数(bignum)?那些语言支持它?

设计和实现

可执行的类声明

Python和Ruby在把类声明看作可执行代码方面的立场都很有趣,在这两种语言里,加工一个声明将执行其中的代码。除了用于其他目的,我们还可以用这种机制得到条件编译的效果:

这里不是每次调用在get内部的计算代价很高的函数expensive_function,而只是事先计算它一次,得到一个适当的更特殊的get版本。

56.   什么是关联数组?它还有哪些其他的名字?

57.   为什么大多数脚本语言里都没有直接支持记录?

58.   Perl里的类型组是什么?它有什么用?

59.   请说明Python的元组和集合类型。

60.   请解释PHP和Tcl里数组和散列表的统一。

61.   请解释JavaScript里数组和对象的统一。

62.   请解释为什么可以在Python里用元组和散列表模拟多维数组。

63.   请解释Perl里上下文的概念。为什么它与类型相容性和类型推理有关?这一语言的运算符定义的两个主要上下文是什么?

64.   请比较Perl 5、PHP 5、JavaScript、Python和Ruby所采纳的面向对象途径。

65.   在Perl里赐福一个引用是什么意思?

66.   什么是JavaScript里的原型?它们有什么用处?

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论