1. 引言
你应该已经习惯用像 + 这样的操作符来做数字相加,或者用 == 来比较。对于内置数据类型,这些操作符用起来很顺手。但如果你写了自己的类,比如 Vector(向量),想让两个向量相加看起来很自然,比如 vector1 + vector2 呢?或者你有 Money(钱)对象,想让比较两个对象像 moneyAmount1 == moneyAmount2 一样简单?这时候,C# 的 操作符重载 就派上用场了。
操作符重载让你可以定义标准操作符(比如 +、-、*、/、==、!=、>、<、++、-- 等等)在你自定义的类或结构体实例上怎么工作。这样你的代码就更直观、可读性更强,也更有表现力,可以用熟悉的语法操作你自己的类型。
操作符重载基础
要重载操作符,你需要在自己的类或结构体里声明一个静态方法,用 operator 关键字,后面跟你想重载的操作符符号。
- 静态方法: 重载操作符的方法必须是 public static。
- 方法名: 方法名就是 operator 关键字,后面跟操作符符号(比如 operator +、operator ==)。
- 参数: 参数数量取决于操作符类型(单目还是双目)。
- 单目操作符(比如 +、-、!、++、--):只接收一个参数,类型就是你定义的类/结构体。
- 双目操作符(比如 +、-、*、/、==、!=):接收两个参数,至少有一个是你定义的类/结构体类型。
- 返回类型: 操作符返回的类型。
2. 双目操作符重载 (Binary Operators)
双目操作符有两个操作数。最常见的就是算术操作符(+、-、*、/)和比较操作符(==、!=、>、<、>=、<=)。
例子:重载加法操作符 (+)
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y) => (X, Y) = (x, y);
// 重载 + 操作符
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 point1 = new Point(1, 2);
Point point2 = new Point(3, 4);
Point sumPoint = point1 + point2; // 这里调用了重载的 + 操作符
Console.WriteLine($"点的和: {sumPoint}"); // 输出: 点的和: (4, 6)
这里我们定义了两个 Point 对象相加时,返回一个新的 Point,它的坐标就是原来两个点坐标的和。
例子:重载乘法操作符 (*),向量乘以标量(数字)
public struct Vector
{
public double X { get; set; }
public double Y { get; set; }
public Vector(double x, double y) => (X, Y) = (x, y);
// 重载 * 操作符(向量 * 数字)
public static Vector operator *(Vector vec, double scalar)
{
return new Vector(vec.X * scalar, vec.Y * scalar);
}
// 重载 * 操作符(数字 * 向量)- 对称写法
public static Vector operator *(double scalar, Vector vec)
{
return new Vector(vec.X * scalar, vec.Y * scalar);
}
public override string ToString() => $"<{X}, {Y}>";
}
// 用法:
Vector vec1 = new Vector(2, 3);
Vector scaledVec1 = vec1 * 5; // 调用 Vector * double
Vector scaledVec2 = 5 * vec1; // 调用 double * Vector
Console.WriteLine($"缩放后的向量1: {scaledVec1}"); // 输出: 缩放后的向量1: <10, 15>
Console.WriteLine($"缩放后的向量2: {scaledVec2}"); // 输出: 缩放后的向量2: <10, 15>
注意我们对乘法操作符重载了两次,这样 Vector * double 和 double * Vector 都能用。
例子:重载比较操作符 (== 和 !=)
重载 == 和 != 时有个重要约定:你重载了其中一个,必须也重载另一个。而且,强烈建议这两个操作符的行为要和 Equals() 和 GetHashCode() 方法一致。
public class Money
{
public decimal Amount { get; set; }
public string Currency { get; set; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
// 如果重载 == / !=,一定要重写 Equals 和 GetHashCode
public override bool Equals(object? obj)
{
if (obj is not Money other) return false;
return Amount == other.Amount && Currency == other.Currency;
}
public override int GetHashCode() => HashCode.Combine(Amount, Currency);
// 重载 == 操作符
public static bool operator ==(Money? m1, Money? m2)
{
// 引用类型要判空
if (ReferenceEquals(m1, null)) return ReferenceEquals(m2, null);
return m1.Equals(m2); // 用我们重写的 Equals
}
// 重载 != 操作符(重载 == 时必须也重载)
public static bool operator !=(Money? m1, Money? m2)
{
return !(m1 == m2);
}
}
// 用法:
Money cash1 = new Money(100, "USD");
Money cash2 = new Money(100, "USD");
Money cash3 = new Money(50, "USD");
Money cash4 = new Money(100, "EUR");
Console.WriteLine($"cash1 == cash2: {cash1 == cash2}"); // True
Console.WriteLine($"cash1 == cash3: {cash1 == cash3}"); // False
Console.WriteLine($"cash1 == cash4: {cash1 == cash4}"); // False
对于值类型(结构体),== 和 != 默认是按位比较,通常没问题。但对于类(class),默认 == 比较的是引用,所以重载它可以让你实现值比较。
3. 单目操作符重载 (Unary Operators)
单目操作符只操作一个对象。比如:+(正号)、-(负号)、!(逻辑非)、~(按位非)、++(自增)、--(自减)。
例子:重载单目负号 (-)
public struct Vector3D
{
public double X, Y, Z;
public Vector3D(double x, double y, double z) => (X, Y, Z) = (x, y, z);
// 重载单目 - 操作符
public static Vector3D operator -(Vector3D vec)
{
return new Vector3D(-vec.X, -vec.Y, -vec.Z);
}
public override string ToString() => $"<{X}, {Y}, {Z}>";
}
// 用法:
Vector3D originalVec = new Vector3D(1, -2, 3);
Vector3D invertedVec = -originalVec; // 调用重载的单目 - 操作符
Console.WriteLine($"原始向量: {originalVec}"); // 输出: 原始向量: <1, -2, 3>
Console.WriteLine($"反向向量: {invertedVec}"); // 输出: 反向向量: <-1, 2, -3>
例子:重载自增 (++) 和自减 (--) 操作符
这些操作符会改变操作数并返回改变后的值。
public struct Counter
{
public int Value { get; set; }
public Counter(int value) => Value = value;
// 重载自增 ++ 操作符
public static Counter operator ++(Counter c)
{
// 注意:如果结构体是不可变的,返回新对象;
// 如果是类,可以修改当前对象再返回
return new Counter(c.Value + 1);
}
// 重载自减 -- 操作符
public static Counter operator --(Counter c)
{
return new Counter(c.Value - 1);
}
public override string ToString() => $"[计数器: {Value}]";
}
// 用法:
Counter myCounter = new Counter(5);
myCounter++; // 后缀自增
Console.WriteLine(myCounter); // 输出: [计数器: 6]
++myCounter; // 前缀自增
Console.WriteLine(myCounter); // 输出: [计数器: 7]
myCounter--;
Console.WriteLine(myCounter); // 输出: [计数器: 6]
重载的 ++ 和 -- 总是前缀形式(先改再返回)。编译器会自动生成后缀形式的正确行为。
4. 类型转换操作符重载
你可以定义你的类型实例怎么被显式或隐式转换成其他类型,反之亦然。
- implicit(隐式转换):当转换总是安全的、不会丢数据时用(比如 int 转 long)。
- explicit(显式转换):当转换可能丢数据或出错,需要你手动指定((Type)obj)。
例子:隐式把 Score 转成 int
public struct Score
{
public int Points { get; set; }
public Score(int points) => Points = points;
// 隐式把 Score 转成 int
public static implicit operator int(Score s)
{
return s.Points;
}
}
// 用法:
Score examScore = new Score(95);
int scoreValue = examScore; // 隐式转换
Console.WriteLine($"分数: {scoreValue}"); // 输出: 分数: 95
例子:显式把 Celsius 转成 Fahrenheit
public struct Celsius
{
public double Degrees { get; set; }
public Celsius(double degrees) => Degrees = degrees;
}
public struct Fahrenheit
{
public double Degrees { get; set; }
public Fahrenheit(double degrees) => Degrees = degrees;
// 显式把 Fahrenheit 转成 Celsius
public static explicit operator Celsius(Fahrenheit f)
{
return new Celsius((f.Degrees - 32) * 5 / 9);
}
// 显式把 Celsius 转成 Fahrenheit
public static explicit operator Fahrenheit(Celsius c)
{
return new Fahrenheit(c.Degrees * 9 / 5 + 32);
}
}
// 用法:
Celsius c = new Celsius(25);
Fahrenheit f = (Fahrenheit)c; // 显式转换
Console.WriteLine($"25°C = {f.Degrees}°F"); // 输出: 25°C = 77°F
Fahrenheit f2 = new Fahrenheit(212);
Celsius c2 = (Celsius)f2; // 显式转换
Console.WriteLine($"212°F = {c2.Degrees}°C"); // 输出: 212°F = 100°C
5. 限制和建议
不是所有操作符都能重载: 你不能重载像 &&、||、?.、new、typeof、is、as、==(对于 string,因为它已经在 string 类里重载并有特殊行为)、()(方法调用)、=(赋值)等操作符,还有一些别的。
对称性: 如果你重载了一个双目操作符,通常也建议对调参数顺序也重载一下(如果有意义),就像 Vector * double 的例子。
约定: 一定要遵守约定,特别是比较操作符(==、!=、>、<、>=、<=)和 Equals()、GetHashCode() 的关系。违背这些约定会导致很奇怪的 bug,尤其是在集合里。
可读性和直观性: 只有当重载操作符让代码更直观、更好读时才去重载。如果操作符的行为不明显或者容易误导,最好用有明确名字的方法(比如 Add() 代替 +)。
可变对象: 给可变类型重载操作符要小心。比如 Vector v1 = v2 + v3; 默认是创建新对象,不会改 v2。如果你的操作符会改原对象,别人可能会看不出来。
结构体 vs 类: 操作符重载更常用于结构体,因为它们本来就代表值(比如 Point、ComplexNumber),而且数学运算或值比较很自然。
GO TO FULL VERSION