Eiffel 简介

作者:Dan Wilder

在 1970 年代后期,当我还是一个在宏汇编语言上辛勤工作的熟练工人时,一种简单、优雅且富有表现力的语言 C 从结构化编程传统中脱颖而出。我的朋友们建议我忘记 C,去学习一种“真正的编程语言”,比如 PL/1 或 Ada,它们在更多重要平台上可用,并且由于政府和主要公司的支持而具有商业持久力。我无视这些建议,专注于 C。你们中的一些人可能会同意我的选择在当时是正确的。

现在,十多年过去了,另一种简单、优雅且富有表现力的语言出现了,这次是从面向对象传统中而来。Eiffel 编程语言起源于 Simula,不受向后兼容性问题的束缚,由 Bertrand Meyer 和他的同事从头开始设计,目的是构建健壮、通用、可重用的软件组件。凭借一些供应商的商业支持,在几个重要平台上可用,以及在主要商业应用程序开发方面取得的一些成功案例,这种语言值得仔细观察。

我从 1989 年就认识了 Eiffel,当时我读了 Meyer 博士的书《面向对象软件构造》(Prentice Hall,1988 年)。这门语言给我留下了深刻的印象,就像十年前我读到 Kernighan 和 Ritchie 的经典著作《C 程序设计语言》(Prentice Hall,1988 年)时对 C 印象深刻一样。随着 Bertrand Meyer 的公司 Interactive Software Engineering of Goleta, California 在 1994 年宣布推出 Linux 端口,我获得了尝试 Eiffel 的机会。

推介

那么,所有关于面向对象编程的喧嚣都是怎么回事?Eiffel 又与众多面向对象语言有何不同呢?

面向对象软件构造所展现的许多前景可能归因于代码重用的可能性,特别是软件组件的重用。

我所说的重用是指将先前编写的软件未经修改地纳入新程序中。在美国,我们有一句谚语:“如果它没坏,就别修它。” 这句民间智慧突出了研究结果,这些研究表明,在现有软件上工作时引入新问题的风险很大。理想情况下,应该可以编写一次,然后将其冻结起来以供重用,但永远不要修改,除非修复错误,也许还要添加新功能。如果可能,应保护现有用户免受此类更改的影响。

不幸的是,到目前为止,重用仅在有限的方面取得了成功。例如,Unix C 库或用于构建图形用户界面的各种 widget 库在许多平台上得到广泛使用。尽管如此,当今生产的大多数重要计算机程序都包含大量的手工代码。生产具有足够内置灵活性的软件组件的难度非常突出——而这种难度很大一部分必须归因于语言问题。

Eiffel 编程语言的设计旨在促进重用。Bertrand Meyer 在《Eiffel:库》(Prentice Hall,即将出版)的前言中回忆了他如何开始编写可重用组件,以及他如何放弃尝试使用现有语言,而是为此目的编写了一种语言。

Eiffel 是编译型和强类型语言,具有泛型(模板)、多态性、动态绑定、异常、垃圾回收、真正有用的多重继承实现以及独特的断言处理,仅从技术角度来看,就应该被视为竞争者。

Eiffel 提供断言作为语言原语,既提供内联设计文档,又提供可选的运行时错误检查。断言是可继承的。这有助于保证后代将达到或超过其祖先的承诺。使用这些断言是所谓的“契约式设计”的一部分。这些代表了在语言层面上责任驱动设计的应用。

还有其他因素

  • Eiffel 拥有已发布的、非专有的设计,由一个非营利性联盟协调,其决定所有现有供应商都同意遵守。

  • 简单而一致的语法使 Eiffel 成为一种易于学习的语言。您在这门语言中找不到密集的括号、星号、方括号或 & 符号。如果您喜欢特殊情况和规则的晦涩例外,以及专门用于处理这些事情的语言原语,那么 Eiffel 不适合您。

  • 除非您的母语编程语言是 Smalltalk、Lisp 或 Forth,否则算术运算符的优先级给出了您期望的数学表达式的求值顺序。

  • Eiffel 在各种平台上可用。

