老顽固

  下载本文的PDF版本 PDF

语法糖衣炮弹

Rodney Bates,威奇托州立大学

用户自定义重载就像毒品。起初,它给你一种快速、感觉良好的快感。没有必要用冗长而丑陋的函数名(如 IntAbs、FloatAbs、DoubleAbs 或 ComplexAbs)来 clutter 代码;只需将它们都命名为 Abs。更好的是,使用代数符号,如 A+B,而不是 ComplexSum(A,B)。这当然使编码更简洁。但是,一种危险的成瘾很快就会产生。原本已经复杂到足以考验每个人能力的语言和程序突然变得更加复杂。

它是什么?

一般来说,重载意味着一个函数名或运算符有两个或多个不同的含义。当您使用它时,语言会使用其操作数的类型来确定应应用哪个含义。这在编程语言中对于内置运算符(如 +)很常见,例如,它可以是整数加法或浮点加法。这两个操作作用于不同的操作数类型,并且需要生成不同的机器代码,因此它们是真正不同的含义。它也发生在一些内置函数中,例如绝对值函数 ABS,它像 + 一样,可以处理整数或浮点操作数。

相等比较,在某些语言中拼写为 ==,在另一些语言中拼写为 =,通常将这个概念进一步扩展,对于语言中的大多数类型都有许多重载含义。这很可能不仅包括内置类型,还包括程序员定义的类型的无界子集,如果不是全部的话。

当只有内置函数和运算符被重载,并且每个函数和运算符只有一组固定的语言定义的含义时,通常不会有太多麻烦。但我谈论的是程序员定义的重载。在某些语言中,程序员可以在同一作用域中声明多个不同的函数,它们具有相同的名称,但参数类型不同。使用此函数名称的调用在编译时通过查看实际参数的类型解析为多个函数之一。

程序员还可以通过声明一个或多个使用运算符表示法调用的函数来重载运算符。声明与任何函数几乎相同,只是有一种语法可以给它们命名,例如 +,它已经是内置运算符。当出现诸如 A+B 之类的表达式时,A 和 B 的类型决定了是使用 + 的内置含义还是程序员定义的含义,以及使用哪一个。如果是程序员定义的含义之一,则相当于调用声明它的函数。有时,程序员甚至可以用他们自己的函数替换运算符的内置含义——例如,整数上的 +。

它是语法糖吗?

对于程序员定义的重载,你能说的最好的就是它只是语法糖——也就是说,它使事物看起来更漂亮,但它根本没有增加重要的编程能力。您可以用它做的任何事情都可以通过简单地为所有函数赋予不同的名称(标识符,而不是运算符符号),并使用普通的旧式函数调用来调用它们来完成。但它真的是糖吗?

重载函数名通过节省击键次数来节省编写时间,更重要的是,节省了思考唯一名称的精神努力。然而,对可读性的影响纯粹是负面的,因为具有不同含义的事物看起来是相同的。

即使对于运算符,对可读性的影响也最多是值得商榷的。至少当运算符在数学中具有某些传统时,运算符表示法比函数调用表示法更容易阅读。与函数名一样,重载运算符也遭受不同事物看起来相同的问题。幸运的是,运算符最多只有两个操作数,而函数有时有很多。

不幸的是,有了这个新玩具,程序员们很少能抵制住过度使用它的诱惑。结果是,各种各样根本不同的含义都被重载到相同的名称或运算符上。可读性直线下降。

所以这里没有真正的语法糖。对于运算符,至少,您可以 concoct 一些看起来更甜美的特定调用示例。但是,总的来说,这是一大剂醋。

吹笛人要求他的报酬

即使你喜欢糖的高潮,程序员定义的重载的成本也是过高的。语言设计者、编译器编写者、开发人员和用户都遭受痛苦。以 C++ 为例,从声明开始。程序员声明一个函数,其名称与同一作用域中的其他函数相同。这实际上可能是同一函数的重新声明、不同的重载函数或非法重载。仅仅为了确定声明了什么就需要仔细审查,并且您必须将每个声明的函数与作用域中的每个其他函数进行比较。

现在考虑调用。程序员编写(或更常见的是,读取)一个看起来无辜的调用,例如,F(X,Y,Z)。它是什么意思?首先,有一个名为 F 的候选函数列表。这些函数在一个多分支作用域树中搜索,其中有五个规则给出了此树可以分支的不同方式。幸运的是,搜索在包含任何名为 F 的声明的第一个作用域处停止,因此所有候选函数至少都将来自同一作用域。

然后检查候选函数的可用性。如果调用中的所有实际参数都可以隐式转换为候选函数的相应形式参数的类型,则候选函数是可用的。有 29 种可能的隐式转换类型是标准的(即,由语言定义的)。我保守地计算了规则;宽松的计数是 138。

