作者adxis (Acquire higher)
看板Translate-CS
标题[翻译] Eiffel Tutorial (from tecomp)
时间Wed Aug 28 13:10:30 2013
github markdown:
https://github.com/yangacer/translation/blob/master/eiffel/tutorial.md
==== 换行怪怪的文字版 ====
中文版译自
http://tecomp.sourceforge.net/index.php?file=doc/lang/tutorial.txt
# 简介
本文件提供一个快速的 Eiffel 简介,涵盖了撰写 Eiffel 程式的基本需求。
就如着名的 "The C programming Language" 作者 Brian Kernighan 与 Denis Ritchie
所言,学习程式语言最好的方法就是用该语言写程式。因此我们会专注在一些简单但实用
的程式。大部分简介中的程式为上述名着中范例的 Eiffel 版本。
以下并非程式设计简介而是针对如何以 Eiffel 撰写程式,因此会假设读者已具备撰写
C, C++, 或 java 的基础能力。
# Hello world
我们的第一个程式仅印出
```
Hello, world
```
要达到这个目的的 Eiffel 程式如下
```
class
HELLO
create
make
feature
make
do
io.put_string("Hello, world")
io.put_new_line
end
end
```
所有 Eiffel 程式码都存在於类别 (class) 之中。
各个类别的程式码存在於单一档案。以上的程式码
必须被写到一个 "hello.e" (全小写) 的档案中。大小写对 Eiffel 来说是相同的。然而
类别名称通常都是以大写表示,而特徵 (feature) 通常是以小写表示。
编译与执行 Eiffel 程式依系统与编译器不同。本文的资讯以 UNIX 环境下的 tecomp 为
准。为编译该程式,编译器需要额外资讯,这些资讯可透过 ace-file 取得。例如前面程
式
的 ace-file "hello.ace" 可含有以下资讯
```
root
HELLO.make
cluster
"./"
"`path_to_tecomp_installation'/library/kernel"
end
```
目前的 tecomp 版本需给定安装目录的绝对路径或相对路径,未来版本会支援符号定义方
式。你可以
键入以下命令来编译与执行该程式
```
tecomp hello.ace
```
然後它会印出
```
Hello, world
```
接着来解释一下这个程式。一个 Eiffel 程式由任意数量的类别组成。其中一个类别必须
是根类别 (root class) 、一个程序必须是根程序 (root procedure)。以上面的程式来
说
根类别名为 HELLO 而跟程序为 make。
编译器得知道如何找到类别;类别存在於通常以系统目录实现的丛集 (cluster) 中。上
面的
ace-file 定义了两个丛集─ "./" (即当前目录) 与
"`path_to_tecomp_installation'/library/kernel"
(即 Eiffel 核心类别存放处。编译器会在这些丛集中搜寻类别,当你的程式中
使用了一个搜寻不到的类别,编译器则会提报错误。丛集中的 Eiffel 类别集合称为
universe。
执行一个 Eiffel 程式从建立一个根型别的物件并呼叫其根程序开始 (此处型别与类别两
词
为同义,它们只有在使用泛型时不同)。该跟程序可以创建任意数量的各种物件并呼叫任
何已
建立物件的常式。
Eiffel 与许多现代语言相同,使用自由格式─程式语法中的空白字元是不重要的。退格
是为了人眼的可读性而非编译器限制。
名字像是 class, create, feature, do, 与 end 是语言中的关键字,它们是被保留的
;所有类别、特徵或变数不能使用这些保留字。
现在,我们来看看上面程式的结构
```
class
HELLO -- 类别名称
create
make -- 建构程序
feature
... -- 类别特徵
end
```
这个框架表示一个 HELLO 类别的定义。型别为 HELLO 的物件只能以 make 建构程序建立
。
所以特徵宣告在 feature...end 区块。
一个特徵可以是常式或属性。我们的简单程式只有一个特徵称为 make。该特徵为常式,
一个常式可以接受参数并回传结果。没有回传结果的常式称为命令 (command) 或程序
反之称为查询 (query)。make 这个常式为 HELLO 的建构程序是因为他被列在建构程序
的集合中(在 HELLO 中它是唯一一个)。
make 的程式码
```
make
do
io.put_string("Hello, world")
io.put_new_line
end
```
只有两个陈述式,```io.put_string("Hello, world")``` 呼叫 io 这个特徵;所有
Eiffel 中的类别都可以呼叫 io,因为该 io 为 STD_FILES 型别,此型别在 ANY 类别中
被定义而 ANY 是被所有的类别隐式继承。既然 io 传回的物件为 STD_FILES 型别
,STD_FILES 中的特徵得以被呼叫。
STL_FILES 有特徵 put_string 接受一个字串参数。该特徵 put_string 是一个命令因为
它没有任何回传值。它将字串参数输出至标准输出。而特徵 put_new_line 做的事情如字
面
所述(译:输出换行)。
特徵的概念是 Eiffel 语言的基础,因此以下解释一些基本观念:
一个特徵有两种观点─使用者或客户端观点,与实作观点。由客户端观点,我们会区分查
询
与命令。一个查询可接受零或多个参数并传回一个值。虽然语言本身并未强迫,不过一个
查询最好可以不含有副作用(译:例如改变物件状态,类似 C++ const member function
概念)。一个命令一样可以接受零或多个参数但没有回传值,它通常都被预期会改变物件
状态。
因此,一个查询可以属性或函式实作,一个命令则需以程序实作。(?)
在两个双引号间的字串序列,像是 "Hello, world",被称为字串或字串常数。特殊字元
如
换行或退格可藉由逸出序列包含在字串中。例如 %N 与 %T 为换行与退格的逸出序列。所
以
我们也能这样写
```
io.put_string("Hello, world%N")
```
得到与下面程式同样的输出
```
io.put_string ("Hello, world")
io.put_new_line
```
果想要依照程式码中的格式输出含有多个换行的字串,可以使用逐字(verbatim)字串,
例如
```
io.put_string ("[
usage: tecomp options ace_file
options
-t{p,v,e}{0,1,2,3} trace parsing, validation,
execution with level 0,1,2,3
-ws{0,1,2,3} write statistics
]")
```
会完全依照程式码中字串的格式做输出(译:保留所有空白字元)。
将多行逐字字串摆在 "[" 与 "]" 之间会移除每一行的"最长相同前缀空白字元
(longest common whitespace prefix)" (即向左对齐)。若摆在 "{" 与 "}" 间则会直接
复制所有字串而不移除任何前缀。逐字字串常数与此文件语法和许多 UNIX shell
类似。
若想在原码中的字串换行但不希望换行包含在字串时,可使用包装字串。陈述式
```
io_put_string ("Hello, %
%world")
```
与下面有相同输出。
```
io.put_string("Hello, world%N")
```
在两个 '%' 符号中的空白字元会在建构字串常数的过程中被忽略。
ANY 类别还有另一个特徵 print 用来将任何物件输出到标准输出。所以最短的 Hello
world程式长得像这样
```
class HELLO create make feature
make
do
print("Hello, world%N")
end
end
```
# 区域变数、计算式与回圈
下一个程式使用公式
```
degree Celsius = (5/9)( degree Fahrenheit - 32 )
```
印出以下的华氏摄氏温度对照表:
```
0 -17
20 -6
40 4
60 15
80 26
100 37
120 48
140 60
160 71
180 82
200 93
220 104
240 115
260 126
280 137
300 148
```
此表可以下面的 Eiffel 程式输出
```
class
FAHR_CELSIUS
-- 印出华氏-摄氏对照表
create
make
feature
make
local
fahr: INTEGER -- 华氏温度
do
from
fahr := 0
until
fahr > 300
loop
io.put_character('%T')
io.put_integer (fahr)
io.put_character('%T')
io.put_integer ((fahr-32) * 5 // 9)
io.put_new_line
fahr := fahr + 20
end
end
end
```
任何在 '--' 与行尾间的字元都会被编译器忽略,这些字元可作为注解。
Eiffel 中,区域变数可在各个常式中的 do end 区块前宣告。上面的程式中,区域变数
fahr 宣告为 INTEGER 型别;Eiffel 是强型别语言,因此任何变数,运算式等都必须
对应一个型别。 在 Eiffel 里,一个 INTEGER 是介於 -2^31 至 2^31 - 1 之间的数值
,意即 INTEGER 至少有 32 位元。
INTEGER 为核心函式库中的一个类别。
FAHR_CELSIUS 的程序 _make_ 含有一回圈,由初始化区段、终止条件、与回圈本体组成
。
在 Eiffel 里一个回圈运作如下。
- 执行初始化区段 (from ...)
- 测试终止条件 (until ...)
- 当条件为假,回圈本体 (loop ...) 即被执行,之後再次测试终止条件
- 一旦终止条件为真,回圈终止并执行回圈(... end)之後的第一个陈述式
运算式 ```(fahr-32) * 5 // 9``` 是一个整数运算式,适用通用的计算规则。因此
fahr-32 必须加上括号。运算子 // 代表整数除法。
字元放在两个单引号之前,'a' 代表字元 _a_. '%T' 代表特殊字元退格。
# 字元输入与输出
核心函式库让我们得以读写档案;一个档案可视为一连串以换行符号分隔的行,每一
行则为一个字元的序列。这样的观点是独立於平台或作业系统的;在某些系统上
(如 Windows),每一行其实是以两个字元分隔,分别为 carriage return 与 linefeed。
核心函示库会对应不同的系统,让 Eiffel 程式处理的每一行都是以换行字元分隔。
每个 Eiffel 程式会开启三个标准输出入档案,或称文字串流(text stream):
standard_input, standard_output, 与 standard_error。
依照预设,standard_input 对应到键盘,standard_output 与 standard_error
对应到萤幕。当使用管线 (pipes) 或输出入重导时,标准输出入档案也指向实体档案
或暂存缓冲区。程式从 standard_input 读取并写出到 standard_output 时并不需要
关心档案实际指向哪个资源。
类别 ANY 中的 io 查询回传一个 STD_FILES 型别的物件让我们得以操作标准输出入
档案。STD_FILES 有许多特徵,以下的程式会用到的重要特徵包含:
```
end_of_file: BOOLEAN
-- standard_input 在上一次读取时是否已到达档案结尾
read_character
-- 从 standard_input 读入下一个字元并让它可透过 last_character 取得。
-- 若没有读取到任何字元,令 end_of_file 为真。
require
not end_of_file
...
last_character: character
-- 字元,在 read_character 被呼叫时读取的字元
put_character (c: CHARACTER)
-- 写入字元 'c' 到预设输出的结尾後
注意:EiffelStudio 的 STD_FILES 型别不支援 end_of_file。需使用
io.input.end_of_file。
```
上面只是复制核心函式库文件 std_files.e 中的一部分。通常 Eiffel 的特徵都带有
一小段标头说明,描述特徵的用途与回传值。
以上所述的四个特徵呈现出一般查询与命令的分别。read_character 是一个命令,它
尝试从 standard_input 读取一个字元并让 last_character 查询可取得这个字元,或
是在状况成立时令 end_of_file 为真,表示 standard_input 中没有没有更多的字元。
put_character 写出一个字元到 default_output,预设为 standard_output。
对 put_character, put_string 等的呼叫可以交错进行;输出会依照呼叫的顺序呈现。
命令 read_character 有一个前条件(precondiction) (译:断言(assertion)的一种)
```
require not end_of_file
```
意即当上一次读取输入串流时遇到了档案结尾,就不允许呼叫 read_character。
可透过 ace-file 来设定断言的监控,例如
```
root
...
default
assertions(all)
cluster
...
end
```
所有断言,像是前条件都会在执行期被监控,这对程式的除错是极佳的辅助。一旦程式
进入成熟并经过完整测试,我们可以改变监控模式为```assertion(no)```而不须更动
任何程式码。
前条件乃条约式设计(Design by Contract)的一部分,这种设计在 Eiffel 中被广放的
运用。一个特徵的前条件,建立了客户端与供给端的一部分条约,并赋予客户端一项义
务。
- 客户端义务:只有在前条件成立时才呼叫一个特徵。
条约的另一部分,特过制定後条件(postcondition) 来赋予供给端责任。条约式设计
中,断言可区分为前条件、後条件、类别不变性(class invariants)、回圈不变性、
与检定(checks)。相关资讯在之後的条约式设计章节会提及。
## 档案复制
仅给予字元输出入功能时,就能撰写许多有用的程式。第一个程式是将所有的字元
从输入复制到输出。
```
class
COPY
create
make
feature
make
do
from
io.read_character
until
io.end_of_file
loop
io.put_character (io.last_character )
io.read_character
end
end
end
```
程式码说明了程式的用意:在回圈中我们尝试从输入串流读取第一个字元,终止条件
io.end_of_file 检查是否到达输入串流的结尾。只要结尾还未到达,刚刚读取的字元
会以 io.pu_character(io.last_character) 写入到输出串流。
这个终止条件保证我们能满足 read_character 的前条件,也就是不会超过串流结尾後
仍进行读取。
## 字数计算
稍稍修改档案复制程式可以让我们计算输入串流中的字元数目。
```
class
CHAR_COUNT
create
make
feature
make
local
nc: INTEGER -- 字元数目
do
from
io.read_character
until
io.end_of_file
loop
nc := nc + 1
io.read_character
end
io.put_string ("number of characters: ")
io.put_integer (nc)
io.put_new_line
end
end
```
相对於将读取到的字元写入到输出串流,我们递增 nc 这个计数器,最後输出读取到的
字元数目。
Eiffel 里面,大部分的型别有合理的预设值。所有 INTEGER 型态的变数会被初始化为
0 。因此并不需要明确初始化 nc 。
与 C 不同,Eiffel 并没有递增运算元(++),你必须写成```nc := nc + 1 ```以递增
nc 。
# 阵列与物件
为了展示阵列与物件的用途,我们来写个统计数字、空白字元与其他字元出现次数的
程式。
输入字元可区分为 12 个种类,为了储存不同数字的出现次数,我们使用一个整数阵列
而非使用个别的变数。
```
class COUNT_DIGITS create make feature
make
local
ndigit: ARRAY[INTEGER]
nwhite, nother: INTEGER
c: CHARACTER
i: INTEGER
do
create ndigit.make_filled(0, ('0').code, ('9').code)
-- create array object
from io.read_character until io.end_of_file loop
c := io.last_character
if '0' <= c and c <= '9' then
i := c.code
ndigit[i] := ndigit[i] + 1
elseif c = ' ' or c = '%N' or c= '%T' then
nwhite := nwhite + 1
else
nother := nother + 1
end
io.read_character
end
io.put_string ("digits = ")
from i := ('0').code until i > ('9').code loop
io.put_character(' ')
io.put_integer (ndight[i])
i := i + 1
end
io.put_string(", white space = ");
io.put_integer( nwhite )
io.put_string(", other = ");
io.put_integer( nother )
io.put_new_line
end
end
```
如果将以上程式码当作输入文字,程式输出类似
```
digits = 5 5 0 0 0 0 0 0 0 2, white space = 298, other = 58
```
宣告式```ndigit: ARRAY[INTEGER]```宣告 ndigit 是一个整数阵列,Eiffel 里阵列
大小是在执行期指定而非编译期。陈述式
```
create ndigit.make_filled(0, ('0').code, ('9').code)
```
建立一个阵列物件,其下界索引 (lower index) 为字元 '0' 的编码值,上界索引
为字元 '9' 的编码值,即一个大小为 10 的阵列,且每个阵列元素的值为 0 。
相对与解释 ARRAY 的特徵,我们来看看 array.e 中是怎麽定义 ARRAY 的。
```
class ARRAY[G] ... create make ... feature ...
make_filled (value:G; l,u: INTEGER)
-- 建立一个下界为小写 L ,上界为 `u' 的阵列
-- 并以 `value' 填满之。
-- 若 u < l 则阵列为空。
lower: INTEGER
-- 阵列索引下界
upper: INTEGER
-- 阵列索引上界
count: INTEGER
-- 元素个数
item alias "[]" (i: INTEGER): G
-- 第 i 个阵列元素
require
lower <= i
i <= upper
...
end
put (v: G; i: INTEGER)
-- 将 `v' 放到阵列的第 `i' 个位置
require
lower <= i and i <= upper
...
end
end
```
这样一来程式中与阵列相关的陈述式代表甚麽意思,应该就很清楚了。
ARRAY 类别是一个泛型 (generic) 类别,使用了泛型参数 G。 我们可套用任何型别到
G ,来宣告一个阵列。以下是合法的阵列宣告
```
a1: ARRAY[CHARACTER]
a2: ARRAY[INTEGER]
a3: ARRAY[ARRAY[INTEGER]]
```
a3 宣告了一个阵列的阵列,然而
```
a1: ARRAY[ARRAY]
```
是不合法的。现在我们可以理解类别与型别的差异,(译:牵涉到泛型时)ARRAY 是一个
类别,而 ARRAY[INTEGER] 是一个型别。对於非泛型类别来说,类别名称同时代表一
个类别与型别。
item 这个特徵宣告时同时指定了别名 "[]"。这表示除了可以写```ndigit.item(i)```
也可以使用```ndigit[i]```。
别名机制在基础型别像是 INTEGER 也被使用。例如在 INTEGER 类别的原始码中可以看
到类似宣告
```
plus alias "+" (other: INTEGER) : INTEGER
```
意即运算式```a + b```其实等同```a.plus(b)```也就是以 b 为参数,呼叫 a 物件的
特徵 plus (在 Eiffel 或称为 target a) 。
回到数字统计程式。程式中的回圈一次读取一个字元,它必须决定一个字元是数字、空
白字元或者其他。作法上需要使用条件式,通常形式为:
```
if condiftion_1 then
compound_1
elseif condition_2 then -- 零或更多 elseif
compound_2
else --可有可无
compound
end
注意:因为不须括号(译:与 C 相较),elseif 关键字不含任何空白!
```
一个复合 (compound) 是任一合法 Eiffel 陈述句的序列。条件式陈述句的行为与其他
语言,如 C, java 等相同。
字元能以通用的关系运算元作比较。每个字元有对应的编码 (通常是 ascii 编码) ,
CHARACTER 类别含有一个 code 查询可回传对应的字元编码,条件
```'0' <= c and c <= '9' ```测试 c 是否为数字。
为表示特殊字元如换行等,Eiffel 字元常数能以逸出序列写为 '%N' 与 '%T' 分别代
表换行与退格字元。
对常数使用特徵与进行运算时,有一点得先了解─字元常数 '0' 是一个型别为
CHARACTER
的运算式,因此,对字元常数所代表的物件,所有 CHARACTER 的特徵都可以被呼叫。
然而,无法直接以```'0'.code```来呼叫,因为这将造成模棱两可的语法而无法解析。
要将常数当成物件来使用时,我们必须加上括号,意即,```('0').code```表示 '0' 字
元的编码。
虽然这个程式有点矫情 (谁会想要统计档案里的数字?) 我们仍要写一个不同版本来展
示更多 Eiffel 技巧 (译:再矫情一下)。
上面的程式用条件式陈述句来判断字元的种类。不考量 unicode 的状况下其实我们只
有 256 个不同的字元,因此可用一个阵列来辅助判断。
主要的想法是用一个大小为 256 的阵列,每一个阵列元数参照一个计数器,三个空白
字元的元素应该要参照到同一个空白字元计数器。对应数字的元素参照数字字元计数器
,其它元素则参照剩下的它类字元计数器。
设计一个计数物件不难:
```
class COUNTER_OBJECT feature
value: INTEGER
increment do value := value + 1 end
invariant
value >= 0
end
```
类别与对应的型别只能是复制语意或是参照语意。COUNTER_OBJECT 类别使用参照语意。
INTEGER 类别是复制语意,它的宣告类似
```
expanded class INTEGER ... end
```
使用了 expanded 关键字来宣告具参照语意的类别。复制与参照语意间的差异性,对於
赋值、参数传递与使用 = 比对运算元而言,相当重要。
有参照语意的物件在被赋值与传递(call by reference)时,物件不会被复制,仅参照
被复制到目的地。比对运算元 = 只有在左右两边参照同一物件时为真。
若想要比对被参照物件的等价性(内容相同),必须要使用等价比对运算元 ~ 。对於使用
复制语意的类别,比对运算元 = 与 ~ 是同义的。
在 COUNTER_OBJECT 中我们宣告了类别不变性。
```
invariant
value >= 0
```
类别不变性可以宣告在类别的最後面 (最後一个特徵区块後) 。它是一种恒常性条件,
表明在每个特徵被叫用前後,该恒常性条件必须被满足。
当类别拥有许多属性时,加上类别不变性是相当有利的。有时为了增加或改良特徵而
延伸(extend)类别,人们会忘了满足不变性,这时打开断言监控可以让执行期监控
在不变性被违反时给予提示。
有了 COUNTER_OBJECT 类别,数字统计程式可以简单完成,这里先给出架构如下:
```
class COUNT_DIGIT2 create make feature {NONE}
white_counter, other_counter: COUNTER_OBJECT
char_counter: ARRAY[COUNTER_OBJECT]
make
do
initialize
read_input
write_statistics
end
iniialize
...
read_input
...
write_statistics
...
end
```
既然这个程式有点长,为了提供较好的可读性与可维护性,我们将它切割成三个程序
initialize, read_input 与 write_statics。
initialize 程序初始化计数器物件与阵列,read_input 扫描输入并妥善地递增计数器
,而 write_statistics 在程式结束前给我们预期的输出。
这三个程序须能够存取计数器,因此我们将计数器作为属性来使用,藉以避免参数
传递。
initialize 的程式码如下:
```
initialize
local
co: COUNTER_OBJECT
i: INTEGER
do
create white_counter; create other_counter
create char_counter.make_filled(other_counter, 0, 255)
from i:= ('0').code until i = ('9').code + 1 loop
create co
char_counter[i] := co
i := i + 1
end
char_counter[('%N').code] := white_counter
char_counter[('%T').code] := white_counter
char_counter[(' ').code] := white_counter
ensure
char_counter.count = 256
end
```
译:digital_counter 不见了?为何是区域变数?
没甚麽特别的地方,初始化计数器物件、前条件声明 char_counter 已被恰当初始化。
之後的程序可以倚赖这个性质。
接着程序 read_input 更简单
```
read_input
require
char_counter.count = 256
local
c: CHARACTER
do
from
io.read_character
until
io.end_of_file
loop
c := io.last_character
char_counter[c.code].increment
io.read_character
end
end
```
每次读取字元 c 时,只要透过```char_counter[c.code]```取得对应的参照并呼叫其特
徵 increment 即可。
前条件表明该程序预期 char_counter 阵列已被妥善初始化。若前条件未被满足,
read_character 可能会越界存取 char_counter 阵列。
接着是 write_statistics
```
write_statistics
require
digit_counter.count = 10
local
i: INTEGER
do
io.put_string ("digits = ")
from i := 0 until i = 10 loop
io.put_character (' ')
io.put_integer ( digit_counter[i].value )
i := i + 1
end
io.put_string ( ", white space = " )
io.put_integer ( white_counter.value )
io.put_string ( ", other = " )
io.put_integer ( other_counter.value )
io.put_new_line
end
```
TODO These 3 routine obviouly won't get compiled.
# 函式
目前我们仅止於撰写程序;记得,泛用的名称为常式。从使用者角度来看,常式一类是
命令,另一类是查询 (有传回值的特徵) ,查询能实作为属性或函式。
现在来写个计算阶乘数的函式,还记得数学定义是
```
n! = 1, if n = 0
n! = n * (n-1)!, if n > 0
```
Eiffel 里可以撰写递回函式,既然数学上以递回定义,以递回函式实作十分简单
```
fac (n: INTEGER): INTEGER
require
n >= 0
do
if n = 0 then
Result := 1
else
Result := n * fac(n - 1)
end
end
```
所有 Eiffel 函式都隐含一个预先宣告的区域变数 Result ,所以我们不需要额外宣告
此一变数,编译器会自动帮你添加。Result 变数的型别取决於函式的传回值型别,在
常式中你必须赋值给 Result (或建立 Result) ,Result 的值会被传回函式的呼叫者。
注意:函式可以有零或多个参数,以下都是合法的函式
```
five: INTEGER do Result := 5 end
array_of_10_ints: ARRAY[INTEGER] do create Result.make_filled(0,0,9) end
```
使用者不需要知道 five 与 array_of_10_ints 是函式,他们可以视为无参数的查询
,与属性没有分别。这是一致化存取(uniform access) 的原则,实作者可自行定夺
要以无参数查询还是属性实作,都不会影响到客户端程式码。
对於不欣赏递回函式的人们,我们也写个迭代版本的 factorial
```
factorial_iterative (n: INTEGER): INTEGER
require
n >= 0
local
i: INTEGER
do
from Result:=1; i:=0 until i = n loop
i := i + 1
Result := i * Result
end
end
```
撰写风格注意事项:Eiffel 并不要求以分号作为陈述句的结尾或分隔,但是它们是被允
许的,语法上分号都是定义为可有可无的。上面的程式我们可以改用
```Result:=1 n:=1```而不使用分号。不过多个陈述句摆在同一行时,为了可读性最好
还是加上分号。
递回版本的函式很容易可以验证,因为他只是 Eiffel 语法版的数学定义;而验证迭代
版本则需要思考一下:回圈边界是否正确?迭代次数是否正确?虽然这个回圈并不复杂
,我们还是严谨一点地以 Eiffel 技巧来验证一下。
关键想法是 Result 总是含有 ```i!``` 。回圈从 ```Result=1``` 开始,从定义得知
这是 ```0!``` 的值。也就是说在回圈初始时 ```Result=i!``` 是被满足的。每次迭
代我们递增 i 并将 ```i*Result``` 赋值给 Result,即```i*(i-1)!```。因此,若
```Result=i!``` 在回圈初始时是合法的,在回圈结尾时也会是合法的。
我们称一个在回圈开始与结束都为真的条件为回圈不变性 (loop invariant)。
截至目前,我们可以说服自己 ```Result=i!``` 是一个回圈不变性。
在回圈结尾时,我们知道终止条件 i=n 为真,因此我们知道在回圈结束时
```
i = n and Result = i!
```
等同
```
Result = n!
```
Eiffel 允许我们制订回圈不变性。既然我们已经有个可靠的递回函式 fac ,我们可以
完全倚靠 Eiffel 撰写不变性(译:a little bit overkill)
```
factorial_iterative ( n: INTEGER) : INTEGER
require n >= 0
local i: INTEGER
do
from i:=0; Result:=1 invariant
0 <= i and i <= n
Result = fac(i)
until i = n loop
loop
i := i + 1
Result := i * Result
variant
n - i
end
end
```
我们增加了直观的不变性条件声明 i 在 0 到 n 之间迭代。你可以使用 Eiffel 的断言
监控工具来检查回圈不变性,方法是在 ace-file 中添加```default
assertions(all)```。
在上面的程式我们加入了一个回圈可变式 (loop variant) 。这是个检验无穷回圈的工
具,一个可变式是一个非负的整数运算式,他必须在每次迭代时至少减 1 。
这个可变式是剩下的迭代次数上界。由於 i 从 0 递增到 n ,剩下的迭代次数为
```n-i``` 。
断言监控模式下,Eiffel 执行期在每次迭代时,会检验可变式为非负并至少被减 1 。
若以上条件为假,同样会告知可变式被违反。
# 更专注於类别
现在为止,我们只建立过根类别以及使用核心函式库的一些类别、专注於演算法角度
撰写一些如回圈的控制结构。然而 Eiffel 真正的威力在於他能用来制作各式的类别
与结合这些类别。
这个章节的范例将展示不同的类别用法;第一个演示继承的一些使用方式,第二个则是
泛型示范,第三个则用类别来呈现复数。
## 一个矩形是一种形状
在图形世界里我们会处理图形物件,像是矩形、圆形等。我们称这些图形物件为形状。
形状有一些共通点,可以被移动、显示、叠在其他形状之上。如果我们每次使用形状时
都要先区分他是矩形或圆形,程式码会变得十分杂乱。
Eiffel 允许使用者定义 SHAPE 抽象类别,拥有一些共通特徵但不真正实作这些特徵。
更加明确的类别像是 RETANGLE 则继承 SHAPE 并实作抽象类别里的特徵。
为了范例的简洁性,我们为 SHAPE 定义四个抽象特徵与一个具体特徵如下。
```
deferred class SHAPE feature
x_left: INTEGER deferred end
x_right: INTEGER deferred end
y_lower: INTEGER deferred end
y_upper: INTEGER deferred end
write_dimensions do ... end
invariant
x_left <= x_right
y_lower <= y_upper
end
```
四个抽象特徵让形状在 x, y 轴上可自由延伸,它们并没有实作,往常的 ```do..end```
区块被 ```deferred end``` 取代了。特徵的实作被延後到继承了 SHAPE 的类别里。
这四个特徵被称为延迟特徵(deferred feature)。
SHAPE 类别不能用来建立物件 (译:抽象类别无法实体化) ,因为这样的物件有未定义
的特徵。一个有延迟特徵的类别本身也会被延迟。因此我们必须写成 ```deferred
class SHAPE```
而不仅仅是 ```class SHAPE``` 。拥有延迟特徵的类别必须标注 deffered 是语言定
义的规则。
你可能觉得编译器已经知道特徵被延迟,不需要多加一个标注到类别名称前,但是语言
这样规定是为了清楚表明一个类别是抽象的。
同时注意一下,SHAPE 类别没有任何建立程序,因为对不能有实体物件的抽象类别,建
立程序是没有意义的。
虽然该类别只宣告了四个抽象特徵,但是有一些不变性已经可以确立了。谨记类别不
变性是一种类别特徵的恒常性关系。
SHAPE 类别将这个恒常性需求,套用至所有的衍生类别。意即每个衍生类别都得满足这
个类别不变性。此外,所有衍生 (译:类别里的) 的特徵也继承了同样的类别不变性。
类别不变性是一种断言,可在执行期监控。在建立类别与使用公开特徵的前後,类别不
变性都必须被满足。这可以强力保证任何常式的修改不会违反这个变性。
有了四个抽象特徵 x_left, xright, y_lower, y_upper 後,我们得以撰写
write_dimensions 程序将形状的 x, y 轴资讯写入到标准输出。write_dimensions
实作如下
```
write_dimensions
do
io.put_string ("shape with dimensions x = ")
io.put_integer(x_left)
io.put_string ("..")
io.put_integer(x_right)
io.put_string (" and y = ")
io.put_integer(y_lower)
io.put_string ("..")
io.put_integer(x_upper)
io.put_new_line
end
```
如你所见,即使是延迟特徵也能被 write_dimensions 使用。这个 SHAPE 类别有时可称
做局部实作 (partial implementation) 。他实做了 write_dimensions 但是将延迟特
徵的实作延後到衍生类别里。
现在来定义一种形状,矩形。矩形可以直接以左右上下的维度定义
```
class
RECTANGLE
inherit
SHAPE
create
make
feature
x_left: INTEGER
x_right: INTEGER
y_lower: INTEGER
y_upper: INTEGER
feature {NONE}
make(x1, y1, x2, y2: INTEGER)
-- 建立一个以 (x1, y1) 为左下角,(x2, y2) 为右上角的矩形
require
x1 <= x2
y1 <= y2
do
x_left := x1; x_right := x2
y_lower := y1; y_upper := y2
end
end
```
RECTANGLE 继承延迟类别 SHAPE 并实作其中的延迟特徵。以 Eiffel 的术语来说,重新
宣告一个延迟特徵并启用他,称为启用特徵 (effecting a feature) 。
RECTANGLE 选择以属性来宣告延迟特徵。
由於 RECTANGLE 重新宣告并启用了所有延迟特徵,这个类别不再是一个延迟类别。
RETANGLE 类别提供了建立程序 make ,接受左下、右上座标并初始化属性。
为了满足父类别 SHAPE 的不变性,建立程序 make 设定了参数的前条件。
藉由在 feature 关键字後加上 ```{NONE}``` 的方式,我们让 make 变成一个私有函式
,加上我们将他列为建构标头区块中 (译:create ...),make 就只有在建构过程中能
被呼叫。建构函式常会以这种方式实作,藉以避免被建构以外的程序呼叫。
不过这并非铁则,你仍可以开放外部直接呼叫建构函式,但是最好确保在物件的生命周
期中,任意呼叫建构函式不会造成任何异常。
另一种形状,圆形,定义成由圆心及半径构成。因此我们需要 x_center, y_center 及
radius 属性。由这些属性我们可以计算出 x_left, x_right, y_lower 及 y_upper ,
在 CIRCLE 类别里,x_left 等并非属性,而是以查询实作。
类别 CIRCLE 定义如下
```
class
CIRCLE
inherit
SHAPE
create
make
feature
x_center: INTEGER
y_center: INTEGER
radius: INTEGER
x_left: INTEGER do Result := x_center - radius end
x_right: INTEGER do Result := x_center + radius end
y_lower: INTEGER do Result := y_center - radius end
y_upper: INTEGER do Result := y_center + radius end
feature {NONE}
make ( x, y: INTEGER; r: INTEGER )
-- (圆心、半径)
require
r >= 0
do
x_center := x
y_center := y
radius := r
end
end
```
RECTANGLE 与 CIRCLE 类别以不同的技巧启用父类别里延迟特徵,两种方式都是合法的。
完整的实作,不论是记忆体型的属性,或计算型的函式,都能延迟到子类别再定义。
有了以上两个类别,我们就能建立两种图形物件。由於两种类别都继承了 SHAPE 类别
,我们能将一个 RECTANGLE 型别的变数,赋值给 SHAPE 型别的变数。我们称
RECTANGLE 遵循 (conforms) SHAPE 。
```
local
s: SHAPE
r: RECTANGLE
c: CIRCLE
do
create r.make(0, 0, 10, 20)
create c.make(5, 5, 30)
s := r -- OK, RECTANGLE 遵循 SHAPE
s.write_dimensions
s := c -- OK, CIRCLE 遵循 SHAPE
s.write_dimensions
end
```
范例较为简单,你也可以宣告一个```ARRAY[SHAPE]```,将两个不同的图形物件,放
到阵列中。
若 SHAPE 定义了一些额外的延迟特徵如下
```
deferred class SHAPE feature
...
move (x, y: INTEGER)
-- 将物件向左移动 x,向右移动 y
deferred end
draw
-- 绘制图形
deferred end
wipe_out
-- 清除图形
deffered end
end
```
并在子类别里实作以上延迟特徵,我们就能简单地撰写一个移动所有图形物件的功能
```eiffel
class GRAPHICAL_SYSTEM feature
...
objects: ARRAY[SHAPE]
move_all (x, y: INTEGER)
-- 移动所有物件
local
i: INTEGER
do
from i:=objects.lower until i > objects.upper loop
objects[i].wipe_out
objects[i].move (x, y)
objects[i].draw
i := i + 1
end
end
end
```
由於 SHAPE 是一个参照类别,每个阵列元素都是参照物件。
```
+----+
objects --> | | --> rectangle object
+----+
| | --> circle object
+----+
| | --> rectangle object
+----+
| | --> triangle object
+----+
| | --> ...
+----+
| | --> ...
+----+
```
## 矩阵物件
矩阵是种矩形的数字编排法
```
col 1 col 2 col 3
row 1: 1 10 -5
row 2: -1 2 -7
```
上面的矩阵有两列三行,每个元素为整数值。 ```a[1, 3]``` 则用来定位第 1 列第 3
行的元素。
我们以 a.rows 与 a.columns 来取得列与行的总数。
两个矩阵可以被相加减、互乘。
做矩阵加减法的两个矩阵必须有相同维度,c = a+b 可根据以下数学式计算
```
c[i, j] = a[i, j] + b[i, j]
```
c = a*b 乘法运算的数学式为
```
c[i, k] = a[i, 1] * b[1, k] + a[i, 2] * b[2, k] + ... a[i,n] * b[n, k]
where n = a.columns = b.rows
```
目前为止是矩阵的基本数学性质。
现在来建立一个可以 Eiffel 的 MATRIX 类别,我们并不需要为了不同的元素型别各
写一种矩阵类别,在这里泛型可派上用场,省略细节後的 MATRIX 类别如下
```eiffel
class
MATRIX[G->NUMERIC creat default_create end]
create
make
feature {NONE}
make (r, c: INTEGER)
-- r 列数, c 行数
...
ensure
rows = r
columns = c
end
feature
rows: INTEGER ...
columns: INTEGER ...
is_valid_row (i: INTEGER) : BOOLEAN
do Result := 1 <= i and i < rows end
is_valid_column (j: INTEGER) : BOOLEAN
do Result := 1 <= j and j <= columns end
item alias "[]" (i, j: INTEGER) : G
-- 在第 i 列第 j 行的元素
require
is_valid_row (i)
is_valid_column (j)
...
end
put (e1: G; i,j: INTEGER)
-- 将元素 e1 放到 [i, j] 位置
require
is_valid_row (i)
is_valid_column (j)
...
ensure
item (i, j) = el
end
plus alias "+" (other: like Current): like Current
require
rows = other.rows
columns = other.columns
...
end
minus alias "-" (other: like Current): like Current
require
rows = other.rows
columns = other.columns
...
end
product alias "*" (other: like Current): like Current
require
columns = other.rows
...
end
feature {NONE} -- 实作
...
end
```
这个架构里可以看到数个 Eiffel 的观念
### 1. 制约式泛型 (Constrained genericity)
```class MATRIX[G->NUMERIC create default_create end]``` 表明 MATRIX 是个
泛型类别,可以用来定义型别为 ```MATRIX[INTEGER]``` 或 ```MATRIX[REAL]```
的变数。藉由设定 "G 必须遵循 NUMERIC 型别" 这项制约,我们得以避免
```MATRIX[BOOLEAN]``` 被使用。
NUMERIC 是一个核心函式库中的抽象类别,将加减乘等运算宣告为延迟特徵。类别
INTEGER 与 REAL 继承了 NUMERIC (即遵循 NUMERIC) 并实作数值运算元。
进一步我们希望所有 MATRIX 都能自行初始化,藉由
```create default_create end```指定泛型 MATRIX 都需具备 default_create 这个
建构程序。预设建构式的语法为 ```create cp1, cp2, ... end``` 且是可有可无的。
可用来表明 cp1, cp2 在此泛型类别中必须是建构程序。
### 2. 运算元别名 (Operator aliases)
使用 Eiffel 的别名机制,我们可以写出 a+b, a-b 与 a+b 。
### 3. 下标别名 (Bracket alias)
```a[i, j]``` 的定址语法相较於 ```a.item(i, j)``` 来说可读性较高。须注意
每个类别最多只能有一个下标别名。这里我们使用在 item 特徵上。
### 4. 锚点型别 (Anchored types)
我们可以定义 plus 特徵为
```eiffel
plus alias "+" (other: MATRIX[G]): MATRIX[G]
```
但你可以看到我们实际上如此定义
```
plus alias "+" (other: like Current): like Current
```
在 MATRIX 类别中 MATRIX[G] 与 like Current 等价,因为目前型别为 MATRIX[G]。
然而当我们定义 ```class SPECIAL_MATRIX inherit MATRIX ... end``` 时,我们希望
SPECIAL_MATRIX[G] 的运算传回型别为 SPECIAL_MATRIX[G] 的回传值。
在 plus 等特徵的参数与回传值,使用锚点型别即能达到上述的效果。
```like Current``` 设定一个对应到 **目前型别** 的锚点,在 SPECIAL_MATRIX[G]
里,目前型别即 SPECIAL_MATRIX[G] 。
锚点可以设置到 Current 或其他类别的特徵 (必须为查询) 。
### 5. 条约式设计 (Design by Contract)
与其他范例一样,前条件与後条件可用来说明允许的、预期的常式行为。
plus, minus 与 product 里的前条件避免我们对两个不相容的矩阵做运算。
断言在 Eiffel 程式设计中占有非常重要的地位。
第一,它们能够说明你的意图,使用者能透过阅读常式的标头注解、前条件、後条件
,对常式的功能有精确的了解,而不需要阅读实作码。
第二,它们能辅助程式的除错。开发中的程式通常会开启所有种类的断言监控,若有
足够多的断言,一个错误通常能在接近源头的地方被截取。这能显着加速除错。
第三,若一个常式本身必定满足後条件,需要检查的只有客户端的呼叫方式。
这时断言是最好的理论根据。
现在我们得找个适合的实作来储存矩阵的元素。若我们定义
```eiffel
feature {NONE} -- 实作
matrix: ARRAY[ARRAY[G]]
end
```
我们能以 matrix[i][j] 或 matrix.item(i).item(j) 存取 (i, j) 的矩阵元素。
make 特徵则妥善地初始化矩阵
```eiffel
feature {NONE}
make (r, c: INTEGER)
local
i: INTEGER
one_row: ARRAY[G]
default_value: G
do
create matrix.make_empty(r, 1)
from i:=1 until i > r loop
create one_row.make_filled(default_value, 1, c)
matrix.extend_rear(one_row)
i := i + 1
end
ensure
rows = r
columns = c
end
```
我们使用 matrix 的行列数作为 rows 与 columns 特徵的实作
```eiffel
rows: INTEGER
do Result := matrix.upper end
columns: INTEGER
do
if rows > 0 then
Results := matrix[1].upper
end
end
```
元素存取常式 item 与 put
```eiffel
item alias "[]" (i, j: INTEGER): G
require
is_valid_row (i)
is_valid_column (j)
do
Result := matrix[i][j]
end
put (e1: G; i, j: INTEGER)
require
is_valid_row (i)
is_valid_column (j)
do
matrix[i][j] := e1
ensure
item(i, j) = e1
end
```
以下呈现乘法运算的实作,其他则留给读者做练习
```eiffel
product alias "*" (other: like Current): like Current
require
columns = other.rows
local
i,j,k: INTEGER
do
create Result.make ( rows, other.columns)
from i:=1 until i>rows loop
from k:=1 until k > other.columns loop
from j := 1 until j > columns loop
Result[i,k] := Result[i, k] + Current[i, j] * other[j, k]
j := j + 1
end
k := k + 1
end
i := i + 1
end
end
```
### 习题:
- 撰写 MATRIX 的类别不变性,确保每一列长度相同
- 实作特徵 transporsed 传回一个旋转过的矩阵
(即 ```a.transposed[i, j] = a[j, i]``` ) ,并添加後条件验证 rows 与 columns。
## 复数
复数包含两个实数,用以表示实部与虚部,通常 COMPLEX 类别架构会是
```
class COMPLEX feature
...
real: REAL
imag: REAL
...
end
```
不过,一般数值型别会使用复制语意,让变数赋值时进行复制而非参照,因此定义成
```expanded class COMPLEX``` 较为恰当。
此外,核心函式库的 NUMERIC ─ 数值类别的基底型别 ─ 其定义了数个数值特徵如下
```
deferred class NUMERIC feature
zero: like Current
-- 加法单元
deferred end
one : like Current
-- 乘法单元
deferred end
plus alias "+" (other: like Current): like Current
deferred end
minus alias "-" (other: like Current): like Current
deferred end
product alias "*" (other: like Current): like Current
deferred end
divided alias "/" (other: like Current): like Current
require
good_divisor : divisible (other)
deferred end
identity alias "+": like Current
deferred end
negated alias "-": like Current
deferred end
divisible (other: like Current): BOOLEAN
deferred end
end
```
如 NUMERIC 这种类别也称为行为类别 (behaviour class) ,它们定义子类应满足的
行为。用户得以预期所有继承行为类别的衍生类别,将妥善实作延迟特徵。
实作 COMPLEX 类别这项任务,实际上就是启用 NUMERIC 里定义的延迟特徵。
```
expanded class COMPLEX inherit NUMERIC create
make, default_create
feature {NONE}
make (r, i: REAL) do read := r; imag := i end
feature
read: REAL
imag: REAL
one : like Current do create Result.make(1.,0.) end
zero: like Current do end
puls alias "+" (other: like Current): like Current
do
create Result.make(real + other.real, imag + other.imag)
end
minus alias "-" (other: like Current): like Current
do
create Result.make(real - other.read, imag - other.imag)
end
product alias "*" (other: like Current): like Current
do
create Result.make(real * other.real - imag * other.imag,
imag * other.real + read * other.imag)
end
divided alias "/" (other: like Current): like Current
local
a, b, c, d: REAL
r, i: REAL
n: REAL
do
a := real; b:= imag;
c := other.real; d := other.imag
r := a*c + b*d
i := b*c - a*d
n := c*c + d*d
create Result.make (r/n, i/n)
end
identity alias "+": like Current
do Result := Current end
negated alias "-": like Current
do create Result.make(-real, -imag) end
divisible (other: like Current): BOOLEAN
do Result := other /~ zero end
end
```
注意,这里有些确保浮点运算正确性的技巧。由於实数是以 IEEE 浮点数格式储存,
所以 0 有分正负,两个值代表同一数值但在电脑里是不同的。布林运算式
```0. ~ -0.``` 为假。因此,特徵 divisible 在参数 other 含有负零值时,会传回
不正确的结果。
正确的实作会将 other 的绝对值与一个极小的实数做比较,这里我们忽略这个问题。
值得一提的是建构程序包含了 make 与 default_create 。前者用来初始化一个复数,
後者并未在 COMPLEX 里定义,它继承自核心函式库的 ANY ,不做任何事。
不做任何事的建构式乍看之下令人讶异,然而,一旦了解 COMPLEX 仅有的两个属性
皆为 REAL 型别,且 REAL 可以自我建构 (不明确初始化即初始化为零) 。不做任何
事就代表实部与虚部会初始化为零。
基於同样事实,我们可以将特徵 zero 写成
```
zero: like Current do end
```
而不是
```
zero: like Current do create Result.make(0.,0.) end
```
未明确初始化的变数 ```var``` 会在第一次被使用前,被系统如下初始化
```
create var.default_create
```
但前提是这个变数的型别能自我建构。将 default_create 指定为建构程序之一即代表
这个意义。
额外的好处是我们的 COMPLEX 类别,遵循了 NUMERIC 类别,因此得以套用到前一章节的
MATRIX 泛型,具现化为 MATRIX[COMPLEX] 型别。
# 链结串列 (Linked lists)
链结串列是基础的资料结构,优点是能快速地在头尾两端插入元素,缺点是随机存取
元素成本高昂。
资料结构的概念如下
```
+-------+
| List |
+-|---|-+
| |
| \----------------------------------------------\
| |
V V
+-------+ +-------+ +-------+ +-------+
| first |---->| |--->| |--->.....--->| last |---X
+-------+ +-------+ +-------+ +-------+
```
串列含有两个参照,一个指涉第一节点,另一个则指涉最後结点。
第一节点内含有一个参照,指涉第二个节点,第二节点也含有参照,
指涉第三节点,以此类推。
最後一个节点则含有空参照。
当串列为空,参照到第一与最後节点的参照皆为空参照。
节点为元素的容器,含有一个元素与下一个节点的参照。
移除第一节点的动作很简单,将原本参照到第一节点的参照,改参照第二节点即可。
现在考虑一下边界状况,即空串列与只有一个元素的串列,可图是如下。
```
empty one element
+-------+ +-------+
| List | | List |
+-|---|-+ +-|---|-+
x x v v
+-------+
| |
+-------+
```
节点的设计很简单
```
class LINKABLE[G] create put feature
item: G
next: ?like Current
put (element: G)
do item := element end
put_next (n: ?like Current)
do next := n end
end
```
唯一的新东西是 like Current 前的问号,型别 ?T 称为可卸除型别 (detachable type)
Eiffel 里所有型别预设为 "已附带 (attached)",变数、运算式等必须附带到物件上。
但以串列来说,我们需要一个可以指涉到 "无" 的型别。可卸除型别即符合这个需求。
若变数 v 的型别为 ?T ,我们可令 ```v:= Void``` 或执行
```v /= Void``` 布林运算,测试这个变数是否附带在一个物件上。
对於已附带型别的变数,Eiffel 编译器会验证他们总是附带至实体物件,否则会视为
错误。
链结骨架的外观如下
```
class
LINKED_LIST[G]
feature {NONE}
first_linkable: ?LINKABLE[G]
last_linkable : ?LINKABLE[G]
feature
is_emptr: BOOLEAN
do Result := first_linkable = Void end
first: G
...
last: G
...
extend_front ( element: G )
...
extend_rear ( element: G )
...
remove_first
...
invriant
(first_linkable = Void) = (last_linkable = Void)
end
```
我们的链结串列中有两个参照属性,分别指涉第一个与最後一个节点。
它们只能都指涉到 Void 或指涉到 LINKABLE[G] 节点。这一点我们透过类别不变性
确认。
错误的函式 first 实作:
```
first: G
require
not is_empty
do
Result := first_linkable.item
end
```
由於 first_linkable 是可卸除型别,可能为 Void ,因此编译器不会接受运算式
```first_linkable.item``` 。
然而我们已经透过 not is_empty 前条件确认 first_linkable 不可能是 Void ,因此
我们希望让编译器能认可上面的操作,这时我们必须添加 check 断言
```
first: G
require
not is_empty
do
check {l:LINKABLE[G]} first_linkable end
Result := l.item
end
```
运算式 ```{x:T} expr``` 是一种物件测试,检查 expr 是否附带到一个 T 型别的
物件上。若结果为真,该物件会被附加到区域变数 x 。
这个变数会再被宣告的区域中存活,以此例而言,l 会存活到常式结束。一个 check 的
区域变数是唯读的。
离开了存活区域就无法使用该变数了,如:
```
...
if condition then
...
check {x:T} expr end
...
x.some_feature -- 合法
...
end
x.some_feature -- 非法
...
```
函式 last 与 first 几乎相同。
插入元素到串列前端的程序不难,我们需建立一个新的节点用来存放新的元素,接着
将 first_linkable 指涉到原有的第一个节点。另外,这个程序也得考量串列为空的
状况,以确保正确性。
```
extend_front (element: G)
local
new_cell: LINKABLE[G]
do
create new_cell.put (element)
if is_empty then
first_linkable := new_cell
last_linkable := new_cell
else
new_cell.put_next (first_linkable)
first_linkable := new_cell
end
ensure
first = element
end
```
插入新元素到串列结尾我们再次使用物件测试。
```
extend_rear (element: G)
local
new_cell: LINKABLE[G]
do
create new_cell.put (element)
if is_empty then
first_linkable := new_cell
last_linkable := new_cell
else
check {l:LINKABLE[G]} last_linkable end
l.put_next (new_cell)
last_linkable := new_cell
end
ensure
last = element
end
```
我们使用物件测试 ```{l:LINKABLE[G]} last_linkable``` 来确保 last_linkable
附带在一个物件实体上。注意 else 的部分只有在串列非空时才会执行。
### 习题
- 实作 remove_first
- 实作 remove_last 。为何这个功能较复杂?
- 增加 ```count: INTEGER``` 属性,纪录串列内的元素个数。
- 实作 ```item alias "[]" (i:INTEGER): G``` 函式,传回第 i 个元素,假设 i 从
0 开始
- 保留 LINKABLE 类别不动的状况下,修改 LINKED_LIST 类别,让下面的迭代可以快
速存取串列
```
from i:=0 until i=list.count loop
list[i].do_something
i := i+1
end
```
# 中英对照
```
条约式设计 designed by contract
类别 class
物件 object
泛型 generic type, genericity
程序 procedure
常式 routine
函式 function
查询 query
命令 command
特徵 feature
丛集 cluster
逐字字串 verbatime string
逸出序列 escaped sequence
属性 attribute
前条件 precondition
後条件 postcondition
不变性 invariant
恒常性条件 consistency condition
制约式 constrained
参照 reference
启用 effect
遵循 conform
已附带 attached
可卸除 detachable
```
--
※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 220.132.12.57
1F:→ adxis:不过建议使用 LibertyEiffel 编译器 08/28 13:12