Eiffel 的设计已由 Bertrand Meyer 的公司 Interactive Software Engineering 放入公共领域。Eiffel 商标归 NICE 所有,NICE 是非营利性国际 Eiffel 联盟,它在授予商标使用权方面非常慷慨。验证套件将于今年晚些时候由 NICE 提供。主要的 Eiffel 供应商和用户,包括来自公司和学术界的代表,都在 NICE 中有代表。任何感兴趣的方都可以加入会员。NICE 的提案发布在新闻组 comp.lang.eiffel 上。任何人都可以参与随后的讨论。

官方语言定义是 Bertrand Meyer 的《Eiffel:语言》(Prentice Hall,1992 年)。这本近 600 页的书包含了对语言的精确定义、许多示例和大量的讨论。形式语法定义仅占八页。

NICE 正在标准化库。PELKS,即提议的 Eiffel 库内核标准,正处于最终采用阶段,即使供应商们也在加速使其自己的类库符合该标准。其他库可能会跟进。

Eiffel 可从许多供应商处获得或宣布,可在包括 Windows 3.1、VMS、SunOS、Solaris、Ultrix、OSF/1、DG Aviion、IBM RS/6000、Silicon Graphics、Macintosh、OS-2 和 NEXTSTEP 以及 Linux 在内的操作系统或平台上使用。

许多 Linux 用户对看到供应商对我们操作系统的兴趣感兴趣,一些精明的供应商也开始加入进来。一个在许多方面领先于其他语言多年的语言的供应商,也应该足够精明地认识到 Linux 社区的本质,这似乎并不太令人惊讶。而他们确实做到了——所有四家 Eiffel 供应商都提供 Linux 端口。

面向对象编程借鉴了几个主要思想。我将谈谈其中三个重要的思想,并说明它们在 Eiffel 语言中的实现。

第一个重要的思想是封装:将数据与操作数据的方法打包在一起。用编程语言编写的这样一个包是一个类,但类在执行(或存储)中的实例是一个对象。

在 Eiffel 中,一切都存在于类中。没有外部变量或例程。一个类有特性。特性反过来要么是属性,要么是例程。

属性存储值,包括对对象的引用。它们可以是常量或变量。

例程执行操作。例程要么是过程,要么是函数。函数返回结果,并且不应该改变系统状态。过程改变系统状态,但不返回任何内容。

所有特性,甚至是常量或变量属性,都被称为“调用”。这可能没有看起来那么奇怪,因为在 Eiffel 中,调用一个没有参数的函数与调用一个属性的写法相同。如果在某个类中您编写了

that := the.other

the.other 可能是函数或属性,在这种情况下,它没有任何区别。

因此,您将数据和操作数据的例程封装在一个类中。前面提到的断言也是类的一部分,用于表达类的前提条件、约束和不变量。

继承

第二个重要的思想是继承。

一旦您有了一个类,它描述了您对某种对象的如何和为什么的了解,那么从它派生出一个新类,进行添加和修改,而不触及原始类中的程序代码,可能会对您更有利。继承是一种允许这样做的机制。

例如,您可以从 INSECT 派生 BEEBEE 的许多特性将直接从 INSECT 继承,一些特性将被修改,而 BEE 将提供一些它自己的新特性。

一个经验法则,称为“is-a”规则,提供了一种方法来确定 A 是否可以有效地从 B 继承。检查句子“A is a B”。它有意义吗?如果 A 是从 B 继承的合理候选者,则应该有意义。例如,“BEE is an INSECT”通过了此测试,因此 BEE 可能从 INSECT 继承。然后,INSECT 将是 BEE 的祖先,而 BEE 将是 INSECT 的后代。

