C# 运算符重载详解
一、什么是运算符重载?
1.1 生活比喻
运算符重载就像给运算符赋予"双重身份":
+号平时做加法:3 + 5 = 8。但如果两个字符串用+:"Hello" + "World" = "HelloWorld"——同一个+号,面对不同类型的数据,做了不同的事。
这就是运算符重载——让自定义类型也能用 +、-、== 这些运算符,就像内置类型一样自然。
1.2 为什么需要运算符重载?
// 没有运算符重载——只能用方法
public class Money
{
public decimal Amount { get; set; }
}
Money a = new Money { Amount = 100 };
Money b = new Money { Amount = 50 };
// Money c = a + b; // ❌ 编译错误!Money 不能直接用 + 号
// 只能用方法
Money c = a.Add(b); // 不够自然
// 有了运算符重载——直接写 a + b
Money c = a + b; // ✅ 和内置类型一样自然!
1.3 一句话理解
运算符重载 = 给自定义类型定义运算符行为,让你的类型可以像 int、string 一样使用
+、-、==、>等运算符。
1.4 你其实早就用过了
string s1 = "Hello";
string s2 = "World";
string s3 = s1 + s2; // string 重载了 + 运算符
DateTime d1 = DateTime.Now;
DateTime d2 = d1.AddDays(1);
bool later = d2 > d1; // DateTime 重载了 > 运算符
TimeSpan t1 = TimeSpan.FromHours(1);
TimeSpan t2 = TimeSpan.FromHours(2);
TimeSpan t3 = t1 + t2; // TimeSpan 重载了 + 运算符
二、哪些运算符可以重载?
2.1 一元运算符(一个操作数)
| 运算符 | 说明 | 重载方法签名 |
|---|---|---|
+ |
正号 | public static T operator +(T x) |
- |
负号 | public static T operator -(T x) |
! |
逻辑非 | public static bool operator !(T x) |
~ |
按位取反 | public static T operator ~(T x) |
++ |
自增 | public static T operator ++(T x) |
-- |
自减 | public static T operator --(T x) |
true |
判断是否为 true | public static bool operator true(T x) |
false |
判断是否为 false | public static bool operator false(T x) |
2.2 二元运算符(两个操作数)
| 运算符 | 说明 | 重载方法签名 |
|---|---|---|
+ |
加 | public static T operator +(T a, T b) |
- |
减 | public static T operator -(T a, T b) |
* |
乘 | public static T operator *(T a, T b) |
/ |
除 | public static T operator /(T a, T b) |
% |
取模 | public static T operator %(T a, T b) |
== |
等于 | public static bool operator ==(T a, T b) |
!= |
不等于 | public static bool operator !=(T a, T b) |
< |
小于 | public static bool operator <(T a, T b) |
> |
大于 | public static bool operator >(T a, T b) |
<= |
小于等于 | public static bool operator <=(T a, T b) |
>= |
大于等于 | public static bool operator >=(T a, T b) |
& |
按位与 | public static T operator &(T a, T b) |
| |
按位或 | public static T operator |(T a, T b) |
^ |
按位异或 | public static T operator ^(T a, T b) |
2.3 不能重载的运算符
| 运算符 | 原因 |
|---|---|
= |
赋值运算符不能重载 |
. |
成员访问不能重载 |
?: |
三元条件不能重载 |
?? |
null 合并不能重载 |
-> |
指针访问不能重载 |
=> |
Lambda 不能重载 |
new |
new 不能重载 |
typeof |
typeof 不能重载 |
sizeof |
sizeof 不能重载 |
is / as |
类型检查不能重载 |
三、基本语法
3.1 语法格式
public static 返回类型 operator 运算符(参数列表)
{
// 实现逻辑
}
核心规则:
- 必须是
public static(公开的静态方法) - 方法名是
operator关键字,后面跟运算符符号 - 至少有一个参数是本类型
- 不能用
ref、out参数
3.2 第一个例子——重载 + 运算符
public class Point
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y)
{
X = x;
Y = y;
}
// 重载 + 运算符:两个 Point 相加
public static Point operator +(Point p1, Point p2)
{
return new Point(p1.X + p2.X, p1.Y + p2.Y);
}
public override string ToString() => $"({X}, {Y})";
}
// 使用
Point p1 = new Point(3, 5);
Point p2 = new Point(1, 2);
Point p3 = p1 + p2; // (4, 7),像数学坐标一样自然!
Console.WriteLine($"{p1} + {p2} = {p3}");
输出:
(3, 5) + (1, 2) = (4, 7)
四、常用运算符重载示例
4.1 算术运算符(+、-、*、/)
public class Vector2D
{
public double X { get; set; }
public double Y { get; set; }
public Vector2D(double x, double y) { X = x; Y = y; }
// 向量加法
public static Vector2D operator +(Vector2D a, Vector2D b)
=> new Vector2D(a.X + b.X, a.Y + b.Y);
// 向量减法
public static Vector2D operator -(Vector2D a, Vector2D b)
=> new Vector2D(a.X - b.X, a.Y - b.Y);
// 标量乘法(向量 × 数字)
public static Vector2D operator *(Vector2D v, double scalar)
=> new Vector2D(v.X * scalar, v.Y * scalar);
// 标量乘法(数字 × 向量)——交换律的需要
public static Vector2D operator *(double scalar, Vector2D v)
=> new Vector2D(v.X * scalar, v.Y * scalar);
// 取负
public static Vector2D operator -(Vector2D v)
=> new Vector2D(-v.X, -v.Y);
public override string ToString() => $"({X}, {Y})";
}
// 使用
Vector2D v1 = new Vector2D(3, 4);
Vector2D v2 = new Vector2D(1, 2);
Console.WriteLine($"v1 + v2 = {v1 + v2}"); // (4, 6)
Console.WriteLine($"v1 - v2 = {v1 - v2}"); // (2, 2)
Console.WriteLine($"v1 * 2 = {v1 * 2}"); // (6, 8)
Console.WriteLine($"3 * v2 = {3 * v2}"); // (3, 6)
Console.WriteLine($"-v1 = {-v1}"); // (-3, -4)
4.2 比较运算符(==、!=、<、>、<=、>=)
public class StudentScore : IComparable<StudentScore>
{
public string Name { get; set; }
public int Score { get; set; }
public StudentScore(string name, int score)
{
Name = name;
Score = score;
}
// ===== 比较运算符(必须成对出现!) =====
// == 和 != 必须成对
public static bool operator ==(StudentScore a, StudentScore b)
{
if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
return true;
if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
return false;
return a.Score == b.Score;
}
public static bool operator !=(StudentScore a, StudentScore b)
=> !(a == b);
// < 和 > 必须成对
public static bool operator <(StudentScore a, StudentScore b)
=> a.Score < b.Score;
public static bool operator >(StudentScore a, StudentScore b)
=> a.Score > b.Score;
// <= 和 >= 必须成对
public static bool operator <=(StudentScore a, StudentScore b)
=> a.Score <= b.Score;
public static bool operator >=(StudentScore a, StudentScore b)
=> a.Score >= b.Score;
// ===== 重载 == 和 != 时必须重写 Equals 和 GetHashCode =====
public override bool Equals(object obj)
{
if (obj is StudentScore other)
return this == other;
return false;
}
public override int GetHashCode() => Score.GetHashCode();
public override string ToString() => $"{Name}: {Score}分";
}
// 使用
StudentScore s1 = new StudentScore("张三", 92);
StudentScore s2 = new StudentScore("李四", 85);
StudentScore s3 = new StudentScore("王五", 92);
Console.WriteLine($"s1 == s2: {s1 == s2}"); // False
Console.WriteLine($"s1 == s3: {s1 == s3}"); // True(同分)
Console.WriteLine($"s1 > s2: {s1 > s2}"); // True
Console.WriteLine($"s2 < s1: {s2 < s1}"); // True
Console.WriteLine($"s1 >= s2: {s1 >= s2}"); // True
4.3 自增/自减运算符(++、--)
public class Counter
{
public int Value { get; private set; }
public Counter(int value) => Value = value;
public static Counter operator ++(Counter c)
{
c.Value++;
return c;
}
public static Counter operator --(Counter c)
{
c.Value--;
return c;
}
public override string ToString() => $"Count: {Value}";
}
// 使用
Counter c = new Counter(5);
c++; // Value 变成 6
Console.WriteLine(c); // Count: 6
Counter result = ++c; // Value 变成 7,result 指向同一个对象
Console.WriteLine(c); // Count: 7
Console.WriteLine(result); // Count: 7(和 c 是同一个对象!)
注意:对于引用类型,
++和--通常直接修改原对象并返回它。
4.4 true / false 运算符
public class User
{
public string Name { get; set; }
public bool IsActive { get; set; }
public bool HasPermission { get; set; }
public User(string name, bool active, bool permission)
{
Name = name;
IsActive = active;
HasPermission = permission;
}
// true/false 必须成对重载
public static bool operator true(User user)
=> user.IsActive && user.HasPermission;
public static bool operator false(User user)
=> !(user.IsActive && user.HasPermission);
public override string ToString() => Name;
}
// 使用——可以像 bool 一样用在条件判断中!
User admin = new User("管理员", true, true);
User guest = new User("游客", true, false);
if (admin)
Console.WriteLine($"{admin} 可以访问"); // ✅ 管理员 可以访问
if (guest)
Console.WriteLine($"{guest} 可以访问"); // 不会执行
else
Console.WriteLine($"{guest} 没有权限"); // ✅ 游客 没有权限
// 甚至可以写在短路运算中
Console.WriteLine(admin ? "允许" : "拒绝"); // 允许
五、转换运算符 —— implicit 和 explicit
5.1 隐式转换(implicit)——自动转,不丢失数据
public class Celsius
{
public double Degrees { get; }
public Celsius(double degrees) => Degrees = degrees;
// 隐式转换:Celsius → double(自动,安全)
public static implicit operator double(Celsius c) => c.Degrees;
// 隐式转换:double → Celsius(自动,安全)
public static implicit operator Celsius(double d) => new Celsius(d);
public override string ToString() => $"{Degrees}°C";
}
// 使用——自动转换,不需要任何标记
Celsius temp = 36.5; // double 自动转 Celsius
double degrees = temp; // Celsius 自动转 double
Console.WriteLine(temp); // 36.5°C
Console.WriteLine(degrees); // 36.5
5.2 显式转换(explicit)——可能丢数据,必须显式写
public class Money
{
public decimal Amount { get; }
public Money(decimal amount) => Amount = amount;
// 显式转换:Money → double(可能丢失精度)
public static explicit operator double(Money m) => (double)m.Amount;
// 显式转换:Money → int(丢失小数部分)
public static explicit operator int(Money m) => (int)m.Amount;
public override string ToString() => $"¥{Amount:F2}";
}
// 使用——必须显式强转
Money money = new Money(99.99m);
double d = (double)money; // 必须写 (double)
int i = (int)money; // 必须写 (int),小数部分丢失
Console.WriteLine($"Money: {money}"); // ¥99.99
Console.WriteLine($"double: {d}"); // 99.99
Console.WriteLine($"int: {i}"); // 99
5.3 implicit vs explicit 选择表
| 转换类型 | 适用场景 | 示例 |
|---|---|---|
implicit |
转换是安全的,不会丢数据 | Celsius → double、int → long |
explicit |
可能丢失数据,需要提醒调用者 | Money → int(丢小数)、double → int |
六、完整的实战示例——银行账户系统
using System;
public class BankAccount : IComparable<BankAccount>
{
public string Owner { get; }
public decimal Balance { get; private set; }
public BankAccount(string owner, decimal balance)
{
Owner = owner;
Balance = balance;
}
// ===== 算术运算符 =====
// 存款:账户 + 金额 = 新余额的账户
public static BankAccount operator +(BankAccount account, decimal amount)
{
return new BankAccount(account.Owner, account.Balance + amount);
}
// 取款:账户 - 金额
public static BankAccount operator -(BankAccount account, decimal amount)
{
if (amount > account.Balance)
throw new InvalidOperationException("余额不足!");
return new BankAccount(account.Owner, account.Balance - amount);
}
// ===== 比较运算符 =====
public static bool operator >(BankAccount a, BankAccount b)
=> a.Balance > b.Balance;
public static bool operator <(BankAccount a, BankAccount b)
=> a.Balance < b.Balance;
public static bool operator >=(BankAccount a, BankAccount b)
=> a.Balance >= b.Balance;
public static bool operator <=(BankAccount a, BankAccount b)
=> a.Balance <= b.Balance;
public static bool operator ==(BankAccount a, BankAccount b)
{
if (ReferenceEquals(a, null) && ReferenceEquals(b, null)) return true;
if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) return false;
return a.Owner == b.Owner && a.Balance == b.Balance;
}
public static bool operator !=(BankAccount a, BankAccount b)
=> !(a == b);
// ===== 自增/自减 =====
public static BankAccount operator ++(BankAccount account)
{
account.Balance += 1; // 加 1 元利息
return account;
}
public static BankAccount operator --(BankAccount account)
{
account.Balance -= 1; // 扣 1 元手续费
return account;
}
// ===== 转换运算符 =====
public static implicit operator decimal(BankAccount account)
=> account.Balance;
public static explicit operator double(BankAccount account)
=> (double)account.Balance;
// ===== 必需的重写 =====
public override bool Equals(object obj)
{
if (obj is BankAccount other)
return this == other;
return false;
}
public override int GetHashCode()
=> (Owner, Balance).GetHashCode();
public override string ToString()
=> $"{Owner} 的账户: ¥{Balance:F2}";
}
class Program
{
static void Main()
{
Console.WriteLine("===== 银行账户系统 — 运算符重载演示 =====\n");
BankAccount zhangSan = new BankAccount("张三", 1000);
BankAccount liSi = new BankAccount("李四", 500);
Console.WriteLine($"初始状态:");
Console.WriteLine($" {zhangSan}");
Console.WriteLine($" {liSi}");
// 存款(+ 运算符)
Console.WriteLine("\n--- 存款 200 ---");
zhangSan += 200;
Console.WriteLine($" {zhangSan}");
// 取款(- 运算符)
Console.WriteLine("\n--- 取款 100 ---");
liSi -= 100;
Console.WriteLine($" {liSi}");
// 比较(>、< 运算符)
Console.WriteLine("\n--- 比较余额 ---");
Console.WriteLine($" 张三 > 李四? {zhangSan > liSi}");
Console.WriteLine($" 李四 < 张三? {liSi < zhangSan}");
// 自增(+1 元利息)
Console.WriteLine("\n--- 发放利息(++) ---");
zhangSan++;
Console.WriteLine($" {zhangSan}");
// 自减(-1 元手续费)
Console.WriteLine("\n--- 扣除手续费(--) ---");
zhangSan--;
Console.WriteLine($" {zhangSan}");
// 隐式转换(当成 decimal 用)
Console.WriteLine("\n--- 转换 ---");
decimal balance = zhangSan; // 隐式转换
Console.WriteLine($" 余额转为 decimal: {balance:C}");
double d = (double)liSi; // 显式转换
Console.WriteLine($" 余额转为 double: {d:F2}");
// 比较账户(用重载的 ==)
Console.WriteLine("\n--- 相等判断 ---");
BankAccount zhangSanCopy = new BankAccount("张三", 1200);
Console.WriteLine($" zhangSan 和 copy 余额相等? {zhangSan == zhangSanCopy}");
}
}
输出:
===== 银行账户系统 — 运算符重载演示 =====
初始状态:
张三 的账户: ¥1000.00
李四 的账户: ¥500.00
--- 存款 200 ---
张三 的账户: ¥1200.00
--- 取款 100 ---
李四 的账户: ¥400.00
--- 比较余额 ---
张三 > 李四? True
李四 < 张三? True
--- 发放利息(++) ---
张三 的账户: ¥1201.00
--- 扣除手续费(--) ---
张三 的账户: ¥1200.00
--- 转换 ---
余额转为 decimal: ¥1,200.00
余额转为 double: 400.00
--- 相等判断 ---
zhangSan 和 copy 余额相等? True
七、常见易错点(避坑指南)
坑1:重载 == 时忘记重写 Equals 和 GetHashCode
// ❌ 只重载了 == 和 !=,没重写 Equals/GetHashCode
// 编译器会警告!
public class Person
{
public string Name { get; set; }
public static bool operator ==(Person a, Person b) { ... }
public static bool operator !=(Person a, Person b) { ... }
// ⚠️ 警告:'Person' defines operator == or operator != but does not override Object.Equals(object o)
}
// ✅ 必须同时重写
public class Person
{
public string Name { get; set; }
public static bool operator ==(Person a, Person b) { ... }
public static bool operator !=(Person a, Person b) { ... }
public override bool Equals(object obj) { ... } // ← 必须
public override int GetHashCode() { ... } // ← 必须
}
坑2:比较运算符必须成对定义
// ❌ 只重载了 >,没重载 <
// public static bool operator >(T a, T b) { ... }
// ⚠️ 编译错误!> 和 < 必须成对定义
// ✅ 必须成对
public static bool operator >(T a, T b) { ... }
public static bool operator <(T a, T b) { ... } // ← 必须一起
// 同样,>= 和 <= 也必须成对
public static bool operator >=(T a, T b) { ... }
public static bool operator <=(T a, T b) { ... } // ← 必须一起
成对规则汇总:
| 如果重载 | 必须同时重载 |
|---|---|
== |
!= |
< |
> |
<= |
>= |
true |
false |
坑3:重载 == 时没有处理 null
// ❌ 没判断 null——可能 NullReferenceException
public static bool operator ==(Person a, Person b)
{
return a.Name == b.Name; // 如果 a 或 b 是 null 就崩了!
}
// ✅ 先处理 null 情况
public static bool operator ==(Person a, Person b)
{
if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
return true; // 两个都是 null,相等
if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
return false; // 一个是 null,不相等
return a.Name == b.Name; // 都不是 null,正常比较
}
坑4:运算符重载不能改变优先级和结合性
// 运算符重载只能改变行为,不能改变优先级
// a + b * c 中 * 的优先级还是高于 +,无法改变
// a = b = c 的结合性还是从右到左,无法改变
坑5:运算符重载必须是 static
// ❌ 错误
// public Point operator +(Point other) { ... } // 不可以是实例方法
// ✅ 正确
public static Point operator +(Point a, Point b) { ... } // 必须是静态
坑6:ref 和 out 不能在运算符中使用
// ❌ 不允许
// public static Point operator +(ref Point a, ref Point b) { ... }
// ✅ 正确
public static Point operator +(Point a, Point b) { ... }
坑7:过度重载导致歧义
// ❌ 不要用运算符做"不直观"的事
// public static Person operator +(Person p, string address) { ... }
// + 号用来"添加地址"不直观,应该用方法名
// ✅ 运算符的含义应该符合数学或逻辑直观
// Point + Point → 坐标相加 ✅
// Money + decimal → 存款 ✅
// Person + string → 不直观 ❌
八、总结
运算符重载速查
| 类别 | 运算符 | 关键字 | 必成对? |
|---|---|---|---|
| 算术 | + - * / % |
operator + |
否 |
| 一元 | + - ! ~ ++ -- |
operator ++ |
否 |
| 比较 | == != |
operator == |
✅ |
| 比较 | < > |
operator < |
✅ |
| 比较 | <= >= |
operator <= |
✅ |
| 逻辑 | true false |
operator true |
✅ |
| 位运算 | & | ^ |
operator & |
否 |
| 转换 | 隐式 | implicit operator |
— |
| 转换 | 显式 | explicit operator |
— |
核心规则速记
1. public static —— 必须是公开的静态方法
2. 成对出现 —— ==/!=、</>、<=/>=、true/false
3. 重载 == 必须重写 Equals 和 GetHashCode
4. 处理 null —— == 和 != 必须先判空
5. 语义合理 —— 运算符含义要符合直觉
6. 不能改变优先级 —— 只能改行为,不能改语法规则
记忆口诀
运算符重载有门道,public static 少不了
operator 关键字后跟符号,参数至少有一个本类型
大于小于要成对,等于不等也要配
true 和 false 一起写,重载等号重写 Equals
隐式转换 implicit,数据安全自动转
显式转换 explicit,可能丢数要强转
算术比较看语义,直觉不对别乱加
一句话总结:运算符重载让你的自定义类型可以像内置类型一样使用
+、-、==、>等运算符。语法是public static T operator X(T a, T b)。关键规则是成对重载(==/!=、>/<、true/false),重载 == 必须重写 Equals/GetHashCode,运算符含义要符合数学直觉。