本节将介绍为类或结构定义的另一种类型的成员:运算符重载。
C++开发人员应很熟悉运算符重载。但是,因为这个概念对Java和VB开发人员来说是全新的,所以这里要解释一下。C++开发人员可以直接跳到主要示例上。
运算符重载的关键是在类实例上不能总是只调用方法或属性,有时还需要做一些其他的工作,例如对数值进行相加、相乘或逻辑操作,如比较对象等。例如,假定要定义一个类,表示一个数学矩阵,在数学中,矩阵可以相加和相乘,就像数字一样。所以可以编写下面的代码:
Matrix a, b, c;
// assume a, b and c have been initialized
Matrix d = c * (a + b);
通过重载运算符,就可以告诉编译器,+和*对Matrix进行什么操作,以编写上面的代码。如果用不支持运算符重载的语言编写代码,就必须定义一个方法,以执行这些操作,结果肯定不太直观,如下所示。
Matrix d = c.Multiply(a.Add(b));
学习到现在,像+和*这样的运算符只能用于预定义的数据类型,原因很简单:编译器认为所有常见的运算符都是用于这些数据类型的,例如,它知道如何把两个long加起来,或者如何从一个double中减去另一个double,并生成合适的中间语言代码。但在定义自己的类或结构时,必须告诉编译器:什么方法可以调用,每个实例存储了什么字段等所有的信息。同样,如果要在自己的类上使用运算符,就必须告诉编译器相关的运算符在这个类中的含义。此时就要定义运算符重载。
要强调的另一个问题是重载不仅仅限于算术运算符。还需要考虑比较运算符 ==、<、>、!=、>=和<=。例如,语句if(a==b)。对于类,这个语句在默认状态下会比较引用a和b,检测这两个引用是否指向内存中的同一个地址,而不是检测两个实例是否相同的数据。对于string类,这种操作就会重写,比较字符串实际上就是比较每个字符串的内容。可以对自己的类进行这样的操作。对于结构,==运算符在默认状态下不做任何工作,要比较两个结构,看看它们是否相等,如果相等就会产生一个编译错误,除非显式重载了==,告诉编译器如何进行比较。
在许多情况下,重载运算符允许生成可读性更高、更直观的代码,包括:
●
在数学领域中,几乎包括所有的数学对象:坐标、矢量、矩阵、张量和函数等。如果编写一个程序执行某些数学或物理建模,肯定会用类表示这些对象。
●
图形程序在计算屏幕上的位置时,也使用数学或相关的坐标对象。
●
表示大量金钱的类(例如,在财务程序中)。
●
字处理或文本分析程序也有表示语句、子句等的类,可以使用运算符把语句连接在一起(这是字符串连接的一种比较复杂的版本)。
另外,有许多类与运算符重载并不相关。不恰当地使用运算符重载,会使使用类型的代码很难理解。例如,把两个DateTime对象相乘,在概念上没有任何意义。
5.4.1 运算符的工作方式
为了理解运算符是如何重载的,考虑一下在编译器遇到运算符时会发生什么样的情况是很有用的
——我们用相加运算符+作为例子来讲解。假定编译器遇到下面的代码:
int a = 3;
uint b = 2;
double d = 4.0;
long l = a + b;
double x = d + a;
会发生什么情况:
long l = a + b;
编译器知道它需要把两个整数加起来,并把结果赋予long。调用一个方法把数字加在一起时,表达式a+b是一种非常直观、方便的语法。该方法带有两个参数a和b,并返回它们的和。所以它完成的任务与任何方法调用是一样的
——给定了参数类型后,它会查找最匹配的+运算符重载,把两个整数加在一起是很简单的。与一般的重载方法一样,预定义的返回类型不会因为调用的方法版本而影响编译器的选择。在本例中调用的重载方法带两个int类型参数,返回一个int,这个返回值随后会转换为long。
下一行代码让编译器使用+运算符的另一个重载:
double x = d + a;
在这个例子中,参数是一个double类型的数据和一个int类型的数据,但+运算符没有带这种复合参数的重载形式,所以编译器认为,最匹配的+运算符重载是把两个double作为其参数的版本,并隐式地把int转换为double。把两个double加在一起与把两个整数加在一起完全不同,浮点数存储为一个尾数和一个指数。把它们加在一起要按位移动一个double的尾数,让两个指数有相同的值,然后把尾数加起来,然后移动所得尾数的位,调整其指数,保证答案有尽可能高的精度。
现在,看看如果编译器遇到下面的代码,会发生什么:
Vector vect1, vect2, vect3;
// initialise vect1 and vect2
vect3 = vect1 + vect2;
vect1 = vect1*2;
其中,Vector是结构,稍后再定义它。编译器知道它需要把两个Vector加起来,即vect1 和 vect2。它会查找+运算符的重载,把两个Vector作为参数。
如果编译器找到这样的运算符定义,就调用它的实现。如果找不到,就要看看有没有可以用作最佳匹配的其他+运算符重载,例如某个运算符重载的参数是其他数据类型,但可以隐式地转换为Vector实例。如果编译器找不到合适的运算符重载,就会产生一个编译错误,就像找不到其他方法调用的合适重载一样。
5.4.2 运算符重载的示例:Vector结构
本节将开发一个结构Vector,来演示运算符重载,这个结构Vector表示一个三维矢量。如果数学不是你的强项,不必担心,我们会使这个例子尽可能简单。三维矢量只是三个数字的一个集合,说明物体和原点之间的距离,表示数字的变量是x、y和z,x表示物体与原点在x方向上的距离,y表示它与原点在y方向上的距离,z表示高度。把这3个数字组合起来,就得到总距离。例如,如果x=3.0, y=3.0, z=1.0,一般可以写作(3.0, 3.0, 1.0),表示物体与原点在x方
向上的距离是
3
,与原点在
y
方向上的距离是
3
,高度为
1
。
矢量可以与矢量或数字相加或相乘。在这里我们使用术语“标量”(scalar),它是数字的数学用语
——在C#中,就是一个double。相加的作用是很明显的。如果先移动(3.0, 3.0, 1.0),再移动(2.0, –4.0, –4.0),总移动量就是把这两个矢量加起来。矢量的相加是指把每个元素分别相加,因此得到(5.0, –1.0,–3.0)。此时,数学表达式总是写成c=a+b,其中a和b是矢量,c是结果矢量。这与使用Vector结构的方式是一样的。
注意:
这个例子是作为一个结构来开发的,而不是类,但这并不重要。运算符重载用于结构和类时,其工作方式是一样的。
下面是Vector的定义
—— 包含成员字段、构造函数和一个ToString()重写方法,以便查看Vector的内容,最后是运算符重载:
namespace Wrox.ProCSharp.OOCSharp
{
struct Vector
{
public double x, y, z;
public Vector(double x, double y, double z)
{
this.x = x;
this.y = y;
this.z = z;
}
public Vector(Vector rhs)
{
x = rhs.x;
y = rhs.y;
z = rhs.z;
}
public override string ToString()
{
return "( " + x + " , " + y + " , " + z + " )";
}
这里提供了两个构造函数,通过传递每个元素的值,或者提供另一个复制其值的Vector,来指定矢量的初始值。第二个构造函数带一个Vector参数,通常称为复制构造函数,因为它们允许通过复制另一个实例来初始化一个类或结构实例。注意,为了简单起见,把字段设置为public。也可以把它们设置为private,编写相应的属性来访问它们,这样做不会改变这个程序的功能,只是代码会复杂一些。
下面是Vector结构的有趣部分
——为+运算符提供支持的运算符重载:
public static Vector operator + (Vector lhs, Vector rhs)
{
Vector result = new Vector(lhs);
result.x += rhs.x;
result.y += rhs.y;
result.z += rhs.z;
return result;
}
}
}
运算符声明的方式与方法声明的方式相同,但operator关键字告诉编译器,它实际上是一个运算符重载,后面是相关运算符的符号,在本例中就是+。返回类型是在使用这个运算符时获得的类型。在本例中,把两个矢量加起来会得到另一个矢量,所以返回类型就是
Vector
。对于这个
+
运算符重载,返回类型与包含类一样,但这种情况并不是必需的。两个参数就是要操作的对象。对于二元运算符
(
带两个参数
)
,如
+
和-运算符,第一个参数是放在运算符左边的对象或值,第二个参数是放在运算符右边的对象或值。
C#要求所有的运算符都声明为public和static,这表示它们与它们的类或结构相关联,而不是与对象相关联,所以运算符重载的代码体不能访问非静态类成员,也不能访问this指针;这是可以的,因为参数提供了运算符执行任务所需要知道的所有数据。
前面介绍了声明运算符+的语法,下面看看运算符内部的情况:
{
Vector result = new Vector(lhs);
result.x += rhs.x;
result.y += rhs.y;
result.z += rhs.z;
return result;
}
这部分代码与声明方法的代码是完全相同的,显然,它返回一个矢量,其中包含前面定义的lhs和rhs的和,即把x、y和z分别相加。
下面需要编写一些简单的代码,测试Vector结构:
static void Main()
{
Vector vect1, vect2, vect3;
vect1 = new Vector(3.0, 3.0, 1.0);
vect2 = new Vector(2.0,–4.0,–4.0);
vect3 = vect1 + vect2;
Console.WriteLine("vect1 = " + vect1.ToString());
Console.WriteLine("vect2 = " + vect2.
ToString());
Console.WriteLine("vect3 = " + vect3.ToString());
}
把这些代码保存为Vectors.cs,编译并运行它,结果如下:
Vectors
vect1 = ( 3 , 3 , 1 )
vect2 = ( 2 ,–4 ,–4 )
vect3 = ( 5 ,–1 ,–3 )
1. 添加更多的重载
矢量除了可以相加之外,还可以相乘、相减,比较它们的值。本节通过添加几个运算符重载,扩展了这个例子。这并不是一个功能全面的真实的Vector类型,但足以说明运算符重载的其他方面了。首先要重载乘法运算符,以支持一个标量和多个矢量的相乘以及一个矢量和多个矢量的相乘。
一个矢量乘以一个标量只是矢量的元素分别与标量相乘,例如,2 * (1.0, 2.5, 2.0)就等于(2.0, 5.0, 4.0)。相关的运算符重载如下所示。
public static Vector operator * (double lhs, Vector rhs)
{
return new Vector(lhs * rhs.x, lhs * rhs.y, lhs * rhs.z);
}
但这还不够,如果a和b被声明为Vector 类型,就可以编写下面的代码:
b = 2 * a;
编译器会隐式地把整数
2
转换为
double
类型,以符合运算符重载的要求。但不能编译下面的代码:
b = a * 2;
编译器处理运算符重载的方式和处理方法重载的方式是一样的。它会查看给定运算符的所有可用重载,找到与之最匹配的那个运算符重载。上面的语句要求第一个参数是一个Vector,第二个参数是一个整数,或者可以隐式转换为整数的其他数据类型。我们没有提供这样一个重载。有一个运算符重载,其参数是一个double和一个Vector,但编译器不能改变参数的顺序,所以这是不行的。还需要显式定义一个运算符重载,其参数是一个Vector和一个double,有两种方式可以定义这样一个运算符重载,第一种方式和处理所有的运算符的方式一样,显式中断矢量相乘操作:
public static Vector operator * (Vector lhs, double rhs)
{
return new Vector(rhs * lhs.x, rhs * lhs.y, rhs *lhs.z);
}
假定已经编写了执行相乘操作的代码,最好重复使用该代码:
public static Vector operator * (Vector lhs, double rhs)
{
return rhs * lhs;
}
这段代码会告诉编译器,如果有Vector和double的相乘操作,编译器就使参数的顺序反序,调用另一个运算符重载。在某种程度上,喜欢哪一个是由用户自己决定的。在本章的示例代码中,我们使用第二个版本,它看起来比较简洁。利用这个版本可以编写出维护性更好的代码,因为不需要复制代码,就可在两个独立的重载中执行相乘操作。
要重载的下一个运算符是矢量相乘。在数学上,矢量相乘有两种方式,但这里我们感兴趣的是点积或内积,其结果实际上是一个标量。这就是我们介绍这个例子的原因,所以算术运算符不必返回与定义它们的类相同的类型。
在数学上,如果有两个矢量(x, y, z)和(X, Y, Z),其内积就是x*X + y*Y + z*Z的值。两个矢量这样相乘是很奇怪的,但这是很有效的,因为它可以用于计算各种其他的数。当然,如果要使用Direct3D 或DirectDraw编写代码来显示复杂的3D图形,在计算对象放在屏幕上的什么位置时,常常需要编写代码来计算矢量的内积,作为中间步骤。这里我们关心的是编写出double X = a*b,其中a和b是矢量,并计算出它们的点积。相关的运算符重载如下所示:
public static double operator * (Vector lhs, Vector rhs)
{
return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
}
定义了算术运算符后,就可以用一个简单的测试方法来看看它们是否能正常运行:
static void Main()
{
// stuff to demonstrate arithmetic operations
Vector vect1, vect2, vect3;
vect1 = new Vector(1.0, 1.5, 2.0);
vect2 = new Vector(0.0, 0.0,–10.0);
vect3 = vect1 + vect2;
Console.WriteLine("vect1 = " + vect1);
Console.WriteLine("vect2 = " + vect2);
Console.WriteLine("vect3 = vect1 + vect2 = " + vect3);
Console.WriteLine("2*vect3 = " + 2*vect3);
vect3 += vect2;
Console.WriteLine("vect3+=vect2 gives " + vect3);
vect3 = vect1*2;
Console.WriteLine("Setting vect3=vect1*2 gives " + vect3);
double dot = vect1*vect3;
Console.WriteLine("vect1*vect3 = " + dot);
}
运行代码,得到如下所示的结果:
Vectors2
vect1 = ( 1 , 1.5 , 2 )
vect2 = ( 0 , 0 ,–10 )
vect3 = vect1 + vect2 = ( 1 , 1.5 ,–8 )
2*vect3 = ( 2 , 3 ,–16 )
vect3+=vect2 gives ( 1 , 1.5 ,–18 )
Setting vect3=vect1*2 gives ( 2 , 3 , 4 )
vect1*vect3 = 14.5
这说明,运算符重载会给出正确的结果,但如果仔细看看测试代码,就会惊奇地注意到,实际上我们使用的是没有重载的运算符
——相加赋值运算符+=:
vect3 += vect2;
Console.WriteLine("vect3 += vect2 gives " + vect3);
虽然+=一般用作运算符,但实际上其操作分为两个部分:相加和赋值。与C++不同,C#不允许重载=运算符,但如果重载+运算符,编译器就会自动使用+运算符的重载来执行+=运算符的操作。–=、&=、*=和/=赋值运算符也遵循此规则。
2. 比较运算符重载
C#中有6个比较运算符,它们分为3对:
●
== 和 !=
●
> 和 <
●
>= 和 <=
C#要求成对重载比较运算符。如果重载了==,也必须重载!=,否则会产生编译错误。另外,比较运算符必须返回bool类型的值。这是它们与算术运算符的根本区别。两个数相加或相减的结果,理论上取决于数的类型。而两个Vector的相乘会得到一个标量。另一个例子是.NET基类System.DateTime,两个DateTime相减,得到的结果不是DateTime,而是一个System.TimeSpan实例,但比较运算得到的如果不是bool类型的值,就没有任何意义。
注意:
在重载==和!=时,还应重载从System.Object中继承的Equals()和GetHashCode()方法,否则会产生一个编译警告。原因是Equals()方法应执行与==运算符相同的相等逻辑。
除了这些区别外,重载比较运算符所遵循的规则与算术运算符相同。但比较两个数并不像想象的那么简单,例如,如果比较两个对象引用,就是比较存储对象的内存地址。比较运算符很少进行这样的比较,所以必须编写运算符,比较对象的值,返回相应的布尔结果。下面给Vector类重载== 和 !=运算符。首先是== 的执行代码:
public static bool operator = = (Vector lhs, Vector rhs)
{
if (lhs.x = = rhs.x && lhs.y = = rhs.y && lhs.z = = rhs.z)
return true;
else
return false;
}
这种方式仅根据矢量组成部分的值,来对它们进行等于比较。对于大多数结构,这就是我们希望的,但在某些情况下,可能需要仔细考虑等于的含义,例如,如果有嵌入的类,是应比较对同一个对象的引用(浅度比较),还是应比较对象的值是否相等(深度比较)?
注意:
不要通过调用从System.Object中继承的Equals()方法的实例版本,来重载比较运算符,如果这么做,就会在objA是null时计算(objA==objB),这会产生一个异常,因为.NET运行库会试图计算null.Equals(objB)。采用其他方法(重写Equals()方法,调用比较运算符)比较安全。
还需要重载运算符!=,采用的方式如下:
public static bool operator != (Vector lhs, Vector rhs)
{
return ! (lhs == rhs);
}
像往常一样,用一些测试代码检查重写方法的工作情况,这次定义
3
个
Vector
,并进行比较:
static void Main()
{
Vector vect1, vect2, vect3;
vect1 = new Vector(3.0, 3.0,–10.0);
vect2 = new Vector(3.0, 3.0,–10.0);
vect3 = new Vector(2.0, 3.0, 6.0);
Console.WriteLine("vect1= =vect2 returns " + (vect1= =vect2));
Console.WriteLine("vect1= =vect3 returns " + (vect1= =vect3));
Console.WriteLine("vect2= =vect3 returns " + (vect2= =vect3));
Console.WriteLine();
Console.WriteLine("vect1!=vect2 returns " + (vect1!=vect2));
Console.WriteLine("vect1!=vect3 returns " + (vect1!=vect3));
Console.WriteLine("vect2!=vect3 returns " + (vect2!=vect3));
}
编译这些代码,会得到一个编译器警告,因为我们没有为Vector重写Equals(),对于本例,这是不重要的,所以我们会忽略它。
csc Vectors3.cs
Microsoft (R) Visual C# .NET Compiler version 7.00.9466
for Microsoft (R) .NET Framework version 1.0.3705
Copyright (C) Microsoft Corporation 2001. All rights reserved.
Vectors3.cs(5,11): warning CS0660: 'Wrox.ProCSharp.OOCSharp.Vector' defines
operator = = or operator != but does not override Object.Equals(object o)
Vectors3.cs(5,11): warning CS0661: 'Wrox.ProCSharp.OOCSharp.Vector' defines
operator = = or operator != but does not override Object.GetHashCode()
在命令行上运行该示例,生成如下结果:
Vectors3
vect1= =vect2 returns True
vect1= =vect3 returns False
vect2= =vect3 returns False
vect1!=vect2 returns False
vect1!=vect3 returns True
vect2!=vect3 returns True
3. 可以重载的运算符
并不是所有的运算符都可以重载。可以重载的运算符如表5-4所示。
表 5-4
类 别
运 算 符
限 制
算术二元运算符
+, *, /, –, %
无
算术一元运算符
+, –, ++, ––
无
按位二元运算符
&, |, ^, <<, >>
无
按位一元运算符
!, ~, true, false
true和false运算符必须成对重载
比较运算符
==, !=, >=, <, <=, >
必须成对重载
赋值运算符
+=,–=,*=,/=,>>=,<<=,%=
,&=,|=,^=
不能显式重载这些运算符,在重写单个运算符如+,–,%等时,它们会被隐式重写
索引运算符
[]
不能直接重载索引运算符。第2章介绍的索引器成员类型允许在类和结构上支持索引运算符
数据类型转换运算符
()
不能直接重载数据类型转换运算符。用户定义的数据类型转换(在本章的第2部分介绍)允许定义定制的数据类型转换
原文:http://blog.cdbook.cn/user1/cdbookcn/archives/2006/20061017125614.html
http://blog.cdbook.cn/user1/cdbookcn/archives/2006/20061017125746.html