第 23 节 · 第三部分 面向对象

运算符重载

运算符重载就是用 operator 符号 写一个 public static 方法,告诉编译器自己写的类型遇到这个运算符怎么算;加号做什么全看方法里怎么写。

时长 8:07授课计划第 8 周

学完这一节,你能

讲义

跟着视频的顺序写。每行代码都有注释,代码和运行结果都是实际运行得到的。

1先试试:两个盒子能直接相加吗

Box 类表示一个盒子,有长、宽、高三项数据。什么都不准备,直接写 Box1 + Box2:

C# · 第 60 行
60Box3 = Box1 + Box2;// Box 里没写 operator+,编译器不知道两个盒子怎么加
编译报错
Program.cs(60,17): error CS0019: 运算符“+”无法应用于“Box”和“Box”类型的操作数

int、double 的加法早就定义好了,可两个盒子怎么加,编译器不知道。得由我们自己告诉它,这就是运算符重载。

2运算符重载怎么写

内置类型(比如 int)的运算符是改不了的,我们能做的是给自己写的类型定义运算符的含义。重载运算符其实就是一个方法,名字很特别:关键字 operator 后面紧跟运算符的符号;它也有返回类型和参数列表,前面的 public 和 static 都不能少。

C# · 第 30-36 行
30public static Box operator+ (Box b, Box c)// operator+:b 是加号左边的盒子,c 是右边的
31{// operator+ 开始
32 Box box = new Box();// new 一个新盒子,用来装结果
33 box.length = b.length + c.length;// 长相加:6 + 12 = 18
34 box.breadth = b.breadth + c.breadth;// 宽相加:7 + 13 = 20
35 box.height = b.height + c.height;// 高相加:5 + 10 = 15
36 return box;// 返回新盒子 (18, 20, 15);b 和 c 一点没变

以 Box1 (6, 7, 5) 加 Box2 (12, 13, 10) 为例,可以把它想成一台拼接台:长、宽、高各加各的,放进新盒子。原来的两个盒子一点都没变,加号给的是一个新盒子。

public 和 static 不能少

把 static 去掉,编译器就报错:

C# · 第 30 行
30public Box operator+ (Box b, Box c)// 去掉了 static,只剩 public
编译报错
Program.cs(30,26): error CS0558: User-defined operator 'Box.operator +(Box, Box)' must be declared static and public

改不了 int 的加法

在 Box 里写一个加号,两个参数都是 int:

C# · 第 29-32 行
29public static int operator+ (int a, int b)// 想改 int 的加法:两个参数都是 int,让 + 变成减法
30{// operator+(int, int) 开始
31 return a - b;// 做减法
32}// operator+(int, int) 结束
编译报错
Program.cs(29,33): error CS0563: 二元运算符的参数之一必须是包含类型

二元运算符的参数里,至少要有一个是包含类型(写这个运算符的那个类),在这里就是 Box。所以 int 加 int 的规则,谁也改不了。

3完整程序

Box 有三个私有字段和三个 set 方法,getVolume 算体积,operator+ 也写在 Box 类里面。长宽高是私有的,operator+ 为什么能读 b 和 c 的字段?因为 private 管的是整个类,同一个类里的代码都能访问。

C# · 第 69-73 行
69Box3 = Box1 + Box2;// 调用 operator+(Box1, Box2),Box3 拿到新盒子
70
71// Box3 的体积
72volume = Box3.getVolume();// 18 × 20 × 15 = 5400
73Console.WriteLine("Box3 的体积: {0}", volume);// 输出 5400,不是 210 + 1560
查看完整代码(77 行)
C#
1using System;// 引入 System 命名空间(Console 在里面)
2
3namespace OperatorOvlApplication// 定义命名空间 OperatorOvlApplication
4{// 命名空间 OperatorOvlApplication 开始
5 class Box// 定义 Box 类:表示一个盒子
6 {// Box 类开始
7 private double length; // 长度// 私有字段:长
8 private double breadth; // 宽度// 私有字段:宽
9 private double height; // 高度// 私有字段:高
10
11 public double getVolume()// getVolume:算体积
12 {// getVolume 开始
13 return length * breadth * height;// 长 × 宽 × 高
14 }// getVolume 结束
15 public void setLength( double len )// setLength:设置长
16 {// setLength 开始
17 length = len;// 把参数 len 存进 length
18 }// setLength 结束
19
20 public void setBreadth( double bre )// setBreadth:设置宽
21 {// setBreadth 开始
22 breadth = bre;// 把参数 bre 存进 breadth
23 }// setBreadth 结束
24
25 public void setHeight( double hei )// setHeight:设置高
26 {// setHeight 开始
27 height = hei;// 把参数 hei 存进 height
28 }// setHeight 结束
29 // 重载 + 运算符来把两个 Box 对象相加
30 public static Box operator+ (Box b, Box c)// operator+:b 是加号左边的盒子,c 是右边的
31 {// operator+ 开始
32 Box box = new Box();// new 一个新盒子,用来装结果
33 box.length = b.length + c.length;// 长相加:6 + 12 = 18
34 box.breadth = b.breadth + c.breadth;// 宽相加:7 + 13 = 20
35 box.height = b.height + c.height;// 高相加:5 + 10 = 15
36 return box;// 返回新盒子 (18, 20, 15);b 和 c 一点没变
37 }// operator+ 结束
38
39 }// Box 类结束
40
41 class Tester// 定义 Tester 类,放 Main
42 {// Tester 类开始
43 static void Main(string[] args)// 程序入口 Main 方法
44 {// Main 开始
45 Box Box1 = new Box(); // 声明 Box1,类型为 Box// 创建第一个盒子
46 Box Box2 = new Box(); // 声明 Box2,类型为 Box// 创建第二个盒子
47 Box Box3 = new Box(); // 声明 Box3,类型为 Box// Box3 先随便 new 一个,后面会换成相加的结果
48 double volume = 0.0; // 体积// 用来存体积
49
50 // Box1 详述
51 Box1.setLength(6.0);// Box1 的长 = 6
52 Box1.setBreadth(7.0);// Box1 的宽 = 7
53 Box1.setHeight(5.0);// Box1 的高 = 5
54
55 // Box2 详述
56 Box2.setLength(12.0);// Box2 的长 = 12
57 Box2.setBreadth(13.0);// Box2 的宽 = 13
58 Box2.setHeight(10.0);// Box2 的高 = 10
59
60 // Box1 的体积
61 volume = Box1.getVolume();// 6 × 7 × 5 = 210
62 Console.WriteLine("Box1 的体积: {0}", volume);// {0} 处填入 volume,输出 210
63
64 // Box2 的体积
65 volume = Box2.getVolume();// 12 × 13 × 10 = 1560
66 Console.WriteLine("Box2 的体积: {0}", volume);// 输出 1560
67
68 // 把两个对象相加
69 Box3 = Box1 + Box2;// 调用 operator+(Box1, Box2),Box3 拿到新盒子
70
71 // Box3 的体积
72 volume = Box3.getVolume();// 18 × 20 × 15 = 5400
73 Console.WriteLine("Box3 的体积: {0}", volume);// 输出 5400,不是 210 + 1560
74 Console.ReadKey();// 等待按键(现在的 VS 不写也会停住)
75 }// Main 结束
76 }// Tester 类结束
77}// 命名空间结束
运行结果
Box1 的体积: 210
Box2 的体积: 1560
Box3 的体积: 5400

很多人会猜 Box3 的体积是 210 + 1560 = 1770,实际是 5400:我们的加号加的是长、宽、高,不是体积。Box3 的长宽高是 18、20、15,乘起来正好 5400。加号到底做什么,全看方法里怎么写。

4哪些运算符能重载

  • 一元运算符(只有一个操作数,操作数就是参与运算的值):+ - ! ~ ++ --,还有 true 和 false,都能重载。
  • 二元运算符(有两个操作数):+ - * / %,还有与 &、或 |、异或 ^、左移 <<、右移 >>,都能重载。
  • 比较运算符:== != < > <= >= 能重载,但必须成对写:写了 == 就得写 !=,写了 < 就得写 >,<= 和 >= 也一样。

只留 ==、删掉 !=:

C# · 第 39 行
39public static bool operator == (Box lhs, Box rhs)// 只写了 ==,下面没写 !=
编译报错
Program.cs(39,35): error CS0216: 运算符“Box.operator ==(Box, Box)”要求也要定义匹配的运算符“!=”

5哪些不能重载

&& 和 ||

并且 &&、或者 || 这两个条件运算符不能直接重载:

C# · 第 29-32 行
29public static Box operator&& (Box b, Box c)// 想重载 &&(并且)
30{// operator&& 开始
31 return b;// 随便返回左边的盒子
32}// operator&& 结束
编译报错
Program.cs(29,33): error CS1020: 应输入可重载的二元运算符

想让它们能用,要改为重载 &、|,再加上 true 和 false。

+= 这类复合赋值

+=、-= 这类复合赋值(先算再赋值回去)不用单独写:只要重载了 +,+= 就自动能用。

C# · 第 69-70 行
69Box3 = Box1;// Box3 先指向 Box1 这个盒子
70Box3 += Box2;// 等于 Box3 = Box3 + Box2,得到 (18, 20, 15)

运行结果和上面一样,Box3 的体积还是 5400,因为 += 用的就是我们写的加号。

从 C# 14(.NET 10) 开始,+= 也能自己写(教程说它不能重载,这个说法已过时)。它不带 static,返回 void,直接改左边那个盒子:

C# · 第 29-34 行
29public void operator+= (Box c)// 不带 static、返回 void,参数 c 是右边的盒子
30{// operator+= 开始
31 length += c.length;// 直接改左边这个盒子自己的长:加上 c 的长
32 breadth += c.breadth;// 宽也加上 c 的宽
33 height += c.height;// 高也加上 c 的高
34}// operator+= 结束

这段代码用 .NET 10 编译通过,Box3 的体积还是 5400;换成 .NET 9 就报错:

编译报错
Program.cs(29,27): error CS1019: 应输入可重载的一元运算符

完全不能重载的

赋值 =、点 .、?:、->、new、is、sizeof、typeof,这些都不能重载。

6扩展实例:重载比较运算符

==:长、宽、高都相等,才返回 true。!= 正好反过来,只要有一项不相等就返回 true。

C# · 第 39-48 行
39public static bool operator == (Box lhs, Box rhs)// 重载 ==:返回 bool(true 或 false)
40{// operator == 开始
41 bool status = false;// 先假设结果是 false
42 if (lhs.length == rhs.length && lhs.height == rhs.height// 长、高相等,&& 表示“并且”……
43 && lhs.breadth == rhs.breadth)// ……宽也相等,三项全相等才成立
44 {// if 开始
45 status = true;// 条件成立,结果改成 true
46 }// if 结束
47 return status;// 返回比较结果
48}// operator == 结束

< 要求三项都更小,> 要求三项都更大,<=、>= 也一样要三项都满足:

C# · 第 59-68 行
59public static bool operator <(Box lhs, Box rhs)// 重载 <:返回 bool(true 或 false)
60{// operator < 开始
61 bool status = false;// 先假设结果是 false
62 if (lhs.length < rhs.length && lhs.height// 长更小、高也更小……
63 < rhs.height && lhs.breadth < rhs.breadth)// ……宽也更小,三项都更小才成立
64 {// if 开始
65 status = true;// 条件成立,结果改成 true
66 }// if 结束
67 return status;// 返回比较结果
68}// operator < 结束

还有一个 ToString 方法,把盒子写成括号里的三个数。它前面写的是 override,这是上一节讲的重写,不是重载(教程注释里写成了“重载”)。

C# · 第 102-105 行
102public override string ToString()// override 是重写(上一节讲的),不是重载
103{// ToString 开始
104 return String.Format("({0}, {1}, {2})", length, breadth, height);// 把长宽高拼成 (长, 宽, 高) 这样的文字
105}// ToString 结束

拿 Box1 (6, 7, 5) 和 Box2 (12, 13, 10) 比:Box1 < Box2 时 6 比 12、7 比 13、5 比 10 三项都更小,结果 true;Box1 >= Box2 三项都不满足,结果 false。

C# · 第 157-160 行
157if (Box1 >= Box2)// 调用 operator >=:三项都不满足,结果 false
158 Console.WriteLine("Box1 大于等于 Box2");// 条件成立时输出这句(这次不走)
159else// 否则
160 Console.WriteLine("Box1 不大于等于 Box2");// 输出 Box1 不大于等于 Box2

最后一行 Box3 等于 Box4:前面让 Box4 指向了 Box3,两个变量其实是同一个盒子,长宽高当然都相等。

C# · 第 169-171 行
169Box4 = Box3;// Box4 改成指向 Box3 那个盒子:两个变量,同一个盒子
170if (Box3 == Box4)// 调用 operator ==:同一个盒子,长宽高当然都相等,结果 true
171 Console.WriteLine("Box3 等于 Box4");// 输出 Box3 等于 Box4
查看完整代码(178 行)
C#
1using System;// 引入 System 命名空间(Console 在里面)
2
3namespace OperatorOvlApplication// 定义命名空间 OperatorOvlApplication
4{// 命名空间 OperatorOvlApplication 开始
5 class Box// 定义 Box 类:表示一个盒子
6 {// Box 类开始
7 private double length; // 长度// 私有字段:长
8 private double breadth; // 宽度// 私有字段:宽
9 private double height; // 高度// 私有字段:高
10
11 public double getVolume()// getVolume:算体积
12 {// getVolume 开始
13 return length * breadth * height;// 长 × 宽 × 高
14 }// getVolume 结束
15 public void setLength( double len )// setLength:设置长
16 {// setLength 开始
17 length = len;// 把参数 len 存进 length
18 }// setLength 结束
19
20 public void setBreadth( double bre )// setBreadth:设置宽
21 {// setBreadth 开始
22 breadth = bre;// 把参数 bre 存进 breadth
23 }// setBreadth 结束
24
25 public void setHeight( double hei )// setHeight:设置高
26 {// setHeight 开始
27 height = hei;// 把参数 hei 存进 height
28 }// setHeight 结束
29 // 重载 + 运算符来把两个 Box 对象相加
30 public static Box operator+ (Box b, Box c)// operator+:b 是加号左边的盒子,c 是右边的
31 {// operator+ 开始
32 Box box = new Box();// new 一个新盒子,用来装结果
33 box.length = b.length + c.length;// 长相加:6 + 12 = 18
34 box.breadth = b.breadth + c.breadth;// 宽相加:7 + 13 = 20
35 box.height = b.height + c.height;// 高相加:5 + 10 = 15
36 return box;// 返回新盒子 (18, 20, 15);b 和 c 一点没变
37 }// operator+ 结束
38
39 public static bool operator == (Box lhs, Box rhs)// 重载 ==:返回 bool(true 或 false)
40 {// operator == 开始
41 bool status = false;// 先假设结果是 false
42 if (lhs.length == rhs.length && lhs.height == rhs.height// 长、高相等,&& 表示“并且”……
43 && lhs.breadth == rhs.breadth)// ……宽也相等,三项全相等才成立
44 {// if 开始
45 status = true;// 条件成立,结果改成 true
46 }// if 结束
47 return status;// 返回比较结果
48 }// operator == 结束
49 public static bool operator !=(Box lhs, Box rhs)// 重载 !=:返回 bool(true 或 false)
50 {// operator != 开始
51 bool status = false;// 先假设结果是 false
52 if (lhs.length != rhs.length || lhs.height != rhs.height// || 表示“或者”:长或高有一项不等……
53 || lhs.breadth != rhs.breadth)// ……或宽不等,只要一项不等就成立
54 {// if 开始
55 status = true;// 条件成立,结果改成 true
56 }// if 结束
57 return status;// 返回比较结果
58 }// operator != 结束
59 public static bool operator <(Box lhs, Box rhs)// 重载 <:返回 bool(true 或 false)
60 {// operator < 开始
61 bool status = false;// 先假设结果是 false
62 if (lhs.length < rhs.length && lhs.height// 长更小、高也更小……
63 < rhs.height && lhs.breadth < rhs.breadth)// ……宽也更小,三项都更小才成立
64 {// if 开始
65 status = true;// 条件成立,结果改成 true
66 }// if 结束
67 return status;// 返回比较结果
68 }// operator < 结束
69
70 public static bool operator >(Box lhs, Box rhs)// 重载 >:返回 bool(true 或 false)
71 {// operator > 开始
72 bool status = false;// 先假设结果是 false
73 if (lhs.length > rhs.length && lhs.height// 长更大、高也更大……
74 > rhs.height && lhs.breadth > rhs.breadth)// ……宽也更大,三项都更大才成立
75 {// if 开始
76 status = true;// 条件成立,结果改成 true
77 }// if 结束
78 return status;// 返回比较结果
79 }// operator > 结束
80
81 public static bool operator <=(Box lhs, Box rhs)// 重载 <=:返回 bool(true 或 false)
82 {// operator <= 开始
83 bool status = false;// 先假设结果是 false
84 if (lhs.length <= rhs.length && lhs.height// 长、高都小于或等于……
85 <= rhs.height && lhs.breadth <= rhs.breadth)// ……宽也小于或等于,三项都满足才成立
86 {// if 开始
87 status = true;// 条件成立,结果改成 true
88 }// if 结束
89 return status;// 返回比较结果
90 }// operator <= 结束
91
92 public static bool operator >=(Box lhs, Box rhs)// 重载 >=:返回 bool(true 或 false)
93 {// operator >= 开始
94 bool status = false;// 先假设结果是 false
95 if (lhs.length >= rhs.length && lhs.height// 长、高都大于或等于……
96 >= rhs.height && lhs.breadth >= rhs.breadth)// ……宽也大于或等于,三项都满足才成立
97 {// if 开始
98 status = true;// 条件成立,结果改成 true
99 }// if 结束
100 return status;// 返回比较结果
101 }// operator >= 结束
102 public override string ToString()// override 是重写(上一节讲的),不是重载
103 {// ToString 开始
104 return String.Format("({0}, {1}, {2})", length, breadth, height);// 把长宽高拼成 (长, 宽, 高) 这样的文字
105 }// ToString 结束
106
107 }// Box 类结束
108
109 class Tester// 定义 Tester 类,放 Main
110 {// Tester 类开始
111 static void Main(string[] args)// 程序入口 Main 方法
112 {// Main 开始
113 Box Box1 = new Box(); // 声明 Box1,类型为 Box// 创建第一个盒子
114 Box Box2 = new Box(); // 声明 Box2,类型为 Box// 创建第二个盒子
115 Box Box3 = new Box(); // 声明 Box3,类型为 Box// Box3 先随便 new 一个
116 Box Box4 = new Box();// Box4 也先 new 一个,后面会改成指向 Box3
117 double volume = 0.0; // 体积// 用来存体积
118
119 // Box1 详述
120 Box1.setLength(6.0);// Box1 的长 = 6
121 Box1.setBreadth(7.0);// Box1 的宽 = 7
122 Box1.setHeight(5.0);// Box1 的高 = 5
123
124 // Box2 详述
125 Box2.setLength(12.0);// Box2 的长 = 12
126 Box2.setBreadth(13.0);// Box2 的宽 = 13
127 Box2.setHeight(10.0);// Box2 的高 = 10
128
129 // 使用重载的 ToString() 显示两个盒子
130 Console.WriteLine("Box1: {0}", Box1.ToString());// 输出 Box1: (6, 7, 5)
131 Console.WriteLine("Box2: {0}", Box2.ToString());// 输出 Box2: (12, 13, 10)
132
133 // Box1 的体积
134 volume = Box1.getVolume();// 6 × 7 × 5 = 210
135 Console.WriteLine("Box1 的体积: {0}", volume);// 输出 210
136
137 // Box2 的体积
138 volume = Box2.getVolume();// 12 × 13 × 10 = 1560
139 Console.WriteLine("Box2 的体积: {0}", volume);// 输出 1560
140
141 // 把两个对象相加
142 Box3 = Box1 + Box2;// 调用 operator+:Box3 = (18, 20, 15)
143 Console.WriteLine("Box3: {0}", Box3.ToString());// 输出 Box3: (18, 20, 15)
144 // Box3 的体积
145 volume = Box3.getVolume();// 18 × 20 × 15 = 5400
146 Console.WriteLine("Box3 的体积: {0}", volume);// 输出 5400
147
148 //comparing the boxes
149 if (Box1 > Box2)// 调用 operator >:6 > 12 不成立,结果 false
150 Console.WriteLine("Box1 大于 Box2");// 条件成立时输出这句(这次不走)
151 else// 否则
152 Console.WriteLine("Box1 不大于 Box2");// 输出 Box1 不大于 Box2
153 if (Box1 < Box2)// 调用 operator <:6 < 12、5 < 10、7 < 13 都成立,结果 true
154 Console.WriteLine("Box1 小于 Box2");// 输出 Box1 小于 Box2
155 else// 否则
156 Console.WriteLine("Box1 不小于 Box2");// 条件不成立时输出这句(这次不走)
157 if (Box1 >= Box2)// 调用 operator >=:三项都不满足,结果 false
158 Console.WriteLine("Box1 大于等于 Box2");// 条件成立时输出这句(这次不走)
159 else// 否则
160 Console.WriteLine("Box1 不大于等于 Box2");// 输出 Box1 不大于等于 Box2
161 if (Box1 <= Box2)// 调用 operator <=:三项都满足,结果 true
162 Console.WriteLine("Box1 小于等于 Box2");// 输出 Box1 小于等于 Box2
163 else// 否则
164 Console.WriteLine("Box1 不小于等于 Box2");// 条件不成立时输出这句(这次不走)
165 if (Box1 != Box2)// 调用 operator !=:长 6 和 12 就不等,结果 true
166 Console.WriteLine("Box1 不等于 Box2");// 输出 Box1 不等于 Box2
167 else// 否则
168 Console.WriteLine("Box1 等于 Box2");// 条件不成立时输出这句(这次不走)
169 Box4 = Box3;// Box4 改成指向 Box3 那个盒子:两个变量,同一个盒子
170 if (Box3 == Box4)// 调用 operator ==:同一个盒子,长宽高当然都相等,结果 true
171 Console.WriteLine("Box3 等于 Box4");// 输出 Box3 等于 Box4
172 else// 否则
173 Console.WriteLine("Box3 不等于 Box4");// 条件不成立时输出这句(这次不走)
174
175 Console.ReadKey();// 等待按键(现在的 VS 不写也会停住)
176 }// Main 结束
177 }// Tester 类结束
178}// 命名空间结束
运行结果
Box1: (6, 7, 5)
Box2: (12, 13, 10)
Box1 的体积: 210
Box2 的体积: 1560
Box3: (18, 20, 15)
Box3 的体积: 5400
Box1 不大于 Box2
Box1 小于 Box2
Box1 不大于等于 Box2
Box1 小于等于 Box2
Box1 不等于 Box2
Box3 等于 Box4

编译时还有两条警告:

编译警告
Program.cs(5,11): warning CS0660: “Box”定义运算符 == 或运算符 !=,但不重写 Object.Equals(object o)
Program.cs(5,11): warning CS0661: “Box”定义运算符 == 或运算符 !=,但不重写 Object.GetHashCode()

重载了 == 和 !=,最好也重写 Equals(每个对象都有的“判断相等”方法)和 GetHashCode(算出对象的哈希码,给字典这类集合用,后面会讲),这样用 Equals 判断相等时,结果才和 == 一致。

7小结

  • 运算符重载就是用 operator 加符号,写一个 public static 的方法,告诉编译器自己写的类型遇到这个运算符该怎么算。
  • 加号做什么全看方法里怎么写,Box 的加号加的是长宽高。
  • 比较运算符要成对写;&&、|| 不能直接重载。
  • 重载了 +,+= 自动能用,C# 14 起还能自己写。

下一节:如果好几个类都必须提供同样的方法,能不能像签合同一样,提前把这件事定下来?

检查题

5 道,选完马上告诉你对不对。

  1. 1没写任何运算符重载,直接写 Box3 = Box1 + Box2; 会怎样?

  2. 2Box1(6,7,5)、Box2(12,13,10),教程的 operator+ 下 Box3 的体积是?

  3. 3Box 里只写 operator ==、不写 operator !=,会怎样?

  4. 4只重载了 +,写 Box3 += Box2; 会怎样?

  5. 5Box4 = Box3; 之后 Box3 == Box4 打印什么?