第 33 讲:面向对象编程(五):运算符重载和类型转换器
运算符(Operator)和类型转换器(Cast)。
实际上类还有三个成员类型还没有说:运算符、类型转换器和事件(Event),不过因为事件这个成员需要委托类型的依赖,而委托类型我们现在还没有讲,难度也比较大,因此我们只能等到后面才能说了。
Part 1 运算符系统和运算符重载
要了解运算符,就得了解运算符在 C# 里的体系架构。你可能会问我,运算符不是早就讲过了吗?是的,不过……没有那么容易。
1-1 运算符重载的语法
要知道运算符在使用的时候是有类型、优先级一说。优先级我们并未在之前提到过,因为计算顺序一般跟我们人类的计算思路和思维是差不多的,因此我们不必去记住这些运算符的顺序(比如先乘除后加减之类的)。但是,运算符实际上是和数据类型绑定的一种概念。这句话是什么意思呢?字符串的加号是拼接字符串,而数值的加法是计算求和的结果。它们表达的意义是不同的。在 C# 里,为了帮助我们理解和自定义使用运算符的逻辑过程,运算符重载的概念就产生了。运算符的书写格式是这样的:
比如说,字符串的拼接运算可通过这样的语法写成这样:
这里用到了
string
的一个构造器。C# 的那些个值类型是不需要这么书写代码的,因为直接有字面量的支持和变量的赋值,所以我们基本上碰不到那些构造器(实际上比如说decimal
类型,就有自己的构造器,但我们基本上碰不到它们)。字符串是一个特殊的内置类型,因为它是一个引用类型。所谓的引用类型就是内存大小不定的、总是以引用进行传递的数据类型。在这个定义下,所有类声明出来,一经实例化(
new
语句产生了对象),那么它们就都是引用类型的对象。而这个引用类型有一些很特殊的构造器,比如说传入一个char[]
,将其转换成一个string
这样的代码我们称为运算符重载(Operator Overloading)。这里用的重载和我们实际上说的方法的重载类似,也有一点不同。毕竟这里的运算符重载是基于这里的运算符的,所以书写格式非常诡异。
另外顺带一提,我们需要注意的运算符重载有如下的一些规则:
重载的所有运算符,参数或者返回值的类型必须至少包含这个类型本身;
重载的运算符并非全部都可以,比如
&&
、=
、? :
(条件运算符) 这类本身就有固定含义的运算符就不可重载;重载的运算符有些时候是配套的,比如
+
(加法)在重载之后,那么+=
就自动被重载,因为a = a + b
里可写成a += b
,而这个运算符是可翻译成+
计算模式的。因此你实现了+
的重载规则,+=
就不必单独写出来了(实际上也不让你单独为+=
这样的复合赋值运算符重载;重载的运算符有些时候是成对的,比如
>
(大于)运算符在重载之后,你就必须要把<
(小于)也一起重载了;==
和!=
也是;>=
和<=
也是;重载的
>>
(位右移运算符)和<<
(位左移运算符)需要传入两个参数,而参数的右侧必须是一个int
类型的参数,即你只能写成比如public static 类型 operator >>(类型 变量, int 参数)
。这个int
你是不可以随便换的;重载的运算符必须是
public static
修饰的。
这些规则需要你记住,因为它们约束了运算符不能随便乱写和乱用。比如 %
只能是左右两个数值计算,而你不能改变篡改定义,改成只需要一个数就参与数值计算。
这么做是有道理的。因为运算符本身就不一定非要跟一个实体对象绑定起来,比如加法用的就是两个数值的计算过程,因此定义成 static
是有道理的;而 public
是因为,如果你不暴露运算符给外界的话,你还不如不用运算符,因为没有意义。当初你在设计数据类型,把它们写成类的时候,肯定就是想希望这些东西外部都能用,对吧;哪有你写个运算符,结果只让类内部随便用,出了类就不能用的道理呢?
尽管约束很多,但我们仍旧有很多很丰富的选择和使用。这就是为了保证运算符在执行的时候,语义和语法模型不会变动。比如说 +
(加号),就一定是两个数值运算。如果你改成了一个数,加号就不叫加号了;改成三个数计算,那么 +
又怎么书写的问题。运算符的优先级是系统规定的,所以我们不可能通过重载改变这样的规则,比如我重载了 +
,怎么着它的优先级也必须是在乘除号之后计算的,毕竟先乘除后加减的规则是不可撼动的。所以,重载运算符仅仅是为了提供一些简单的语义模型的拓展,但我们不能篡改语义模型的体系。
这里唯一需要说的重载规则,就是逻辑元运算符。
1-2 逻辑元运算符的重载
很奇妙。C# 完全不允许我们重载 &&
和 ||
,但我们为了重载 &&
和 ||
,C# 是提供了这样的重载机制的,那就是之前说过的逻辑元运算符 true
和 false
了。还是需要你注意,这里的 true
和 false
不是一个布尔类型的字面量,而是真正的运算符。
还记得我们之前讲解逻辑元运算的例子吗?假设你在开车,开车能过马路将同时取决于你的油箱里的油的多少,和红绿灯的当前亮灯的颜色。因为我们说油箱油的多少和红绿灯亮灯的颜色我们都用了一个叫 Status
的数据类型来抽象表示了,那么它应该是有三个数值的:Green
(OK)、Yellow
(看起来可以,但有点危险)和 Red
(完全不行)。那么,我们如果要想书写代码的话,我们就可以使用这样的语法:
假设我们要测试这个类型。
请注意这里的 GetFuelLaunchStatus
和 GetNavigationLaunchStatus
方法,我们并未写出来,这仅仅表达的是“获取油箱油的状态”和“红绿灯状态”,然后返回 Status
类型的结果。可以从这个代码里看到,我们直接将 Status
的结果用 &&
连起来了。刚才才说,&&
是不可重载的,可我们在写 Status
的代码里,确实是没有写类似 operator &&
之前我们说过,&&
等价于 false(a) ? a : a & b
,所以,我们只需重载 false
和 &
就可以得到 &&
的重载规则的调用逻辑;同理,||
等价于 true(a) ? a : a | b
,所以我们只需要重载 true
和 |
就可以得到 ||
的调用逻辑了。所以,我们为什么没有重载 &&
和 ||
的语法,就是因为这一点。
C# 团队这么设计语法(不让重载 &&
和 ||
而是让你重载 true
和 false
这种不是很好理解的运算符)是有原因的。还记得我们在处理布尔类型的 true
和 false
的计算规则吗?拿 &&
来说,如果两侧的数值有一个 false
,结果就是 false
。那么是不是可以展开 &&
的运算规则成 false(a) ? a : a & b
呢?要是 a
是 false
(false(a)
表达式成立),那么我们直接取 a
的数值(false
这个字面量)作为表达式结果;否则,我们就得将 a & b
按照 &
的运算规则计算出来,把得到的结果给返回出来。这个语法保证了 &&
和 ||
运算符的短路现象依然在重载运算符之后还存在。这下知道为什么 &&
里用 false
这个逻辑元运算符了吧:因为在布尔型表达式计算的时候,本身就是“&&
表达式里有一方是 false
,结果就会发生短路”,这正好就对应了逻辑元运算符用的这个 false
符号。所以它们是相通的。
Part 2 类型转换系统和类型转换器
C# 还有一个很骚的操作是,它直接允许我们自定义转换器,将当前类型直接转换成别的数据类型。只要你想,什么类型都可以转。
2-1 强制类型转换器
Status
的实体对象在使用的时候可以写成 bool condition = (bool)status
的语法。这里的 operator
后面的这个 bool
就是表示 Status
类型的对象可转换过去的类型。
语法上,我们使用 explicit
关键字来表达是强制转换。这个词很少用到,它在英语里是“必须有的”、“明确的”、“直率的”的意思。比如说例句:
That wasn't an explicit rule in the meeting, but I'm sure that was part of it, you know.
这倒不是在集会上必有的原则,但是我相信这是其中一个。
咳咳咳,这个不是英语课。反正大概是这么一个词语。因为 explicit
关键字意思是“显式的”,所以 explicit operator
是强制转换的意思。
2-2 隐式类型转换器
比如还是前面这个例子,我们把 explicit
改成 implicit
即可。这样的话,类型就允许直接将 Status
类型转换成 bool
,比如 bool condition = status
的写法,你甚至可以不写 (bool)
这个强制转换器都行,因为它表达的是隐式转换。和前面 的写法类似,operator
后面紧跟的这个 bool
就是这个 Status
类型可以转换过去的类型。
2-3 转换器和逻辑元运算符的重载
前面我们说到了逻辑元运算符的重载规则和语法。可能你会有这样的疑问。我如果实现了 Status
到 bool
类型的隐式转换器的话(比如上面的这个例子的写法),那么是不是就意味着我们没有必要实现那些个 &
、|
、true
、false
这四个运算符了啊?显然直接一个隐式转换器就可以解决实现四个运算符的问题。
实际上,并不等价。你可以思考一点,在我们举例说明 Status
这个类型的实现规则的时候,如果油的状态是 Yellow
的时候,而且红绿灯也是 Yellow
这个状态的话,按规则我们得得到 Red
的结果才对,那么两个状态的 &&
的结果一定是 false
的;但如果按照隐式转换的规则,两个状态全变成 true
了,使得运算结果肯定是 true
,因此,并不能这么写代码。
Part 3 别在运算符重载和类型转换器里抛异常
因为运算符和类型转换器的语法规则比较特殊,所以我们不建议任何使用 C# 的朋友在执行的代码里抛异常。比如说这样:
这样实现代码虽然严谨,但不是良好的代码实现(Ill-formed Code)。运算符和类型转换器的语法是写成运算符的符号,以及类型转换器的小括号。如果在里面抛异常的话,比如假设我们写 bool condition = status;
这样的隐式转换语句,如果转换失效,那么显然 status
这里就会产生一个异常。但这仅仅是赋值语句,在别人看来就会产生困惑:这都哪儿跟哪儿啊,哪有赋值语句抛异常的。所以,bug 产生在这些地方会很隐蔽。
至此,我们就把类的所有成员都说完了一遍。