下载本文的 PDF 版本 PDF

精神分裂的类

关于类、类型和方法

Rodney Bates,威奇托州立大学

Java 也将接收者称为“调用实例方法的对象”,C++ 也将其称为“调用函数的对象”。

施乐帕克研究中心(Palo Alto Research Center)在 1970 年代进行的 Smalltalk 项目,以引入 GUI(图形用户界面)而闻名。它也为我们带来了一系列三种编程语言,最终形成了 Smalltalk-80,这是一种纯粹的面向对象语言,因为每个类型都是类类型,每个操作都是可重写的方法。这适用于语言的内置类型和操作,以及程序员定义的类型和操作。

Smalltalk-80 是一个重要且具有启发性的实验,它展示了面向对象在编程语言中可以达到的程度。它简洁、紧凑,并展现出罕见且令人耳目一新的概念完整性。为了实现其目标,它引入了类的变量可以是类变量实例变量,方法可以是类方法实例方法的思想。这使得类成为两种截然不同的概念——类型和模块——的混合体,它们具有非常不同的语义。Smalltalk 设法相对干净地做到了这一点。

不幸的是,最近的两种语言 C++ 和 Java,采用了同样的区分,却将其变成了一团毫无必要的混乱。让我们先看看这两种语言,然后再回到 Smalltalk。首先,回顾一下一些概念。不同的语言经常对相同或相似的概念使用不同的术语。为了避免术语爆炸,我将使用 Smalltalk 的术语,这些术语与 Java 术语基本相同(见表 1)。

类型定义了一组可能的值(以及可能的一组对它们的操作)。它可以内置于语言中(例如,整数或布尔值)或由程序员定义(例如,数组、记录或类类型)。程序中可以有类型的许多实例,或者一个也没有。简单地声明一个类型不会创建任何实例。它们通常通过声明局部或全局变量,或通过执行堆分配对象的分配器来创建。

相比之下,模块始终在一个完整的程序中只存在一个实例,并且此实例由模块声明创建。它将一堆声明分组在一起,表明它们在逻辑上是相关的。它通常引入一个名称空间,并允许使用限定名来明确分组并避免名称冲突。它通常对应于单独的编译或源文件。模块引发了几个额外的、重要的编程问题,但其余的与本次讨论无关。

对象类型是一种面向对象的、程序员定义的类型,其中包含变量和方法。每个实例都有一整套变量。每个方法都非常像一个函数,但具有一些重要的附加属性。它有一个特殊的参数,称为接收者参数,它是该类型的一个实例。

在方法调用中,有一个特殊的参数称为接收者参数,它扮演着三个角色。首先,它的静态类型给出了对象类型,方法名称的搜索将从这里开始。其次,它表示的对象的动态类型在运行时用于分派到可能存在的多个方法体之一,所有方法体都具有相同或兼容的参数列表。第三,除了括号中找到的参数之外,它还作为特殊参数传递给方法。

在 C++ 和 Java 中,称为的构造既不是模块也不是类型,而是两者的混淆。然而,关于类变量,它是一个模块,因为每个类变量始终只有一个实例,并且仅类声明就创建了它。然而,关于实例变量,它是一个对象类型。类声明不会创建这些变量的实例,但每个声明的局部或全局变量以及每个将类标识为其“类型”的堆分配对象都有一整套实例变量。

类方法实际上根本不是方法。它只是穿着奇怪外衣的普通函数。它没有接收者参数,并且对其的调用不提供接收者也不进行分派。关于类“方法”,类表现得就像一个模块,就像它对类变量一样。相比之下,实例方法是一个真正的方法。它有一个接收者参数,并且它是使用接收者参数调用的,该接收者参数扮演着所有三个接收者角色。关于实例方法,类是一个真正的类型。

所有这些都极大地混淆了调用。调用可以是普通函数调用,也可以是带有接收者的真正方法调用。对于编写或阅读调用并思考其含义的程序员来说,这是一个非常重要的语义差异。如果这种区别在语法上是显式的,这可能是合理的,但在 Java 和 C++ 中,“接收者”在源代码中的存在或不存在与调用的语义是否实际涉及接收者无关。

考虑这个简单的 C++ 调用:r -> f()。在一致的情况下,这是一个真正的方法调用,在名为 f 的实例方法上。接收者是 r,并扮演所有三个角色。但 f 也可能实际上是一个类方法。语法上有一个具有误导性的“接收者”r,但在语义上没有接收者。事实上,r 的唯一功能是提供一个类型,用于查找方法 f。没有分派,r 没有传递——甚至可以是 nil。

或者,考虑一个语法上不同的调用 f()。语法省略了接收者,在一致的情况下,没有接收者。如果 f 是一个普通函数,这可能是一个传统调用。如果调用发生在某个类 C 的方法(类或实例)g 的主体内部,则 f 可能是 C 的方法。如果它是类方法,则也没有接收者。但如果它是实例方法,则有一个隐含的接收者,即调用 g 中的接收者对象。

“接收者”在语法中的存在或不存在与是否实际存在接收者无关。四种情况中有两种具有误导性。在另外两种情况下,调用的语法与其语义一致,但这没有帮助,因为读者必须经过一个复杂的过程来弄清楚给定的调用是否是矛盾的情况之一。一般来说,找到 f 的正确声明,这将回答问题,涉及到搜索一个复杂的、多分支的声明区域树。Java 的可能性是相似的,尽管在某些方面稍微简单一些。

