欢迎光临散文网 会员登陆 & 注册

第 33 讲:面向对象编程(五):运算符重载和类型转换器

2021-05-06 08:16 作者:SunnieShine  | 我要投稿

今天我们要说的是类里的两个比较重要的成员:运算符(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# 是提供了这样的重载机制的,那就是之前说过的逻辑元运算符 truefalse 了。还是需要你注意,这里的 truefalse 不是一个布尔类型的字面量,而是真正的运算符。

还记得我们之前讲解逻辑元运算的例子吗?假设你在开车,开车能过马路将同时取决于你的油箱里的油的多少,和红绿灯的当前亮灯的颜色。因为我们说油箱油的多少和红绿灯亮灯的颜色我们都用了一个叫 Status 的数据类型来抽象表示了,那么它应该是有三个数值的:Green(OK)、Yellow(看起来可以,但有点危险)和 Red(完全不行)。那么,我们如果要想书写代码的话,我们就可以使用这样的语法:

假设我们要测试这个类型。

请注意这里的 GetFuelLaunchStatusGetNavigationLaunchStatus 方法,我们并未写出来,这仅仅表达的是“获取油箱油的状态”和“红绿灯状态”,然后返回 Status 类型的结果。可以从这个代码里看到,我们直接将 Status 的结果用 && 连起来了。刚才才说,&& 是不可重载的,可我们在写 Status 的代码里,确实是没有写类似 operator && 之类的重载的代码的,那么怎么这个代码就可以运行了呢?

之前我们说过,&& 等价于 false(a) ? a : a & b,所以,我们只需重载 false& 就可以得到 && 的重载规则的调用逻辑;同理,|| 等价于 true(a) ? a : a | b,所以我们只需要重载 true| 就可以得到 || 的调用逻辑了。所以,我们为什么没有重载 &&|| 的语法,就是因为这一点。

C# 团队这么设计语法(不让重载 &&|| 而是让你重载 truefalse 这种不是很好理解的运算符)是有原因的。还记得我们在处理布尔类型的 truefalse 的计算规则吗?拿 && 来说,如果两侧的数值有一个 false,结果就是 false。那么是不是可以展开 && 的运算规则成 false(a) ? a : a & b 呢?要是 afalsefalse(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) 这个强制转换器都行,因为它表达的是隐式转换。和前面 explicit operator 的写法类似,operator  后面紧跟的这个 bool 就是这个 Status 类型可以转换过去的类型。

2-3 转换器和逻辑元运算符的重载

前面我们说到了逻辑元运算符的重载规则和语法。可能你会有这样的疑问。我如果实现了 Statusbool 类型的隐式转换器的话(比如上面的这个例子的写法),那么是不是就意味着我们没有必要实现那些个 &|truefalse 这四个运算符了啊?显然直接一个隐式转换器就可以解决实现四个运算符的问题。

实际上,并不等价。你可以思考一点,在我们举例说明 Status 这个类型的实现规则的时候,如果油的状态是 Yellow 的时候,而且红绿灯也是 Yellow 这个状态的话,按规则我们得得到 Red 的结果才对,那么两个状态的 && 的结果一定是 false 的;但如果按照隐式转换的规则,两个状态全变成 true 了,使得运算结果肯定是 true,因此,并不能这么写代码。

Part 3 别在运算符重载和类型转换器里抛异常

因为运算符和类型转换器的语法规则比较特殊,所以我们不建议任何使用 C# 的朋友在执行的代码里抛异常。比如说这样:

这样实现代码虽然严谨,但不是良好的代码实现(Ill-formed Code)。运算符和类型转换器的语法是写成运算符的符号,以及类型转换器的小括号。如果在里面抛异常的话,比如假设我们写 bool condition = status; 这样的隐式转换语句,如果转换失效,那么显然 status 这里就会产生一个异常。但这仅仅是赋值语句,在别人看来就会产生困惑:这都哪儿跟哪儿啊,哪有赋值语句抛异常的。所以,bug 产生在这些地方会很隐蔽。

至此,我们就把类的所有成员都说完了一遍。


第 33 讲:面向对象编程(五):运算符重载和类型转换器的评论 (共 条)

分享到微博请遵守国家法律