资源描述
• 147 •
第5章 运算符和类型强制转换
第5章 运算符和类型强制转换
前几章介绍了使用C#编写程序所需要的大部分知识。本章将首先讨论基本语言元素,接着论述C#语言的扩展功能。本章的主要内容如下:
● C#中的可用运算符
● 处理引用类型和值类型时相等的含义
● 基本数据类型之间的数据转换
● 使用装箱技术把值类型转换为引用类型
● 通过强制转换技术在引用类型之间转换
● 重载标准的运算符,以支持对定制类型的操作
● 给定制类型添加强制转换运算符,以支持无缝的数据类型转换
5.1 运算符
C和C++开发人员应很熟悉大多数C#运算符,这里为新程序员和VB开发人员介绍最重要的运算符,并介绍C#中的一些新变化。
C#支持表5-1所示的运算符,其中有4个运算符(sizeof、*、–>、&)只能用于不安全的代码(这些代码绕过了C#类型安全性的检查),这些不安全的代码见第7章的讨论。
表 5-1
类 别
运 算 符
算术运算符
+ – * / %
逻辑运算符
& | ^ ~ && || !
字符串连接运算符
+
增量和减量运算符
++ – –
移位运算符
<< >>
比较运算符
== != < > <= >=
赋值运算符
= += –= *= /= %= &= |= ^= <<= >>=
成员访问运算符(用于对象和结构)
.
索引运算符(用于数组和索引器)
[]
数据类型转换运算符
()
条件运算符 (三元运算符)
?:
对象创建运算符
new
类型信息运算符
sizeof (只用于不安全的代码) is typeof as
溢出异常控制运算符
checked unchecked
间接寻址运算符
* –> & (只用于不安全代码) []
使用C#运算符的一个最大缺点是,与C风格的语言一样,赋值(=)和比较(==)运算使用不同的运算符。下述语句表示“x等于3”:
x = 3;
如果要比较x和另一个值,就需要使用两个等号(==):
if (x == 3)
C#非常严格的类型安全规则防止出现常见的C#错误,也就是在逻辑语句中使用赋值运算符代替比较运算符。在C#中,下述语句会产生一个编译错误:
if (x = 3)
习惯使用宏字符&来连接字符串的VB程序员必须改变这个习惯。在C#中,使用加号+连接字符串,而&表示两个不同整数值的按位AND运算。| 则在两个整数之间执行按位OR运算。VB程序员可能还没有使用过%(取模)运算符,它返回除运算的余数,例如,如果x等于7,则x% 5会返回2。
在C#中很少会用到指针,因此也很少会用到间接寻址运算符(–>)。使用它们的惟一场合是在不安全代码块中,因为只有在此C#才允许使用指针。
5.1.1 运算符的简化操作
表5-2列出了C#中的全部简化赋值运算符。
表 5-2
运算符的简化操作
等 价 于
x++, ++x
x = x + 1
x–,–x
x = x – 1
x+= y
x = x + y
x–= y
x = x – y
x *= y
x = x * y
x /= y
x = x / y
x %= y
x = x % y
x >>= y
x = x >> y
x <<= y
x = x << y
x &= y
x = x & y
x |= y
x = x | y
x ^= y
x = x ^ y
为什么有两个例子来说明++增量和– –减量运算符。把运算符放在表达式的前面称为前置,把运算符放在表达式的后面称为后置。表达式x++和++x都等于x = x +1,但它们的执行方式有所不同。
增量或减量运算符可以作用于整个表达式,也可以作用于表达式的内部。当x++和++x单独占一行时,它们的作用是相同的,对应于语句x = x + 1。但当它们用于表达式内部时,把运算符放在前面(++x)会在计算表达式之前递增x,换言之,递增了x后,在表达式中使用新值进行计算。而把运算符放在后面(x++)会在计算表达式之后递增x—— 使用原来的值计算表达式。下面的例子说明了它们的区别:
int x = 5;
if (++x == 6)
{
Console.WriteLine("This will execute");
}
if (x++ == 7)
{
Console.WriteLine("This won't");
}
第一个if条件得到true,因为在计算表达式之前,x从5递增为6。第二个if语句中的条件为false,因为在计算完整个表达式后,x才递增为7。
前置运算符––x和后置运算符x––与此类似,但它们是递减,而不是递增。
其他简化运算符,如+= 和–=需要两个操作数,用于执行算术、逻辑和按位运算,改变第一个操作数的值。例如,下面两行代码是等价的:
x += 5;
x = x + 5;
5.1.2 三元运算符
三元运算符?:是if...else结构的简化形式。其名称的出处是它带有三个操作数。它可以计算一个条件,如果条件为真,就返回一个值;如果条件为假,则返回另一个值。其语法如下:
condition ? true_value : false_value
其中condition是要计算的Boolean型表达式,true_value是condition为true时返回的值,false_value是condition为false时返回的值。
恰当地使用三元运算符,可以使程序非常简洁。它特别适合于给被调用的函数提供两个参数中的一个。使用它可以把Boolean值转换为字符串值true或false。它也很适合于显示正确的单数形式或复数形式,例如:
int x = 1;
string s = x.ToString() + " ";
s += (x == 1 ? "man" : "men");
Console.WriteLine(s);
如果x等于1,这段代码就显示 man,如果x等于其他数,就显示其正确的复数形式。但要注意,如果结果需要用在不同的语言中,就必须编写更复杂的例程,以考虑到不同语言的不同语法。
5.1.3 checked和 unchecked运算符
考虑下面的代码:
byte b = 255;
b++;
Console.WriteLine(b.ToString());
byte数据类型只能包含0~255的数,所以b值的增量会导致溢出。CLR如何处理这个溢出取决于许多方面,包括编译器选项,所以无论溢出有什么样的风险,都需要用某种方式确保得到我们希望的结果。
为此,C#提供了checked和 unchecked运算符。如果把一个代码块标记为checked,CLR就会执行溢出检查,如果发生溢出,就抛出异常。如果改变代码,使之包含checked运算符:
byte b = 255;
checked
{
b++;
}
Console.WriteLine(b.ToString());
运行这段代码,就会得到一个错误信息:
Unhandled Exception: System.OverflowException: Arithmetic operation resulted in an overflow.
at Wrox.ProCSharp.Basics.OverflowTest.Main(String[] args)
注意:
用/checked选项进行编译,就可以检查程序中所有未标记代码中的溢出。
如果要禁止溢出检查,可以把代码标记为unchecked:
byte b = 255;
unchecked
{
b++;
}
Console.WriteLine(b.ToString());
在本例中,不会抛出异常,但会丢失数据——因为byte数据类型不能包含256,溢出的位会被丢掉,所以b变量得到的值是0。
注意,unchecked是默认值。只有在需要把几个未检查的代码行放在一个明确标记为checked的大代码块中,才需要显式使用unchecked关键字。
5.1.4 is运算符
is运算符可以检查对象是否与特定的类型兼容。例如,要检查变量是否与object类型兼容:
注意:
“兼容”是指对象是该类型,或者派生于该类型。
int i = 10;
if (i is object)
{
Console.WriteLine("i is an object");
}
int和从object继承而来的其他C#数据类型一样,表达式i is object将得到true,并显示信息。
5.1.5 as运算符
as运算符用于执行引用类型的显式类型转换。如果要转换的类型与指定的类型兼容,转换就会成功进行;如果类型不兼容,as运算符就会返回值null。如下面的代码所示,如果object引用不指向string实例,把object引用转换为string就会返回null:
object o1 = "Some String";
object o2 = 5;
string s1 = o1 as string; //s1 = "Some String"
string s2 = o2 as string; //s1 = null
as运算符允许在一步中进行安全的类型转换,不需要先使用is运算符测试类型,再执行 转换。
5.1.6 sizeof运算符
使用sizeof运算符可以确定堆栈中值类型需要的长度(单位是字节):
string s = "A string";
unsafe
{
Console.WriteLine(sizeof(int));
}
其结果是显示数字4,因为int有4个字节。
注意,只能在不安全代码中使用sizeof运算符。第7章将详细论述不安全代码。
5.1.7 typeof运算符
Typeof运算符返回一个表示特定类型的Type对象。例如,typeof(string)返回表示System.String类型的Type对象。在使用反射动态查找对象的信息时,这个运算符是很有效的。第10章将介绍反射。
5.1.8 运算符的优先级
表5-3显示了C#运算符的优先级。表顶部的运算符有最高的优先级(即在包含多个运算符的表达式中,最先计算该运算符):
表 5-3
组
运 算 符
初级运算符
() . [] x++ x–– new typeof sizeof checked unchecked
一元运算符
+ – ! ~ ++x ––x和数据类型转换
乘/除运算符
* / %
加/减运算符
+ –
移位运算符
<< >>
关系运算符
< > <= >= is as
比较运算符
= = !=
按位AND运算符
&
按位XOR运算符
|
按位OR运算符
^
布尔 AND运算符
&&
布尔OR运算符
||
三元运算符
?:
赋值运算符
= += –= *= /= %= &= |= ^= <<= >>= >>>=
注意:
在复杂的表达式中,应避免利用运算符优先级来生成正确的结果。使用括号指定运算符的执行顺序,可以使代码更整洁,避免出现潜在的冲突。
5.2 类型的安全性
第1章提到中间语言(IL)可以对其代码强制加上强类型安全性。强类型支持.NET提供的许多服务,包括安全性和语言的交互性。因为C#这种语言将编译为IL,所以C#也是强类型的。这说明数据类型并不总是可互换的。本节将介绍基本类型之间的转换。
注意:
C#还支持在不同引用类型之间的转换,允许指定自己创建的数据类型如何与其他类型进行相互转换。这些论题将在本章后面讨论。
5.2.1 类型转换
我们常常需要把数据从一种类型转换为另一种类型。考虑下面的代码:
byte value1 = 10;
byte value2 = 23;
byte total;
total = value1 + value2;
Console.WriteLine(total);
在编译这些代码时,会产生一个错误:
Cannot implicitly convert type 'int' to 'byte' (不能把int类型隐式地转换为byte类型)。
问题是,我们把两个byte加在一起时,应返回int型结果,而不是另一个byte。这是因为byte包含的数据只能为8位,所以把两个byte加在一起很容易得到不能存储在一个byte中的值。如果要把结果存储在一个byte变量中,就必须把它转换回byte。有两种转换方式,隐式转换方式和显式转换方式。
1. 隐式转换方式
如果能保证值不会发生任何变化,类型转换就可以自动进行。这就是前面代码失败的原因:试图从int转换为byte,而潜在地丢失了3个字节的数据。编译器不会告诉我们该怎么做,除非我们显式地告诉它这就是我们希望的!如果在long型变量中存储结果,而不是byte型变量中,就不会有问题了:
byte value1 = 10;
byte value2 = 23;
long total; // this will compile fine
total = value1 + value2;
Console.WriteLine(total);
这是因为long型变量包含的数据字节比int型多,所以数据没有丢失的危险。在这些情况下,编译器会很顺利地进行转换,我们也不需要显式提出要求。
表5-4介绍了C#支持的隐式类型转换。
表 5-4
源 类 型
目 的 类 型
sbyte
short、int、long、float、double、decimal
byte
short、ushort、int、uint、long、ulong、float、double、decimal
short
int、long、float、double、decimal
ushort
int、uint、long、ulong、float、double、decimal
int
long、float、double、decimal
uint
long、ulong、float、double、decimal
long、ulong
float、double、decimal
float
double
char
ushort、int、uint、long、ulong、float、double、decimal
注意,只能从较小的整数类型隐式地转换为较大的整数类型,不能从较大的整数类型隐式地转换为较小的整数类型。也可以在整数和浮点数之间转换,其规则略有不同,可以在相同大小的类型之间转换,例如int/uint转换为 float,long/ulong转换为double,也可以从long/ulong转换回float。这样做可能会丢失4个字节的数据,但这仅表示得到的float值比使用double得到的值精度低,编译器认为这是一种可以接受的错误,而其值的大小是不会受到影响的。
无符号的变量可以转换为有符号的变量,只要无符号的变量值的大小在有符号的变量的范围之内即可。
2. 显式转换方式
有许多场合不能隐式地转换类型,否则编译器会报告错误。下面是不能进行隐式转换的一些场合:
● int转换为short—— 会丢失数据
● int转换为uint—— 会丢失数据
● uint转换为int—— 会丢失数据
● float转换为int—— 会丢失小数点后面的所有数据
● 任何数字类型转换为char—— 会丢失数据
● decimal转换为任何数字类型—— 因为decimal 类型的内部结构不同于整数和浮点数
但是,可以使用cast显示执行这些转换。在把一种类型转换为另一种类型时,要迫使编译器进行转换。类型转换的一般语法如下:
long val = 30000;
int i = (int)val; // A valid cast. The maximum int is 2147483647
这表示,把转换的目标类型名放在要转换的值之前的圆括号中。对于熟悉C的程序员来说,这是数据类型转换的典型语法。对于熟悉C++特殊数据类型转换关键字(如static_cast)的程序员来说,这些关键字是C#中不存在,必须使用旧的C风格的语法。
这种类型转换是一种比较危险的操作,即使在从long转换为int这样简单的转换过程中,如果原来long的值比int的最大值还大,就会出问题:
long val = 3000000000;
int i = (int)val; // An invalid cast. The maximum int is 2147483647
在本例中,不会报告错误,也得不到期望的结果。如果运行上面的代码,结果存储在i中,则其值为:
–1294967296
最好假定显式数据转换不会给出希望的结果。如前所述,C#提供了一个checked运算符,使用它可以测试操作是否会产生算术溢出。使用这个运算符可以检查数据类型转换是否安全,如果不安全,就会让运行时抛出一个溢出异常:
long val = 3000000000;
int i = checked ((int)val);
记住,所有的显式数据类型转换都可能不安全,在应用程序中应包含这样的代码,处理可能失败的数据类型转换。第11章将使用try和 catch语句引入结构化异常处理。
使用数据类型转换可以把大多数数据从一种基本类型转换为另一种基本类型。例如:
double price = 25.30;
int approximatePrice = (int)(price + 0.5);
这么做的代价是把价格四舍五入为最接近的美元数。但在这个转换过程中,小数点后面的所有数据都会丢失。因此,如果要使用这个修改过的价格进行更多的计算,最好不要使用这种转换;如果要输出全部计算完或部分计算完的近似值,且如果不希望用小数点后面的数据去麻烦用户,这种转换是很好的。
这个例子说明了把一个无符号的整数转换为char型时,会发生的情况:
ushort c = 43;
char symbol = (char)c;
Console.WriteLine(symbol);
结果是ASCII数为43的字符,即+号。可以尝试数字类型之间的任何转换(包括char),这种转换是成功的,例如把decimal转换为char,或把char转换为decimal。
值类型之间的转换并不仅限于孤立的变量。还可以把类型为double的数组元素转换为类型为int的结构成员变量:
struct ItemDetails
{
public string Description;
public int ApproxPrice;
}
//...
double[] Prices = { 25.30, 26.20, 27.40, 30.00 };
ItemDetails id;
id.Description = "Whatever";
id.ApproxPrice = (int)(Prices[0] + 0.5);
使用显式的数据类型转换方式,并小心使用这种技术,就可以把简单的值类型的任何实例转换为另一种类型。但在进行显式的类型转换时有一些限制,例如值类型,只能在数字、char类型和enum类型之间转换。不能直接把Boolean数据类型转换为其他类型,也不能把其他类型转换为Boolean数据类型。
如果需要在数字和字符串之间转换,.NET类库提供了一些方法。Object类有一个ToString()方法,该方法在所有的.NET预定义类型中都进行了重写,返回对象的字符串表示:
int i = 10;
string s = i.ToString();
同样,如果需要分析一个字符串,获得一个数字或Boolean值,就可以使用所有预定义值类型都支持的Parse方法:
string s = "100";
int i = int.Parse(s);
Console.WriteLine(i + 50); // Add 50 to prove it is really an int
注意,如果不能转换字符串(例如要把字符串Hello转换为一个整数),Parse方法就会注册一个错误,抛出一个异常。第11章将介绍异常。
5.2.2 装箱和取消装箱
第2章介绍了所有类型,包括简单的预定义类型,例如int和char,以及复合类型,例如从Object类型中派生的类和结构。下面可以像处理对象那样处理字面值:
string s = 10.ToString();
但是,C#数据类型可以分为在堆栈上分配内存的值类型和在堆上分配内存的引用类型。如果int不过是堆栈上一个4字节的值,该如何在它上面调用方法?
C#的实现方式是通过一个有点魔术性的方式,即装箱(boxing)。装箱和取消装箱(unboxing)可以把值类型转换为引用类型,或把引用类型转换为值类型。这已经在数据类型转换一节中介绍过了,即把值转换为object类型。装箱用于描述把一个值类型转换为引用类型。运行库会为堆上的对象创建一个临时的引用类型“box”。
该转换是隐式进行的,如上面的例子所述。还可以手工进行转换:
int i = 20;
object o = i;
取消装箱用于把引用类型转换为值类型,其中引用类型的值转换为值类型。这里使用术语"“cast”,是因为这种数据类型转换是显式进行的。其语法类似于前面的显式类型转换:
int i = 20;
object o = i; // Box the int
int j = (int)o; // Unbox it back into an int
只能把以前装箱的变量再转换为值类型。当o不是装箱后的int型时,如果执行最后一行,就会抛出一个异常。
这里有一个警告。在把引用类型转换为值类型时,必须非常小心,确保得到的值变量有足够的空间存储值类型的值中的所有字节。例如,C#的int只有32位,所以把long值类型(64位)转换为int时,会产生一个InvalidCastException异常:
long a = 333333423;
object b = (object)a;
int c = (int)b;
5.3 对象的相等比较
在讨论了运算符,并简要介绍了等于运算符后,就应考虑在处理类和结构的实例时相等意味着什么。理解对象相等比较的机制对编写逻辑表达式非常重要,另外,对执行运算符重载和数据类型转换也非常重要,本章的后面将讨论运算符重载。
对象相等比较的机制对于引用类型(类的实例)的比较和值类型(基本数据类型,结构或枚举的实例)的比较来说是不同的。下面分别介绍引用类型和值类型的相等比较。
5.3.1 引用类型的相等比较
System.Object的一个初看上去令人惊讶的方面是它定义了3个不同的方法,来比较对象的相等性:ReferenceEquals()和Equals()的两个版本。再加上比较运算符==,实际上有4种进行相等比较的方式。这些方法有一些微妙的区别,下面就介绍这些方法。
5.3.2 ReferenceEquals()方法
ReferenceEquals()是一个静态方法,测试两个引用是否指向类的同一个实例,特别是两个引用是否包含内存中的相同地址。作为静态方法,它不能重写,所以只能使用System.Object实现。如果提供的两个引用指向同一个对象实例,ReferenceEquals()总是返回true,否则就返回false。但是它认为null等于null:
SomeClass x, y;
x = new SomeClass();
y = new SomeClass();
bool B1 = ReferenceEquals(null, null); //return true
bool B2 = ReferenceEquals(null, x); //return false
bool B3 = ReferenceEquals(x, y); //return false because x and y
//point to different objects
5.3.3 虚拟的Equals()方法
Equals()虚拟版本的System.Object实现也比较引用。但因为这个方法是虚拟的,所以可以在自己的类中重写它,按值来比较对象。特别是如果希望类的实例用作字典中的键,就需要重写这个方法,以比较值,否则,根据重写Object.GetHashCode()的方式,包含对象的字典类要么不工作,要么工作的效率非常低。在重写Equals()方法时要注意,重写的代码不会抛出异常。这是因为如果抛出异常,字典类就会出问题,可能会包含其他在内部调用这个方法的.NET基类。
5.3.4 静态的Equals()方法
Equals()的静态版本与其虚拟实例版本的作用相同,其区别是静态版本带有两个参数,并对它们进行相等比较。这个方法可以处理两个对象中有一个是null的情况,因此,如果一个对象可能是null,这个方法就提供了禁止抛出异常的额外保护。静态重载首先要检查它传送的引用是否为null。如果它们都是null,就返回false。如果两个引用都不指向null,它就调用Equals()的虚拟实例版本。这表示在重写Equals()的实例版本时,其效果相当于也重写了静态版本。
5.3.5 比较运算符==
最好将比较运算符看作是严格值比较和严格引用比较之间的中间选项。在大多数情况下,下面的代码:
bool b = (x == y); //x, y object references
表示比较引用。但是,如果把一些类看作值,其含义就会比较直观。在这些情况下,最好重写比较运算符,以执行值的比较。后面将讨论运算符的重载,但显然它的一个例子是System.String类,Microsoft重写了这个运算符,比较字符串的内容,而不是它们的引用。
5.3.6 值类型的相等比较
在进行值类型的相等比较时,采用与引用类型相同的规则:ReferenceEquals()用于比较引用,Equals()用于比较值,比较运算符可以看作是一个中间项。但最大的区别是值类型需要装箱,才能把它们转换为引用,才能对它们执行方法。另外,Microsoft已经在System.ValueType类中重载了实例方法Equals(),以便对值类型进行合适的相等测试。如果调用sA.Equals(sB),其中sA和sB是某个结构的实例,则根据sA和sB是否在其所有的字段中包含相同的值,而返回true或false。另一方面,在默认情况下,可以对自己的结构使用==的未重载版本。在表达式中使用(sA==sB)会导致一个编译错误,除非在代码中为结构提供了==的重载版本。
另外,ReferenceEquals()在应用于值类型时,总是返回false,因为为了调用这个方法,值类型需要装箱到对象中。即使使用下面的代码:
bool b = ReferenceEquals(v, v); //v is a variable of some value type
也会返回false,因为在转换每个参数时,v都会被单独装箱,这意味着会得到不同的引用。调用ReferenceEquals()来比较值类型实际上没有什么意义。
尽管System.ValueType提供的Equals()默认重载肯定足以应付绝大多数自定义的结构,但仍可以为自己的结构重写它,以提高性能。另外,如果值类型包含作为字段的引用类型,就需要重写Equals(),以便为这些字段提供合适的语义,因为Equals()的默认重写仅比较它们的地址。
5.4 运算符重载
本节将介绍为类或结构定义的另一种类型的成员:运算符重载。
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)
{
展开阅读全文