“has-a”规则提供了一个对比。如果“A has a B”比“A is a B”更有意义,那么让 A 引用或具有 B 类型的特性可能更明智,而不是从 B 继承。“BEE has a STINGER”比“BEE is a STINGER”或“STINGER is a BEE”更有意义。因此,类 BEE 应该具有 STINGER 类型的特性。这使得 BEE 成为 STINGER 的客户端,而 STINGER 成为 BEE 的供应商。

与后代相比,客户端-供应商关系为客户端提供了较少的详细控制。客户端可以使用或不使用供应商的特性,但它不能重新定义这样的特性。供应商的许多特性可能对客户端隐藏,而它们对后代是可见的。这样做的好处是,客户端将相对不受供应商实现细节的影响,并且不太可能受到供应商更改的影响。

客户端关系和继承关系都可以促进软件重用。传统函数调用更类似于客户端关系,过去在面向对象方法之前,许多重用尝试都使用了函数调用。然而,我们仍然发现自己编写和重写熟悉的函数代码片段,这些代码片段过于复杂,无法成为库例程的良好候选者。

继承的实现细节可能会严重影响其作为重用机制的适用性。在理想的实现中,后代类中出现的问题始终可以在那里解决。不幸的是,对于许多语言来说,后代类中出现的问题需要更改祖先。

多重继承更是如此,多重继承是一种技术,通过该技术,一个类可以享受,或者可能不享受,多个祖先。这项技术非常强大,但在许多面向对象语言中不可用,并且在其余大多数语言中也不鼓励使用。在已达到商业可行性的语言中,Eiffel 提供了多重继承的卓越实现。

多态性

从广义上讲,这表明一种情况,即一个简单的请求可能会根据请求的目标引发不同但并非完全不一致的响应。这些响应可能是通过完全不同的方式获得的。

例如,您可能有一些类,部分看起来像这样

class INSECT
-- Description of a standard            -- insect.
     ...
feature flee is
     do
     -- How a standard
     -- insect flees.
     ...
     end; -- flee
end

class BEE
inherit INSECT
     redefine flee
     end;
     ...
feature flee is
     do
     -- How a bee flees.
            ...
     end; --flee
end
class COCKROACH
inherit INSECT
     redefine flee
     end;
     ...
feature flee is
     do
     -- How a cockroach flees.
     ...
     end; -- flee
end

class WATERBUG
inherit INSECT
     redefine flee
     end;
     ...
feature flee is
     -- How a water bug flees.
     ...
     end; -- flee
end

然后,您可能有绑定到 INSECT 引用的 BEECOCKROACHWATERBUG 示例

-- Define references to an
-- INSECT and to a BEE.

     insect:INSECT;
     bob:BEE;

-- Bind some particular
-- insect to the reference

     insect := bugs.get

-- Now you have a BEE, a
-- COCKROACH or a WATERBUG.
-- Make it flee after its own
-- fashion, be it that of BEE,
-- COCKROACH, or WATERBUG.
-- This is a polymorphic call,
-- as the code executed will
-- depend on the type of the
-- object bound to insect.

     insect.flee;

-- You can't make a WATERBUG
-- collect pollen.
-- For this you need a BEE, and
-- a trial assignment is
-- available to assign objects
-- that might conform to the
-- type of a reference.

     bob ?= insect;

-- Then maybe you can make bob
-- the bee collect pollen.
-- If he isn't a BEE or a
-- conforming type, bob is Void.

     if bob /= Void then
        bob.collect_pollen
     end;

Eiffel 中支持另一种多态性,有时称为参数多态性,即泛型。

我要提到的最后一种多态性是在其他语言中作为函数重载找到的。在这里,一个函数可以多次定义,具有不同的类型和参数数量。当调用函数时,实际调用的函数取决于参数列表。

函数重载未在 Eiffel 中实现。存在解决方法,并且算术运算作为特殊情况处理,但 Eiffel 的设计者认为,函数重载的一般情况充满了潜在的歧义、类型检查失败、复杂性和交互作用,现在还不值得。在新闻组 comp.lang.eiffel 上,不时可以看到关于此主题的热烈讨论。