然后有无限数量的程序员定义的转换。这些转换有两种:一种是用户定义的转换函数,很可能仅为此目的而编写;另一种是带有一个参数的构造函数,通常是为另一个目的(构造)而编写的,因此很可能无意中进入重载解析的混乱局面。

单个隐式转换序列最多可以由连续七个转换组成,其中两个从三个可能性集合中选择,两个从六个可能性集合中选择,两个从 20 个可能性集合中选择。剩余的转换来自用户定义的转换的无界集合。如果我们假设每种用户定义的转换都只有一个,则这将产生超过一百万个隐式转换序列,尽管其中许多可能在语义上是非法的。这是针对每个参数而言的。这必须乘以参数的数量,然后依次乘以候选函数的数量。请记住,这些转换都没有写在代码中。它们都是由编译器隐式完成的。

到目前为止,最初的毒品快感已经消失,崩溃已经开始,因为程序员的大脑因超出实际问题的复杂性而变得混乱。但我们离弄清楚 F(X,Y,Z) 实际调用什么还有很长的路要走。每个被证明可用的函数现在都需要与其他所有函数进行比较,以确定拟合优度,这只是一个偏序。对于每对可用的候选函数,首先必须评估每个参数,这反过来又通过对两个隐式转换序列进行排序来完成。有 18 条规则给出了两个隐式转换序列的偏序关系。我也保守地计算了这些,因为一些规则有多个子情况。如果 18 条规则之一适用,则两个序列之一可能比另一个更好或更差。否则,它们不会根据拟合优度进行排名。

参数的拟合优度排名通过另一个偏序规则组合起来,以比较两个可用的候选函数的总体拟合优度。为了使一个候选函数比另一个更好,它必须在每个参数中至少一样好,并且在至少一个参数中更好。如果两个函数在任何参数中都未排名,则它们作为函数未排名。

整个过程必须应用于每对可用的候选函数。如果其中有 n 个,则有 (n*(n-1))/2 对。从所有这些中,必须恰好有一个可用的候选函数比其他所有函数都更好。如果是这样,则它是调用的函数。如果不是,则会出现编译时错误。

成瘾的痛苦

专业的语言设计者、语言律师和编译器编写者可以花费必要的人力年限来艰难地完成这种复杂的混乱,即使这是一笔巨大且完全不必要的开支。但是程序员不可能每次编写或读取简单的函数调用时都进行这种分析,甚至编写自己编译器的编译器编写者也不行。只有极少数的 C++ 程序员能够给出类似上述的、大大缩减的过程摘要。

这种成瘾的可悲结果是,有数百万行代码,充满了运算符和函数调用,无论是原始程序员还是当前的维护人员,都只有一种模糊的直觉希望他们知道他们编写和读取的内容的含义。最好的希望是,语言的规则,加上程序员在声明重载函数方面的习惯,要么给出正确的答案,要么给出编译时错误。当人们的财富、健康和生命依赖于软件时,这远远不够好。

内置重载呢?

我首先描述了语言中内置的重载含义。这些也是毒品吗?它们的代价没有那么高,原因有几个。在语言中具有一组固定的特定重载含义,与提供一个通用系统(允许程序员声明和调用无限可扩展的集合)之间存在巨大差异。前者的语言复杂性至少比后者低一个数量级。

例如,内置运算符始终在任何上下文中都可用。这消除了在解析重载运算符时在何处查找候选声明的所有复杂性。此外,程序员必须记住的内置含义集不会因程序或程序中的位置而异,而程序员定义的重载则会。

戒掉毒瘾

用户定义的重载是一种语法毒品,它已经引诱了太多语言设计者、程序员和项目经理。它的好处充其量是次要的,并且纯粹是表面上的。在最坏的情况下,这是一场语义上的火车脱轨事故,在语言及其编译器的开发、程序员培训、过度的软件开发和维护预算和时间表以及低质量的软件方面造成了巨大且不必要的成本。

那些真正有勇气的人将通过拒绝使用推动这种毒品的编程语言来打破成瘾。那些无法完全戒掉成瘾的人至少可以建立编码标准,禁止编写重载声明,并希望意外违反此规则的情况不会太频繁。

参考文献

Gosling, J., Joy, B., and Steele, G. 1996. The Java Language Specification. Reading, MA: Addison-Wesley.

ISO/IEC. Annotated Ada Reference Manual. International Standard 8652:1995(E).

ISO/IEC. Programming Languages—C++. International Standard 14882:1998(E).

RODNEY BATES 在计算机软件领域工作了 33 年,其中三分之二在工业界,其余在学术界。他主要参与操作系统、编译器以及作为常驻编程语言律师。他是威奇托州立大学计算机科学系的助理教授和研究生协调员。

acmqueue

最初发表于 Queue 第 3 卷,第 5 期
数字图书馆 中评论本文








© 保留所有权利。

© . All rights reserved.