作為有關 C# 語言規范漫談的繼續,本月我們將討論運算符重載的問題。運算符重載(除非特別指明,否則本專欄的其余部分一律將其簡稱為“重載”)是指允許用戶使用用戶定義的類型編寫表達式的能力。它允許用戶定義的類型與預定義的類型具有相同的功能。
例如,通常需要編寫類似於以下內容的代碼,以將兩個數字相加。很明顯,sum 是兩個數字之和。
int i = 5;
int sum = i + j;
如果可以使用代表復數的用戶定義的類型來編寫相同類型的表達式,那當然是最好不過了:
Complex i = 5;
Complex sum = i + j;
運算符重載允許為用戶定義的類型重載(即指定明確的含義)諸如“+”這樣的運算符。如果不進行重載,則用戶需要編寫以下代碼:
Complex i = new Complex(5);
Complex sum = Complex.Add(i, j);
此代碼可以很好地運行,但 Complex 類型並不能象語言中的預定義類型那樣發揮作用。
任何事情都有特定的時間和場所
運算符重載是一個容易引起誤解的語言功能,而且編程人員對待它的態度也大相徑庭。一些人認為:用戶使用這一功能編寫的程序將令人費解,而且它也不應歸於編程語言。另一些人則認為它是一個很不錯的功能,在任何地方都可以使用。
這兩種觀點既包含正確的成分,但也有欠妥之處。應該承認,運算符重載可能會導致編寫出的程序令人費解,但根據我的經驗,即使不使用運算符重載,也很可能編寫出令人費解的代碼。在某些情況下,不使用重載甚至會使代碼更加令人費解。
那些不分場合、隨意使用重載的人“確實”在生產令人費解的代碼。
在語言中之所以使用重載,是為了在概念上對用戶的類或結構進行簡化。只有在有助於提高用戶所寫代碼的可讀性時,才能對運算符進行重載。請注意,我們所說的檢驗標准是“更清晰”,而不是“更簡短”。運用了運算符重載的類幾乎總是會使代碼變得更簡短,但並不能每次都使代碼變得更清晰(即可讀性更強)。
為了說明這一點,我創建了多個重載示例。您需要仔細閱讀這些代碼,想一想哪個運算符進行了重載,重載的運算符執行了什麼運算。
測驗
1
BigNum n1 = new BigNum("123456789012345");
BigNum n2 = new BigNum("11111");
BigNum sum = n1 + n2;
B
Matrix m1 = loadMatrix();
Matrix m2 = loadMatrix();
Matrix result = m1 * m2;
iii
DBRow row = query.Execute();
while (!row.Done)
{
VIEwer.Add(row);
row++;
}
IV
Account current = findAccount(idNum);
current += 5;
答案和討論
1
本示例中,要執行的運算是顯而易見的。這種加法只不過是將預定義的類型相加,每個人都明白執行了什麼運算,因此在這個示例中,使用運算符重載很有意義。
B
本示例演示了矩陣如何相乘。從概念上來說,矩陣乘法與常規乘法不完全類似,但它是一個明確定義的運算,因此任何理解矩陣乘法的人看到這種重載的運算符時,都不會感到驚訝。
iii
本示例中,增量 (++) 運算符進行了重載,它使數據庫行向前移至下一行。任何與數據庫行有關的事物都不可能使我們理解這種增量的真正含義,而且,這種增量要執行的運算也不是那麼明顯。
在這一示例中,重載的使用也沒有使代碼變得更簡單。如果我們轉而使用以下代碼,情況就好多了:
DBRow row = query.Execute();
while (!row.MoveNext())
{
VIEwer.Add(row);
}
IV
將事物和雇員相加代表什麼含義呢?本示例中,選擇是一個不錯的方法,將其與雇員數相加就會注冊雇員。這是一種很糟糕的運算符重載用法。
原則
何時進行重載的原則是相當簡單的。如果用戶希望能執行這種運算,那麼就應該進行重載。
重載算術運算符
要重載 C# 中的運算符,指定要執行運算的函數就可以了。函數必須在運算所涉及的類型中進行定義,並且至少有一個參數屬於該類型。這樣可以防止對 int 的加法或其它奇怪事物進行重載。
為了演示重載,我們將開發一個矢量。矢量可以被認為是從原點到特定二維點的線。可以對矢量執行多種運算。
以下是該類型的粗略定義:
struct Vector
{
float x;
float y;
public Vector(float x, float y)
{
this.x = x;
this.y = y;
}
}
要實際使用,矢量應支持以下運算:
獲取長度
將矢量乘以某個數字
將矢量除以某個數字
將兩個矢量相加
將一個矢量減去另一個矢量
計算兩個矢量的點積
我們的任務是確定應該如何實現這些運算。
長度
對於獲取矢量的長度,似乎沒有任何有意義的運算符。長度不會變化,因此將它作為屬性是很有意義的:
public float Length
{
get
{
return((float) Math.Sqrt(x * x + y * y));
}
}
將矢量乘以/除以某個數字
將矢量乘以某個數字是相當常見的運算,並且是用戶希望實現的運算。以下是相關代碼:
public static Vector Operator*(Vector vector, float multiplIEr)
{
return(new Vector(vector.x * multiplIEr,
vector.y * multiplIEr));
}
應該注意,此處有許多有趣的現象。首先,運算符是 static 函數,因此它必須獲取兩個參數的值,同時在結果中必須返回一個新的對象。運算符的名稱恰好是“Operator”,後面緊跟著要重載的運算符。
除以某個數字的代碼與以上代碼類似。
將兩個矢量進行加減
這是很常見的矢量運算,因此很顯然要對它們進行重載。
public static Vector Operator+(Vector vector1, Vector vector2)
{
return(new Vector(vector1.x + vector2.x,
vector1.y + vector2.y));
}
減法的代碼與以上代碼非常類似。
計算點積
兩個矢量的點積是為矢量定義的特殊運算,在預定義的類型中根本無法找到與之相類似的運算。在方程式中,點積通過在兩個矢量之間寫一個點來表示,因此它和任何現有運算符都不是精確匹配。點積的一個有趣特征是:它獲取兩個矢量的值,但只返回一個簡單的數字。
無論是否對該運算進行重載,用戶代碼都大致相同。第一行顯示了正在使用的重載版本,其它行則顯示了兩個替代版本:
double v1i = (velocity * center) / (t * t);
double v1i = Vector.DotProduct(velocity, center) / (t * t);
double v1i = velocity.DotProduct(center) / (t * t);
此時,它幾乎是一個判斷調用。我編寫的類對“*”運算符進行了重載,以便進行點積運算,但回過頭細想一下,我認為這一代碼並不是最合適的代碼。
在第一個示例中,velocity 和 center 是矢量這一點並不是很清晰,因此,點積是要執行的運算這一點也不是很清晰(我在查找一個使用它的示例時,注意到了這一點)。第二個示例很清楚地說明了要執行什麼運算,我認為使用該示例中的代碼最合適。
第三個示例也還可以,但我認為,如果該運算不是成員函數的話,代碼會更清晰一些。
public static double DotProduct(Vector v1, Vector v2)
{
return(v1.x * v2.x + v1.y * v2.y);
}
C# 和 C++ 重載
與 C++ 相比較,C# 允許重載的運算符很少。有兩條限制。首先,成員訪問、成員調用(也就是函數調用)、賦值以及“新建”無法重載,因為這些運算是運行時定義的。
其次,諸如“&&”、“||”、“?:”這樣的運算符以及諸如“+=”這樣的復合賦值運算符無法重載,因為這會使代碼變得異常復雜,得不償失。
重載的轉換
讓我們返回到最初的示例:
Complex i = 5;
Complex sum = i + j;
雖然知道了如何重載加法運算符,但我們仍需要想方法使第一個語句發揮作用。