多重继承

多重继承在 Eiffel 下是一个巨大的胜利。如果您在其他语言中处理过这种情况,您可能会感到惊讶。多重继承需要解决许多棘手的问题,包括名称冲突和重复继承的复杂性,并且在大多数其他语言中,最好尽可能避免使用多重继承。

当由共同后代继承的两个类具有相同名称的不同特性时,会发生名称冲突。

当一个特性从一个共同祖先沿两条或多条继承路径多次继承时,您就有了重复继承。这可能会导致名称冲突,并引发实际问题,例如在多态调用中使用哪个重复特性。

大多数面向对象语言都不尝试多重继承。文献中充满了对此的详细解释。这很遗憾。Eiffel 表明,多重继承不必困难或复杂,而且它也可以产生一些非常实际的结果。

Eiffel 提供了多重继承的实现,最大限度地减少了名称冲突和重复继承复杂性的不利影响。与 Eiffel 中的典型情况一样,一个问题的解决方案有助于解决另一个问题。在 Eiffel 中,从祖先继承的名称可以在后代中使用重命名子句进行修改。然后,消除名称冲突仅涉及为冲突特性中的一个或两个赋予新名称。该特性不受影响,只是在重命名类以及该类的任何后代中,它以其新名称而闻名。

假设我们有一个名为 SOME_OTHER 的类,它从名为 FIRST_CLASSBOWSER 的两个祖先继承了两个完全不同的名为 put 的特性

class SOME_OTHER inherit
   FIRST_CLASS
     rename
       put as first_put
       end;
   BOWSER
     rename
       put as bowser_put
       end;
     ...
end

那么在某些客户端类中,我们可能有特性 what_now:SOME_OTHER

-- We may invoke the feature
-- named put from either of its
-- originating classes

     what_now.first__put(this);
     what_now.bowser_put(that);

重复继承只是稍微复杂一些。在简单的情况下,重复继承只是您选择的类继承结构的副产品。也许从类库继承的一些类具有共同的祖先——这几乎是肯定的。您的责任很简单:什么也不做。编译器会消除重复项,并且您的类只有一个继承特性的副本,无论特性可能以多少种不同的方式出现。

在不太常见的情况下,您可能想要一个特性的两个副本,例如来自祖先的 put。展示这种情况的类片段可能看起来像这样

class SOME_OTHER
     inherit
     FIRST_CLASS
       rename
          put as first_copy
       select
        -- Resolve an ambiguity.
         first_copy
       end;
     FIRST_CLASS
       rename
         put as second_copy
       end;

select 子句用于消除歧义。假设您引用类型为 FIRST_CLASS 的对象,并且您碰巧调用了 FIRST_CLASS 中已知的特性 put。

SOME_OTHER 继承 FIRST_CLASS 两次,并且由于重命名,对于问题“SOME_OTHER 中 put 的名称是什么?”有两个可能的答案。以下代码片段说明了

-- Declare a reference of type FIRST_CLASS
-- then attach an object of type SOME_OTHER,
-- a descendent of type FIRST_CLASS, using a
-- creation procedure of SOME_OTHER called "make"

     this:FIRST_CLASS;
     !SOME_OTHER!this.make;

-- When you try to invoke put,
-- without select it is not clear what you mean.
-- Could mean first_copy, could mean second_copy.

     this.put;

