本节将开发一个结构Vector,来演示运算符重载,这个Vector结构表示一个三维矢量。如果数学不是你的强项,不必担心,我们会使这个例子尽可能简单。三维矢量
只是三个(double)数字的一个集合,说明物体和原点之间的距离,表示数字的变量是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。对于这个+运算符重载,返回类型与包含类一样,但这种情况并不是必需的。两个参数就是要操作的对象。对于二元运算符(带两个参数),如+和-运算符,第一个参数是放在运算符左边的值,第二个参数是放在运算符右边的值。
注意:
一般把运算符左边的参数命名为lhs,运算符右边的的参数命名为rhs。
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 )
6.4.2 运算符重载的示例:Vector结构
本节将开发一个结构Vector,来演示运算符重载,这个Vector结构表示一个三维矢量。如果数学不是你的强项,不必担心,我们会使这个例子尽可能简单。三维矢量只是三个(double)数字的一个集合,说明物体和原点之间的距离,表示数字的变量是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),总移动量就是把这两个矢量加起来。矢量的相加是指把每个元素分别相加,因此得到(6.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图形,在计算对象放在屏幕上的什么位置时,常常需要编写代码来计算矢量的内积,作为中间步骤。这里我们关心的是使用Vector编写出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.cs),得到如下所示的结果:
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)); }
编译这些代码(下载代码中的Vectors3.cs),会得到一个编译器警告,因为我们没有为Vector重写Equals(),对于本例,这是不重要的,所以忽略它。
csc Vectors3.cs Microsoft (R) Visual C# 2005 Compiler version 8.00.50727.42 for Microsoft (R) Windows (R) 2005 Framework version 2.0.50727 Copyright (C) Microsoft Corporation 2001-2006. 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. 可以重载的运算符
并不是所有的运算符都可以重载。可以重载的运算符如表6-5所示。
表 6-5
类 别 | 运 算 符 | 限 制 |
算术二元运算符 | +, *, /, –, % | 无 |
算术一元运算符 | +, –, ++, –– | 无 |
按位二元运算符 | &, |, ^, <<, >> | 无 |
按位一元运算符 | !, ~, true, false | true和false运算符必须成对重载 |
比较运算符 | ==, !=, >=, <, <=, > | 必须成对重载 |
赋值运算符 | +=,–=,*=,/=,>>=,<<=,%=,&=,|=,^= | 不能显式重载这些运算符,在重写单个运算符如+,–,%等时,它们会被隐式重写 |
索引运算符 | [] | 不能直接重载索引运算符。第2章介绍的索引器成员类型允许在类和结构上支持索引运算符 |
数据类型转换运算符 | () | 不能直接重载数据类型转换运算符。用户定义的数据类型转换(本章的后面介绍)允许定义定制的数据类型转换 |