编程语言导论/特设多态性
如果一种多态性允许使用同一个名称来表示有限数量的编程实体,那么我们就说这种多态性是特设的。特设多态性主要有两种类型:重载和强制转换。
重载是指编程语言使用相同名称来表示不同操作的能力。这些操作可以通过函数名或称为运算符的特殊符号来调用。最常见的重载形式是*运算符重载*。例如,C、C++、SML、Java和Python重载加号(+),表示整数的和或浮点数的和。尽管这两种类型的和在原则上对我们来说可能相同,但实现这些操作的算法却大不相同。整数通常以二进制补码的形式求和。另一方面,求和浮点数的算法涉及分别求和操作数的基数指数和尾数。此外,在Java和Python中,加号还表示字符串连接,这是同一个运算符的第三个含义。
像C和SML这样的语言只重载内置运算符。但是,一些编程语言允许程序员重载名称。下面的示例说明了在C++中用户定义的重载
#include <iostream>
int sum(int a, int b) {
std::cout << "Sum of ints\n";
return a + b;
}
double sum(double a, double b) {
std::cout << "Sum of doubles\n";
return a + b;
}
int main() {
std::cout << "The sum is " << sum(1, 2) << std::endl;
std::cout << "The sum is " << sum(1.2, 2.1) << std::endl;
}
在上面的程序中,我们有两个不同的实现,用于名称sum. 当此名称用作函数调用时,将根据函数的*类型签名*选择正确的实现。函数的签名由其名称加上参数的类型组成。这些类型的顺序很重要。因此,在上面的程序中,我们有两个不同的函数签名sum. 我们有[sum, int, int]和[sum, double, double]. 在大多数编程语言中,此签名是*上下文无关*的。换句话说,返回值的类型不属于签名的一部分。根据给定的名称(或符号)选择适当的实现的过程称为*重载解析*。此选择是通过将调用中实际参数的类型与签名中形式参数的类型进行匹配来完成的。
重载的实现非常简单。编译器只需为程序员用相同名称接收的所有实现生成不同的名称。例如,如果我们将上面的示例编译为汇编代码,我们会发现两个不同的名称用于sum的两种不同实现sum:
$> g++ -S over.cpp
$> cat over.s
...
.globl __Z3sumdd
__Z3sumdd:
...
.globl __Z3sumii
__Z3sumii:
...
一些编程语言支持运算符重载。最著名的例子是C++,但运算符重载也存在于Fortress和Fortran 90等语言中。用Guy Steele的话来说,定义新数据类型和重载运算符的能力为编程语言提供了成长的空间。换句话说,开发人员可以改变编程语言,使其更接近他们必须解决的问题。作为运算符重载的示例,下面的程序用C++编写,包含两个重载运算符:加号(+)和流运算符(<<)。
#include <string.h>
#include <ostream>
#include <iostream>
class MyString {
friend std::ostream & operator<<(std::ostream & os, const MyString & a) {
os << a.member1;
}
public:
static const int CAP = 100;
MyString (const char* arg) {
strncpy(member1, arg, CAP);
}
void operator +(MyString val) {
strcat(member1, val.member1);
}
private:
char member1[CAP];
};
int main () {
MyString s1("Program");
MyString s2("ming");
s1 + s2;
std::cout << s1 << std::endl;
}
一些编程语言允许开发人员*覆盖*名称和符号,但这些语言不提供重载。只有当编程语言允许两个名称在同一范围内共存时,才会出现重载。例如,在SML中,开发人员可以覆盖运算符。但是,此运算符的旧定义将不再存在,因为它已被新定义遮蔽
- infix 3 +;
infix 3 +
- fun op + (a, b) = a - b;
val + = fn : int * int -> int
- 3 + 2;
val it = 1 : int
许多编程语言支持将一个值转换为具有不同数据类型的另一个值。这些类型转换可以隐式或显式执行。隐式转换会自动发生。显式转换由程序员执行。下面的C代码说明了隐式和显式强制转换。在第 2 行中,int常量 3 会自动(即隐式)转换为double在赋值发生之前。 C提供了一种用于显式转换的特殊语法。在这种情况下,我们在要转换的值前面加上目标类型名称,用括号括起来,如第 3 行所示。
double x, y;
x = 3; // implicitly coercion (coercion)
y = (double) 5; // explicitly coercion (casting)
我们将使用术语*强制转换*来指代隐式类型转换。强制转换让应用程序开发人员可以使用相同的语法来无缝地组合来自不同数据类型的操作数。支持隐式转换的语言必须定义在组合兼容值时将自动应用的规则。这些规则是编程语言语义的一部分。例如,Java定义了六种将基本类型转换为double的方法。因此,下面函数f的所有调用都是正确的
public class Coercion {
public static void f(double x) {
System.out.println(x);
}
public static void main(String args[]) {
f((byte)1);
f((short)2);
f('a');
f(3);
f(4L);
f(5.6F);
f(5.6);
}
}
虽然隐式类型转换定义明确,但它们可能会导致创建难以理解的程序。这种困难在将强制转换与重载结合在一起的语言中更加严重。例如,即使是经验丰富的C++程序员也可能不确定下面的程序(第 13-15 行)将调用哪些函数
#include <iostream>
int square(int a) {
std::cout << "Square of ints\n";
return a * a;
}
double square(double a) {
std::cout << "Square of doubles\n";
return a * a;
}
int main() {
double b = 'a';
int i = 'a';
std::cout << square(b) << std::endl;
std::cout << square(i) << std::endl;
std::cout << square('a') << std::endl;
}
尽管上面的程序可能看起来令人困惑,但它是定义明确的:C++的语义在将字符转换为双精度数时会从整数优先于双精度数。但是,有时强制转换和重载的组合可能会导致创建模棱两可的程序。为了说明这一点,下面的程序是模棱两可的,无法编译。在这种情况下,问题是C++不仅允许将整数转换为双精度数,还允许将双精度数转换为整数。因此,对于调用sum(1, 2.1).
#include <iostream>
int sum(int a, int b) { return a + b; }
double sum(double a, double b) { return a + b; }
int main() {
std::cout << "Sum = " << sum(1, 2.1) << std::endl;
}