select 子句通过说“当在其祖先名称下调用此重复特性时,选择此副本”来消除歧义。请注意,Eiffel 将一个棘手的问题,即重复继承,带到了您可能希望的解决方案。重复项被简单地消除,无需进一步干预。如果您希望重复——一种不太常见的情况——您可以使用非常简单的语法来做到这一点。最后,相同名称但不同的特性可以重命名,以便两者都可用,或者可以取消定义不需要的特性。pb 在每种情况下,提供的机制都很简单,并且局限于出现问题的类。不需要修改祖先。这不是偶然的。这种语言的设计的一个主要推动力是消除修改祖先的情况。通过将重复继承和名称冲突所需的任何调整本地化在遇到它们的类中,从而为重用服务。祖先没有被破坏,因此它们不需要修复。如果它们没有被修复,就不会引入错误,并且祖先类的其他客户端或后代将不会受到可能在缺少解决后代冲突的合适手段的情况下可能需要的更改的影响,无论是引入错误的更改还是其他更改。

泛型

假设您需要一个类来操作对象的结构化集合——一个 INSECT 数组、一个分析树、一个销售线索的哈希列表或类似的东西?

一些面向对象语言提供了一种通用方法。您构建一个模板,例如 LINKED_LISTARRAY。然后,您将此模板与一些参数一起使用,这些参数指示用于构建特定 LINKED_LIST 或其他内容的类。

在 Eiffel 中,此功能称为泛型,模板是泛型类。像往常一样,这样做的方式既能完成工作,又如此简单,以至于似乎毫不费力。

假设您正在编写一个蚂蚁丘。首先您需要一些蚂蚁。

class ANT
     inherit INSECT
     -- A basic ant.  It has features to crawl,
     -- forage, dig, tend the young, and so on.
   ...
end

class CARPENTER_ANT
     inherit ANT
       redefine
       -- Redefine some things.
       -- These chew on your house and your apple
       -- tree.
       ...
       end;
     ...
end

class ARMY_ANT
     inherit ANT
       redefine
       -- These are always on the go.
       -- You hope they don't stop by your place
       -- for dinner.
       ...
       end;
     ...
end

现在您已准备好构建蚂蚁丘了。让我们假设您已经有一些类可以模拟昆虫社会。它的标头可能看起来像这样

class INSECT_SOCIETY[G->INSECT]
  ...

这表明 INSECT_SOCIETY 可以使用符合 INSECT 的参数形成。粗略地说,这意味着任何后代 INSECT 都可以。anthill:INSECT_SOCIETY[ANT] 声明对包含蚂蚁的 INSECT_SOCIETY 的引用。然后,此引用可以附加一个包含 CARPENTER_ANTARMY_ANT 或我们定义的任何其他 ANT 后代的 INSECT_SOCIETY。事实上,这使我们可以引用包含多种蚂蚁的蚂蚁丘,这很方便,因为有些蚂蚁丘可能包含多种蚂蚁。

在编写特定的容器类时,例如,我们可能希望利用我们对容器类特性中昆虫的了解。例如,在这种情况下,将 DOGHAMMER 类的对象输入到此容器中是绝对不行的。用于执行此操作的类型安全机制称为约束泛型,并在上面类 INSECT_SOCIETY 的标头行中进行了说明。

总结

Eiffel 编程语言提供了强大功能、简洁性、强类型检查和许多其他便利设施。凭借语言和内核库的开放规范以及多家供应商的支持,Eiffel 现在已准备好起飞。

据一位供应商称,最近的大部分兴趣来自那些已经使用 C++ 多年并确信该语言的培训成本和复杂性与其提供的功能不符的人。

我们这些更有冒险精神、渴望解决面向对象编程语言的人,可能希望进一步探索这门语言,而不受过去做事方式的过分负担。

在我的下一篇文章中,我将更多地介绍 ISE Eiffel 以及来自德克萨斯州奥斯汀市 Tower Technology 的编译器和工具。我还将提供一些关于如何开始使用这门语言的想法。

Introduction to Eiffel
Dan Wilder 自 1975 年以来一直担任软件工程师。Dan 居住在西雅图,他将时间分配在工作、家庭和家用 Linux 系统之间。剩下的时间都用来忽略他的邻居认为他称为草坪的苔藓集合。可以通过电子邮件 dan@gasboy.com 与他联系。
加载 Disqus 评论