CSharp(三十二) 运算符重载(Operator Overloading)详解
目录
- 什么是运算符重载
- 为什么需要运算符重载
- 基本语法骨架
- 一步步写出你的第一个运算符重载
- 运算符重载的工作原理
- 一元运算符重载
- 二元运算符重载
- 比较运算符重载
- 关系运算符重载
- 类型转换运算符
- true 和 false 运算符
- 可重载与不可重载的运算符
- 运算符重载规则速查表
- 实战案例:向量类
- 实战案例:金额类
- 实战案例:复数类
- 常见错误与注意事项
- 面试常考题
- 课后练习
- 小结
1. 什么是运算符重载
一个生活的比喻
想象你小时候玩积木。每个人都有相同的积木块,但不同的人可以搭出不同的东西——有人搭房子,有人搭汽车。
在 C# 里,+、-、*、== 这些运算符就像积木块。对于内置类型,它们的含义是固定的:
int a = 3 + 5; // + 表示"整数相加" = 8
string s = "Hello" + "World"; // + 表示"字符串拼接" = "HelloWorld"
但是,当你定义自己的类时,这些运算符不认识你的类:
Point p1 = new Point(3, 4);
Point p2 = new Point(1, 2);
Point p3 = p1 + p2; // ❌ 编译错误!Point 不认识 +
运算符重载就是教运算符认识你的类——告诉 C#:"当遇到 Point + Point 时,请按照我定义的规则来计算"。
一句话总结:运算符重载让你自定义的类型也能使用
+、-、==等运算符,就像int和string一样自然。
2. 为什么需要运算符重载
没有运算符重载的世界
public class Money
{
public decimal Amount { get; set; }
public Money Add(Money other)
{
return new Money { Amount = this.Amount + other.Amount };
}
public Money Subtract(Money other)
{
return new Money { Amount = this.Amount - other.Amount };
}
}
// 使用时很别扭
Money salary = new Money { Amount = 5000 };
Money bonus = new Money { Amount = 1000 };
Money total = salary.Add(bonus); // 读起来怪怪的
有运算符重载的世界
public class Money
{
public decimal Amount { get; set; }
public static Money operator +(Money a, Money b)
{
return new Money { Amount = a.Amount + b.Amount };
}
public static Money operator -(Money a, Money b)
{
return new Money { Amount = a.Amount - b.Amount };
}
}
// 使用时像内置类型一样自然
Money salary = new Money { Amount = 5000 };
Money bonus = new Money { Amount = 1000 };
Money total = salary + bonus; // 直接相加,直观!
Money left = salary - bonus; // 直接相减,清晰!
运算符重载让代码更接近人类的思考方式——"把工资和奖金加起来"比"调用工资的Add方法传入奖金"更自然。
3. 基本语法骨架
语法格式
public static 返回类型 operator 运算符(参数列表)
{
// 自定义的逻辑
return 结果;
}
每个部分解读
| 部分 | 说明 | 示例 |
|---|---|---|
public |
访问修饰符,必须是 public | public |
static |
固定写法,必须是 static | static |
| 返回类型 | 运算结果的类型 | Point、bool、int 等 |
operator |
关键字,告诉编译器"这是一个运算符重载" | 必须写 operator |
| 运算符 | 你要重载哪个符号 | +、-、==、< 等 |
| 参数列表 | 参与运算的操作数 | (Point a, Point b) |
可视化理解
你写的代码: 编译器转换后:
p1 + p2 ─────────────────▶ Point.operator+(p1, p2)
p1 - p2 ─────────────────▶ Point.operator-(p1, p2)
p1 == p2 ─────────────────▶ Point.operator==(p1, p2)
本质上,
p1 + p2就是调用了Point.operator+(p1, p2)这个静态方法。
4. 一步步写出你的第一个运算符重载
场景:做一个"点"(Point)类,支持加法
using System;
// 第1步:定义类
public class Point
{
public int X { get; set; }
public int Y { get; set; }
// 第2步:写构造函数,方便创建对象
public Point(int x, int y)
{
X = x;
Y = y;
}
// 第3步:重载 + 运算符
// 逻辑:两个点相加 = 两个坐标分别相加
public static Point operator +(Point p1, Point p2)
{
return new Point(p1.X + p2.X, p1.Y + p2.Y);
}
// 第4步:重载 ToString,方便打印
public override string ToString()
{
return $"({X}, {Y})";
}
}
// 第5步:在 Main 里使用
class Program
{
static void Main()
{
Point p1 = new Point(3, 4);
Point p2 = new Point(1, 2);
Point p3 = p1 + p2; // 相当于 operator+(p1, p2)
Console.WriteLine(p1); // 输出:(3, 4)
Console.WriteLine(p2); // 输出:(1, 2)
Console.WriteLine(p3); // 输出:(4, 6)
}
}
运行流程图
p1 = (3, 4) ──┐
├──▶ operator+(p1, p2) ──▶ new Point(3+1, 4+2) ──▶ (4, 6)
p2 = (1, 2) ──┘
5. 运算符重载的工作原理
背后本质:就是一个静态方法
// 你写的
public static Point operator +(Point p1, Point p2)
{
return new Point(p1.X + p2.X, p1.Y + p2.Y);
}
// 编译器给它取的名字(可以用反射看到): op_Addition
// 实际上你可以这样调用(但不推荐):
// Point p3 = Point.op_Addition(p1, p2);
为什么必须是 static?
因为运算符重载是属于类型本身的方法,不属于某个实例。它接收两个操作数作为参数,返回一个新结果。
❌ 错误理解: p1 调用了 + 方法,p2 是参数
✅ 正确理解: Point 类型提供了一个静态的 + 方法,p1 和 p2 都是参数
6. 一元运算符重载
一元运算符只操作一个操作数。
可重载的一元运算符
| 运算符 | 含义 | 签名 |
|---|---|---|
+ |
正号 | operator +(T x) |
- |
负号/取反 | operator -(T x) |
! |
逻辑非 | operator !(T x) |
~ |
按位取反 | operator ~(T x) |
++ |
自增 | operator ++(T x) |
-- |
自减 | operator --(T x) |
true |
判断为真 | operator true(T x) |
false |
判断为假 | operator false(T x) |
示例:给 Point 加上一元运算符
public class Point
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y) { X = x; Y = y; }
// ===== 一元运算符:取反 =====
// -(3, 4) → (-3, -4)
public static Point operator -(Point p)
{
return new Point(-p.X, -p.Y);
}
// ===== 一元运算符:正号(通常直接返回原对象)=====
// +(3, 4) → (3, 4)
public static Point operator +(Point p)
{
return p; // 原样返回
}
// ===== 自增:X 和 Y 各加 1 =====
// ++(3, 4) → (4, 5)
public static Point operator ++(Point p)
{
return new Point(p.X + 1, p.Y + 1);
}
// ===== 自减 =====
public static Point operator --(Point p)
{
return new Point(p.X - 1, p.Y - 1);
}
public override string ToString() => $"({X}, {Y})";
}
// 使用
Point p = new Point(3, 4);
Console.WriteLine(-p); // 输出:(-3, -4) ← 取反
Console.WriteLine(+p); // 输出:(3, 4) ← 正号
Console.WriteLine(p++); // 输出:(3, 4) ← 自增(先返回旧值再加)
Console.WriteLine(p); // 输出:(4, 5) ← 自增后的值
注意
++和--:编译器会自动处理"先加后用"和"先用后加"的语义。
7. 二元运算符重载
二元运算符操作两个操作数。
可重载的二元运算符
| 运算符 | 含义 | 签名 |
|---|---|---|
+ |
加法 | operator +(T a, T b) |
- |
减法 | operator -(T a, T b) |
* |
乘法 | operator *(T a, T b) |
/ |
除法 | operator /(T a, T b) |
% |
取模 | operator %(T a, T b) |
& |
按位与 | operator &(T a, T b) |
| |
按位或 | operator |(T a, T b) |
^ |
按位异或 | operator ^(T a, T b) |
<< |
左移 | operator <<(T a, int b) |
>> |
右移 | operator >>(T a, int b) |
示例:给 Point 加上完整四则运算
public class Point
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y) { X = x; Y = y; }
// ===== 加法 =====
public static Point operator +(Point a, Point b)
=> new Point(a.X + b.X, a.Y + b.Y);
// ===== 减法 =====
public static Point operator -(Point a, Point b)
=> new Point(a.X - b.X, a.Y - b.Y);
// ===== 乘法:点 × 标量 =====
// (3, 4) * 2 → (6, 8)
public static Point operator *(Point p, int scale)
=> new Point(p.X * scale, p.Y * scale);
// 支持反向:2 * (3, 4) → (6, 8)
public static Point operator *(int scale, Point p)
=> new Point(p.X * scale, p.Y * scale);
// ===== 除法 =====
public static Point operator /(Point p, int divisor)
=> new Point(p.X / divisor, p.Y / divisor);
public override string ToString() => $"({X}, {Y})";
}
// 使用
Point p1 = new Point(3, 4);
Point p2 = new Point(1, 2);
Console.WriteLine(p1 + p2); // 输出:(4, 6)
Console.WriteLine(p1 - p2); // 输出:(2, 2)
Console.WriteLine(p1 * 3); // 输出:(9, 12)
Console.WriteLine(2 * p1); // 输出:(6, 8) ← 反向乘法也能用!
Console.WriteLine(p1 / 2); // 输出:(1, 2)
注意:如果需要
2 * p1这样的反向运算,必须单独写一个参数顺序相反的版本。
8. 比较运算符重载
比较运算符用于判断两个对象是否相等或不等。
重要规则(必须遵守)
==和!=必须成对重载- 重载
==和!=时,必须同时重写Equals()和GetHashCode()
public class Point
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y) { X = x; Y = y; }
// ===== 重载 == =====
public static bool operator ==(Point a, Point b)
{
// 先处理 null 的情况
if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
return true;
if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
return false;
return a.X == b.X && a.Y == b.Y;
}
// ===== 重载 != =====
// 必须成对!直接取反 == 的结果
public static bool operator !=(Point a, Point b)
{
return !(a == b);
}
// ===== 必须重写 Equals =====
public override bool Equals(object obj)
{
if (obj is Point other)
return this == other; // 委托给 == 运算符
return false;
}
// ===== 必须重写 GetHashCode =====
public override int GetHashCode()
{
return HashCode.Combine(X, Y);
}
public override string ToString() => $"({X}, {Y})";
}
// 使用
Point p1 = new Point(3, 4);
Point p2 = new Point(3, 4);
Point p3 = new Point(1, 2);
Console.WriteLine(p1 == p2); // 输出:True (坐标相同)
Console.WriteLine(p1 != p3); // 输出:True (坐标不同)
// 注意:如果没有重载 ==,默认比较的是引用地址!
// 两个内容相同的 Point 会被判定为 "不相等"
为什么必须重写 GetHashCode?
当一个对象被放到 Dictionary 或 HashSet 中时,系统会用 GetHashCode() 快速定位。如果两个相等的对象返回不同的哈希码,会导致字典行为异常。
// ❌ 没重写 GetHashCode 的后果
Dictionary<Point, string> dict = new Dictionary<Point, string>();
dict[new Point(3, 4)] = "A";
// 虽然 (3,4) == (3,4) 返回 True
// 但如果 GetHashCode 返回不同值,dict[new Point(3, 4)] 可能会抛异常!
9. 关系运算符重载
关系运算符用于判断大小顺序。
重要规则
<和>必须成对重载<=和>=必须成对重载- 通常会实现
IComparable<T>接口
public class Student : IComparable<Student>
{
public string Name { get; set; }
public int Score { get; set; }
public Student(string name, int score)
{
Name = name;
Score = score;
}
// ===== 重载 < =====
public static bool operator <(Student a, Student b)
{
return a.Score < b.Score;
}
// ===== 重载 > =====
// 必须成对!
public static bool operator >(Student a, Student b)
{
return a.Score > b.Score;
}
// ===== 重载 <= =====
public static bool operator <=(Student a, Student b)
{
return a.Score <= b.Score;
}
// ===== 重载 >= =====
// 必须成对!
public static bool operator >=(Student a, Student b)
{
return a.Score >= b.Score;
}
// ===== 实现 IComparable<Student>(推荐)=====
public int CompareTo(Student other)
{
return Score.CompareTo(other.Score);
}
public override string ToString() => $"{Name}({Score}分)";
}
// 使用
Student s1 = new Student("张三", 90);
Student s2 = new Student("李四", 85);
Console.WriteLine(s1 > s2); // 输出:True
Console.WriteLine(s1 < s2); // 输出:False
Console.WriteLine(s1 >= s2); // 输出:True
Console.WriteLine(s1 <= s2); // 输出:False
常用模式:在类内部让一个"核心字段"(如 Score)来决定大小关系。
10. 类型转换运算符
类型转换运算符让你的类可以和别的类型互相转换。
隐式转换(implicit)
自动转换,不会丢失数据,不需要显式写转换代码:
public class Celsius
{
public double Value { get; set; }
public Celsius(double value) { Value = value; }
// implicit:Celsius 可以自动转为 double 和 Fahrenheit
public static implicit operator double(Celsius c) => c.Value;
// 也支持从基础类型隐式转换
public static implicit operator Celsius(double d) => new Celsius(d);
}
// 使用:完全自动,不需要 (Celsius) 或 (double)
Celsius temp = new Celsius(25); // 正常构造
double d = temp; // 自动转 double → 25.0
Celsius c = 36.5; // 自动转 Celsius
// 甚至可以写这种代码
Celsius sum = temp + 10; // 10 自动转为 Celsius(10),然后相加(需重载 +)
显式转换(explicit)
手动转换,可能丢失数据,需要写 (TargetType) 转换:
public class Fahrenheit
{
public double Value { get; set; }
public Fahrenheit(double value) { Value = value; }
// explicit:必须显式转换(因为可能损失精度)
public static explicit operator Fahrenheit(Celsius c)
{
return new Fahrenheit(c.Value * 9 / 5 + 32);
}
public static explicit operator Celsius(Fahrenheit f)
{
return new Celsius((f.Value - 32) * 5 / 9);
}
}
// 使用:必须显式写 (Fahrenheit)
Celsius c = new Celsius(100);
Fahrenheit f = (Fahrenheit)c; // 必须写 (Fahrenheit)
Console.WriteLine(f.Value); // 输出:212
// Fahrenheit f2 = c; // ❌ 编译错误!没有隐式转换
implicit vs explicit 选择指南
| 场景 | 使用 |
|---|---|
| 转换绝对安全,不丢失数据 | implicit |
| 可能丢失数据、精度或抛出异常 | explicit |
| 不相干的类型之间 | 不要写转换 |
11. true 和 false 运算符
当一个对象可以被判定为"真"或"假"时,可以重载 true 和 false 运算符。
使用场景
想象一个用户类,判断"用户是否有效":
public class User
{
public string Name { get; set; }
public bool IsActive { get; set; }
public User(string name, bool isActive)
{
Name = name;
IsActive = isActive;
}
// ===== 重载 true:什么情况下这个对象算"真" =====
public static bool operator true(User user)
{
return user != null && user.IsActive;
}
// ===== 重载 false:什么情况下这个对象算"假" =====
// true 和 false 必须成对出现!
public static bool operator false(User user)
{
return user == null || !user.IsActive;
}
}
// 使用
User u1 = new User("张三", true);
User u2 = new User("李四", false);
// 可以直接在 if 里判断
if (u1)
Console.WriteLine($"{u1.Name} 是活跃用户");
else
Console.WriteLine($"{u1.Name} 不是活跃用户");
// 输出:张三 是活跃用户
if (u2)
Console.WriteLine("不会输出"); // u2 为 false
else
Console.WriteLine($"{u2.Name} 不是活跃用户");
// 输出:李四 不是活跃用户
重载了
true/false后,对象可以直接用在if(x)、while(x)等需要 bool 的上下文里。
12. 可重载与不可重载的运算符
可重载的完整清单
| 类别 | 运算符 |
|---|---|
| 一元 | +, -, !, ~, ++, --, true, false |
| 二元 | +, -, *, /, %, &, |, ^, <<, >> |
| 比较 | ==, !=, <, >, <=, >= |
| 转换 | implicit, explicit |
不可重载的运算符(记住这些!)
| 运算符 | 原因 |
|---|---|
&& || |
短路逻辑,有特殊求值顺序,无法重载 |
= |
赋值不是运算符,是语言语法 |
. |
成员访问 |
?: |
三元条件运算符,控制流而非计算 |
new |
创建对象的关键字 |
typeof |
获取类型的运行时信息 |
sizeof |
获取类型大小 |
is as |
类型检查关键字 |
=> |
Lambda 表达式 |
?? |
null 合并运算符 |
?. ?[] |
null 条件运算符 |
checked/unchecked |
溢出检查上下文 |
default |
默认值表达式 |
nameof |
获取名称 |
记忆口诀:方法调用式的关键字(
new、typeof、sizeof、is、as、nameof、default)都不行。短路逻辑(&&、||)和三元(?:)也不行。
13. 运算符重载规则速查表
| 规则 | 说明 |
|---|---|
必须是 public static |
关键字不可改变 |
| 必须是类的成员 | 不能在全局定义 |
| 返回类型可以是任意类型 | 不一定要和本类相同 |
| 参数中至少有一个是本类类型 | operator +(Point, int) 可以,但不能两个都是 int |
== 和 != 必须成对 |
缺一个编译错误 |
< 和 > 必须成对 |
缺一个编译错误 |
<= 和 >= 必须成对 |
缺一个编译错误 |
true 和 false 必须成对 |
缺一个编译错误 |
不能有 ref/out/in 参数 |
参数只能按值传递 |
!= 通常委托给 == |
return !(a == b); |
> 通常委托给 < |
return b < a; |
>= 通常委托给 < |
return !(a < b); |
<= 通常委托给 < 或 > |
return a < b || a == b; |
重载 == 必须重写 Equals 和 GetHashCode |
否则编译器会警告 |
14. 实战案例:向量类
数学中的向量(Vector)天然适合用运算符重载:
using System;
public class Vector
{
public double X { get; }
public double Y { get; }
public Vector(double x, double y)
{
X = x;
Y = y;
}
// ========== 加法:向量加向量 ==========
public static Vector operator +(Vector a, Vector b)
=> new Vector(a.X + b.X, a.Y + b.Y);
// ========== 减法:向量减向量 ==========
public static Vector operator -(Vector a, Vector b)
=> new Vector(a.X - b.X, a.Y - b.Y);
// ========== 取反 ==========
public static Vector operator -(Vector v)
=> new Vector(-v.X, -v.Y);
// ========== 数乘:向量 × 标量 ==========
public static Vector operator *(Vector v, double scalar)
=> new Vector(v.X * scalar, v.Y * scalar);
public static Vector operator *(double scalar, Vector v)
=> new Vector(v.X * scalar, v.Y * scalar);
// ========== 数除:向量 ÷ 标量 ==========
public static Vector operator /(Vector v, double scalar)
=> new Vector(v.X / scalar, v.Y / scalar);
// ========== 点积(Dot Product)==========
// 向量1 · 向量2 = X1*X2 + Y1*Y2,结果是一个标量
public static double operator *(Vector a, Vector b)
=> a.X * b.X + a.Y * b.Y;
// ========== 比较 ==========
public static bool operator ==(Vector a, Vector b)
{
if (ReferenceEquals(a, null) && ReferenceEquals(b, null)) return true;
if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) return false;
return a.X == b.X && a.Y == b.Y;
}
public static bool operator !=(Vector a, Vector b) => !(a == b);
public override bool Equals(object obj)
=> obj is Vector other && this == other;
public override int GetHashCode()
=> HashCode.Combine(X, Y);
// ========== 辅助属性 ==========
// 向量长度
public double Magnitude => Math.Sqrt(X * X + Y * Y);
// 单位向量(方向相同,长度=1)
public Vector Normalized => this / Magnitude;
public override string ToString()
=> $"({X:F2}, {Y:F2}) [长度:{Magnitude:F2}]";
}
// ==================== 使用演示 ====================
class Program
{
static void Main()
{
Vector v1 = new Vector(3, 4); // 长度 = 5
Vector v2 = new Vector(1, 2);
// 向量加减
Vector sum = v1 + v2;
Console.WriteLine($"v1 + v2 = {sum}"); // (4.00, 6.00)
Vector diff = v1 - v2;
Console.WriteLine($"v1 - v2 = {diff}"); // (2.00, 2.00)
// 数乘
Vector scaled = v1 * 3;
Console.WriteLine($"v1 * 3 = {scaled}"); // (9.00, 12.00)
// 点积
double dot = v1 * v2;
Console.WriteLine($"v1 · v2 = {dot}"); // 3*1 + 4*2 = 11
// 单位向量
Console.WriteLine($"v1 的单位向量 = {v1.Normalized}"); // (0.60, 0.80)
}
}
这个例子展示了同一符号
*在不同上下文中的不同含义:Vector * double是数乘(返回向量),Vector * Vector是点积(返回标量)。这就是运算符重载的强大之处!
15. 实战案例:金额类
处理金钱时,运算符重载非常有用:
using System;
public class Money
{
// 用 decimal 存储,避免浮点精度问题
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency = "CNY")
{
Amount = amount;
Currency = currency;
}
// ========== 加法 ==========
public static Money operator +(Money a, Money b)
{
// 安全检查:不同币种不能直接相加
if (a.Currency != b.Currency)
throw new InvalidOperationException(
$"不能将 {a.Currency} 和 {b.Currency} 直接相加!");
return new Money(a.Amount + b.Amount, a.Currency);
}
// ========== 减法 ==========
public static Money operator -(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException(
$"不能将 {a.Currency} 和 {b.Currency} 直接相减!");
return new Money(a.Amount - b.Amount, a.Currency);
}
// ========== 数乘:金额 × 数量 ==========
public static Money operator *(Money m, int quantity)
=> new Money(m.Amount * quantity, m.Currency);
public static Money operator *(int quantity, Money m)
=> new Money(m.Amount * quantity, m.Currency);
// ========== 比较 ==========
public static bool operator >(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException("不同币种不能直接比较!");
return a.Amount > b.Amount;
}
public static bool operator <(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException("不同币种不能直接比较!");
return a.Amount < b.Amount;
}
public static bool operator >=(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException("不同币种不能直接比较!");
return a.Amount >= b.Amount;
}
public static bool operator <=(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException("不同币种不能直接比较!");
return a.Amount <= b.Amount;
}
public static bool operator ==(Money a, Money b)
{
if (ReferenceEquals(a, null) && ReferenceEquals(b, null)) return true;
if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) return false;
return a.Currency == b.Currency && a.Amount == b.Amount;
}
public static bool operator !=(Money a, Money b) => !(a == b);
public override bool Equals(object obj)
=> obj is Money other && this == other;
public override int GetHashCode()
=> HashCode.Combine(Amount, Currency);
// ========== 类型转换 ==========
// 可以从 decimal 隐式创建 Money(很方便!)
public static implicit operator Money(decimal amount)
=> new Money(amount);
// 可以显式转为 decimal
public static explicit operator decimal(Money m) => m.Amount;
public override string ToString()
=> $"{Currency} {Amount:N2}";
}
// ==================== 使用演示 ====================
class Program
{
static void Main()
{
Money salary = new Money(5000m);
Money bonus = 1000m; // 隐式转换:1000m → Money
Money total = salary + bonus;
Console.WriteLine($"工资+奖金 = {total}"); // CNY 6,000.00
Money left = salary - 2000m; // 2000m 隐式转为 Money
Console.WriteLine($"剩余 = {left}"); // CNY 3,000.00
Money price = 99.99m;
Money totalCost = price * 3;
Console.WriteLine($"3个 = {totalCost}"); // CNY 299.97
Console.WriteLine(salary > bonus); // 输出:True
// 不同币种会报错
Money usd = new Money(100, "USD");
// Money sum = salary + usd; // ❌ 运行时报错!
}
}
16. 实战案例:复数类
复数(Complex Number)是运算符重载的经典教学案例:
using System;
public class Complex
{
public double Real { get; } // 实部
public double Imaginary { get; } // 虚部
public Complex(double real, double imaginary)
{
Real = real;
Imaginary = imaginary;
}
// ========== 加法:(a+bi) + (c+di) = (a+c) + (b+d)i ==========
public static Complex operator +(Complex a, Complex b)
=> new Complex(a.Real + b.Real, a.Imaginary + b.Imaginary);
// ========== 减法 ==========
public static Complex operator -(Complex a, Complex b)
=> new Complex(a.Real - b.Real, a.Imaginary - b.Imaginary);
// ========== 乘法:(a+bi) * (c+di) = (ac-bd) + (ad+bc)i ==========
public static Complex operator *(Complex a, Complex b)
=> new Complex(
a.Real * b.Real - a.Imaginary * b.Imaginary,
a.Real * b.Imaginary + a.Imaginary * b.Real
);
// ========== 取反 ==========
public static Complex operator -(Complex c)
=> new Complex(-c.Real, -c.Imaginary);
// ========== 比较 ==========
public static bool operator ==(Complex a, Complex b)
{
if (ReferenceEquals(a, null) && ReferenceEquals(b, null)) return true;
if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) return false;
return a.Real == b.Real && a.Imaginary == b.Imaginary;
}
public static bool operator !=(Complex a, Complex b) => !(a == b);
public override bool Equals(object obj)
=> obj is Complex other && this == other;
public override int GetHashCode()
=> HashCode.Combine(Real, Imaginary);
// ========== 隐式转换:double → Complex ==========
public static implicit operator Complex(double real)
=> new Complex(real, 0);
// ========== 辅助属性:模(绝对值)==========
public double Magnitude => Math.Sqrt(Real * Real + Imaginary * Imaginary);
public override string ToString()
{
if (Imaginary >= 0)
return $"{Real} + {Imaginary}i";
else
return $"{Real} - {-Imaginary}i";
}
}
// ==================== 使用演示 ====================
class Program
{
static void Main()
{
Complex a = new Complex(3, 4); // 3 + 4i
Complex b = new Complex(1, -2); // 1 - 2i
Console.WriteLine($"a = {a}");
Console.WriteLine($"b = {b}");
Console.WriteLine($"a + b = {a + b}"); // 4 + 2i
Console.WriteLine($"a - b = {a - b}"); // 2 + 6i
Console.WriteLine($"a * b = {a * b}"); // 11 - 2i
Console.WriteLine($"+2 = {2 + a}"); // 5 + 4i (2 隐式转为 Complex)
Console.WriteLine($"|a| = {a.Magnitude:F2}"); // 5.00
}
}
17. 常见错误与注意事项
错误1:忘记 static 关键字
// ❌ 编译错误
public Point operator +(Point p1, Point p2) // 缺少 static
{
return new Point(p1.X + p2.X, p1.Y + p2.Y);
}
// ✅ 正确
public static Point operator +(Point p1, Point p2)
{
return new Point(p1.X + p2.X, p1.Y + p2.Y);
}
错误2:== 和 != 没成对
public class Point
{
// ❌ 只写了 ==,忘了 != → 编译错误!
public static bool operator ==(Point a, Point b) { ... }
// 缺少 operator !=
}
// ✅ 必须都写
public static bool operator !=(Point a, Point b) => !(a == b);
错误3:重载 == 但没重写 Equals 和 GetHashCode
// ⚠️ 编译器会警告!
// 重载 == 必须同时重写以下两个方法:
public override bool Equals(object obj) => obj is Point p && this == p;
public override int GetHashCode() => HashCode.Combine(X, Y);
错误4:没有处理 null
// ❌ 当 a 或 b 为 null 时,a.X 会抛 NullReferenceException!
public static bool operator ==(Point a, Point b)
{
return a.X == b.X && a.Y == b.Y;
}
// ✅ 先检查 null
public static bool operator ==(Point a, Point b)
{
if (ReferenceEquals(a, null) && ReferenceEquals(b, null)) return true;
if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) return false;
return a.X == b.X && a.Y == b.Y;
}
错误5:链式赋值问题
// ⚠️ 不推荐:重载了 + 但返回 void 或用 ref 参数
// 因为 a + b + c 期望的是 (a.operator+(b)).operator+(c)
错误6:过度使用
// ❌ 滥用:把无关的类型强行转换
public class Person
{
public string Name { get; set; }
// 把 "人" 转成 "int"?毫无意义!
public static implicit operator int(Person p) => p.Name.Length;
}
// ✅ 只在有清晰数学或逻辑关系时使用
原则:运算符重载应该让代码更直观。如果
a + b的含义不明确,不要重载。
18. 面试常考题
Q1:运算符重载的签名为什么是 static?
答:因为运算符操作的是两个(或一个)操作数,不属于任何一个实例。它类似于 static int Add(int a, int b) 这种工具方法,只是用 operator 关键字和符号代替了方法名。
Q2:哪些运算符必须成对重载?
答:
==和!=<和><=和>=true和false
Q3:implicit 和 explicit 有什么区别?
答:
| 特性 | implicit | explicit |
|---|---|---|
| 转换方式 | 自动发生 | 需要显式转换 (Type)x |
| 使用场景 | 绝对安全,不失数据 | 可能损失数据或抛异常 |
| 示例 | int → double |
double → int(截断) |
Q4:运算符重载和扩展方法有什么区别?
答:扩展方法是给已有类"追加"实例方法,不能追加运算符。运算符重载只能在定义类时写在类内部,不能用扩展方法的方式给别人的类添加运算符。
Q5:重载 == 时为什么必须重写 Equals 和 GetHashCode?
答:
Equals因为object.Equals()的默认行为是比较引用,重载==后语义不一致。GetHashCode因为字典/HashSet 依赖哈希码,相等的对象必须返回相同哈希码,否则容器行为异常。
19. 课后练习
练习1:分数类
做一个 Fraction 类,支持四则运算。
// 要求:
// 1. 属性:分子(Numerator) / 分母(Denominator)
// 2. 重载 +, -, *, /
// 3. 分数结果要自动约分
// 4. 重载 == 和 !=
// 期望效果:
// Fraction f1 = new Fraction(1, 2); // 1/2
// Fraction f2 = new Fraction(1, 3); // 1/3
// Fraction sum = f1 + f2; // 5/6
// Fraction diff = f1 - f2; // 1/6
点击查看参考代码
public class Fraction
{
public int Numerator { get; }
public int Denominator { get; }
public Fraction(int numerator, int denominator)
{
if (denominator == 0) throw new ArgumentException("分母不能为零");
// 约分
int gcd = GCD(Math.Abs(numerator), Math.Abs(denominator));
Numerator = numerator / gcd;
Denominator = denominator / gcd;
}
private static int GCD(int a, int b)
{
while (b != 0) { int t = b; b = a % b; a = t; }
return a;
}
public static Fraction operator +(Fraction a, Fraction b)
=> new Fraction(a.Numerator * b.Denominator + b.Numerator * a.Denominator,
a.Denominator * b.Denominator);
public static Fraction operator -(Fraction a, Fraction b)
=> new Fraction(a.Numerator * b.Denominator - b.Numerator * a.Denominator,
a.Denominator * b.Denominator);
public static Fraction operator *(Fraction a, Fraction b)
=> new Fraction(a.Numerator * b.Numerator,
a.Denominator * b.Denominator);
public static Fraction operator /(Fraction a, Fraction b)
=> new Fraction(a.Numerator * b.Denominator,
a.Denominator * b.Numerator);
public static bool operator ==(Fraction a, Fraction b)
=> a.Numerator == b.Numerator && a.Denominator == b.Denominator;
public static bool operator !=(Fraction a, Fraction b) => !(a == b);
public override bool Equals(object obj) => obj is Fraction f && this == f;
public override int GetHashCode() => HashCode.Combine(Numerator, Denominator);
public override string ToString() => $"{Numerator}/{Denominator}";
}
练习2:距离类
做一个 Distance 类,支持不同单位的距离相加。
// 要求:
// 1. 属性:数值 + 单位(米/公里)
// 2. 重载 +(不同单位能自动转换)
// 3. 重载 > 和 <
// 4. 隐式转换:公里 → 米
// 期望效果:
// Distance d1 = new Distance(500, "m"); // 500米
// Distance d2 = new Distance(1, "km"); // 1公里 = 1000米
// Distance sum = d1 + d2; // 1500米
// Console.WriteLine(sum); // 输出:1500m
练习3:游戏中的三维坐标
做一个 Vector3D 类,支持三维空间运算。
// 要求:
// 1. 属性:X, Y, Z
// 2. 重载 +, -, *(数乘), *(点积,返回标量)
// 3. 重载 ==, !=
// 4. 计算模长(Magnitude)和单位向量(Normalized)
20. 小结
┌───────────────────────────────────────────────────────────┐
│ 运算符重载核心要点 │
├───────────────────────────────────────────────────────────┤
│ 语法: public static 返回类型 operator 符号(参数) { } │
│ │
│ 本质: 静态方法,编译后变成 op_Addition 等方法 │
│ │
│ 规则: 必须是 public static,参数至少一个是本类型 │
│ │
│ 必须成对: ==/!= , </> , <=/>= , true/false │
│ │
│ 重载==后: 必须重写 Equals() 和 GetHashCode() │
│ │
│ 不可重载: &&, ||, =, ., ?:, new, typeof, is, as │
│ │
│ 适用场景: 数值类型、数学对象、比较逻辑明确的类 │
│ │
│ 原则: 让代码更自然、更直观,不滥用 │
└───────────────────────────────────────────────────────────┘
一句话总结
运算符重载就是教 C# 的
+、-、==等符号认识你的自定义类,让你能用a + b代替a.Add(b),让代码更接近自然语言。本质是静态方法,核心是直观。
本文档专为教学编写,重在通俗易懂。建议每学完一个概念就动手敲一遍代码,加深理解。
评论区