程式語言導論/特設多型性
如果一種多型性允許使用同一個名稱來表示有限數量的程式設計實體,那麼我們就說這種多型性是特設的。特設多型性主要有兩種型別:過載和強制轉換。
過載是指程式語言使用相同名稱來表示不同操作的能力。這些操作可以透過函式名或稱為運算子的特殊符號來呼叫。最常見的過載形式是*運算子過載*。例如,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;
}