由于程序员定义了重载,这两种语言的情况都变得更加混乱。单个方法名称的多个候选含义可以包括类方法和实例方法的混合。在 Java 中,它们也可以来自几个不同的类定义。这导致了语言复杂性的爆炸式增长,很少有工作程序员能够理解,更不用说为他们编写或阅读的每个调用进行思考。Java 需要 16 页来解释确定调用实际含义的规则,而 C++ 需要 15 页,算上六个额外的类函数构造。整个 Oberon-2 语言仅用这么多页定义。

让我们回到 Smalltalk。它设法引入了类变量和方法,而没有造成这样的混乱。一个小小的优点是,它在语法上将所有类属性组合在一起,并将所有实例属性组合在一起,这有助于更明确地说明这个重要的语义区别。

此外,Smalltalk 在运行时为每个类创建一个实际的对象,我将其称为元对象。这保存了类属性。只有一个这样的对象,语言显式地标识它,最重要的是,原始类的“类”属性实际上是元对象的实例属性。事实上,你可以将定义类属性的表示法视为声明元对象的实例属性的语法糖。该语言创建元对象(以及它的类,称为元类)并确保只有一个实例,从而赋予它类似模块的属性。

最重要的是,访问类变量实际上是通过访问元对象的实例变量来完成的,并且元对象在语法上是显式的。同样,调用类方法是通过对元对象的相应实例方法进行普通方法调用来完成的。因此,语言中只有一个调用构造,具有一套语义规则。它始终有一个接收者,接收者始终在调用中语法显式,并且它始终扮演着真正接收者的所有三个语义角色。

Java 和 C++ 处理“类”变量和方法的方式是单一工具思维方式走火入魔的一个例子。可怜的类患上了无可救药的精神分裂症。它不知道自己是类型还是模块,并且常常是两者的混乱混合体。这种混乱蔓延到许多程序员,尤其是在试图弄清楚一个看起来无辜的调用到底意味着什么时。

将类变量和方法放入类中,是试图使对象类型同时成为类型和模块。与单独的构造相比,它更难单独用于任何一个目的或同时用于两个目的。在一个大多数工作程序员都不理解他们所用语言的世界中,它为编程语言增加了巨大且毫无必要的复杂性。

当你发现你的锤子在切割方面无效时,解决方案是在你的工具箱中添加一把锯子。如果你在预先设想一个工具应该用于一切事物的前提下操作,你将不得不将锯齿放在锤子的手柄上,这使得这个单一工具在驱动钉子和锯切方面都不如单独的专用工具有效。

设计最佳的语言为你提供了两个抽象工具——模块和对象类型——每个工具都相当好地服务于自己的目的。模块可以包含任何可声明的实体,包括变量(而不是类变量)和函数(而不是类方法)。如果你需要将类型与单实例抽象关联,你也可以将该类型放在模块中。如果你需要几个密切相关的类型,你可以将它们都放在那里——例如,图形模块,它需要图形、节点和弧的类型。这是另一个类作为唯一抽象构造而处理不佳的情况。

Smalltalk 在其他方面为将面向对象推向极致付出了高昂的代价,尤其是在完全丧失静态类型和严重的运行时效率损失方面。对于许多编程问题,特殊的、单实例形式的类不如模块那样在概念上匹配。但至少它提供了一个单一、一致且语法显式的调用机制。

喜欢它,讨厌它?请告诉我们

[email protected] 或 www.acmqueue.com/forums

RODNEY BATES ([email protected]) 从未从事过他所受训的电气工程方面的有偿工作。相反,他在计算机软件领域工作了 33 年,三分之二在工业界,其余在学术界。他主要参与操作系统、编译器,并担任常驻编程语言律师。他曾为流行的计算机杂志、贸易和研究会议以及学术期刊撰稿。他目前的大项目是 Modula-3 和其他语言的语义编辑器。他是威奇托州立大学计算机科学系的助理教授和研究生协调员。

© 2004 1542-7730/04/0900 $5.00

acmqueue

最初发表于 Queue vol. 2, no. 6
数字图书馆 中评论本文





更多相关文章

Matt Godbolt - C++ 编译器中的优化
在向编译器提供更多信息时,需要权衡:这可能会使编译速度变慢。诸如链接时间优化之类的技术可以为您提供两全其美的效果。编译器中的优化不断改进,即将到来的间接调用和虚函数分派的改进可能很快会带来更快的多态性。


Ulan Degenbaev, Michael Lippautz, Hannes Payer - 作为合资企业的垃圾回收
跨组件跟踪是解决跨组件边界的引用循环问题的一种方法。只要组件可以形成具有跨 API 边界的非平凡所有权的任意对象图,就会出现此问题。CCT 的增量版本已在 V8 和 Blink 中实现,从而能够以安全的方式有效且高效地回收内存。


David Chisnall - C 不是一种低级语言
在最近的 Meltdown 和 Spectre 漏洞之后,值得花一些时间来研究根本原因。这两种漏洞都涉及处理器推测性地执行超出某种访问检查的指令,并允许攻击者通过侧信道观察结果。导致这些漏洞以及其他几个漏洞的功能被添加,是为了让 C 程序员继续相信他们正在用低级语言编程,而这种情况已经几十年没有发生了。


Tobias Lauinger, Abdelberi Chaabane, Christo Wilson - 你不应该依赖我
大多数网站都使用 JavaScript 库,其中许多库已知是易受攻击的。了解问题的范围,以及包含库的许多意外方式,只是改善情况的第一步。这里的目标是,本文中包含的信息将有助于为社区提供更好的工具、开发实践和教育工作。





© 保留所有权利。

© . All rights reserved.