目 录CONTENT

文章目录

CSharp(三十二) 运算符重载(Operator Overloading)详解

CSharp(三十二) 运算符重载(Operator Overloading)详解


目录

  1. 什么是运算符重载
  2. 为什么需要运算符重载
  3. 基本语法骨架
  4. 一步步写出你的第一个运算符重载
  5. 运算符重载的工作原理
  6. 一元运算符重载
  7. 二元运算符重载
  8. 比较运算符重载
  9. 关系运算符重载
  10. 类型转换运算符
  11. true 和 false 运算符
  12. 可重载与不可重载的运算符
  13. 运算符重载规则速查表
  14. 实战案例:向量类
  15. 实战案例:金额类
  16. 实战案例:复数类
  17. 常见错误与注意事项
  18. 面试常考题
  19. 课后练习
  20. 小结

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 时,请按照我定义的规则来计算"。

一句话总结:运算符重载让你自定义的类型也能使用 +-== 等运算符,就像 intstring 一样自然。


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
返回类型 运算结果的类型 Pointboolint
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. 比较运算符重载

比较运算符用于判断两个对象是否相等或不等。

重要规则(必须遵守)

  1. ==!= 必须成对重载
  2. 重载 ==!= 时,必须同时重写 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?

当一个对象被放到 DictionaryHashSet 中时,系统会用 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. 关系运算符重载

关系运算符用于判断大小顺序。

重要规则

  1. <> 必须成对重载
  2. <=>= 必须成对重载
  3. 通常会实现 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 运算符

当一个对象可以被判定为"真"或"假"时,可以重载 truefalse 运算符。

使用场景

想象一个用户类,判断"用户是否有效":

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 获取名称

记忆口诀:方法调用式的关键字(newtypeofsizeofisasnameofdefault)都不行。短路逻辑(&&||)和三元(?:)也不行。


13. 运算符重载规则速查表

规则 说明
必须是 public static 关键字不可改变
必须是类的成员 不能在全局定义
返回类型可以是任意类型 不一定要和本类相同
参数中至少有一个是本类类型 operator +(Point, int) 可以,但不能两个都是 int
==!= 必须成对 缺一个编译错误
<> 必须成对 缺一个编译错误
<=>= 必须成对 缺一个编译错误
truefalse 必须成对 缺一个编译错误
不能有 ref/out/in 参数 参数只能按值传递
!= 通常委托给 == return !(a == b);
> 通常委托给 < return b < a;
>= 通常委托给 < return !(a < b);
<= 通常委托给 <> return a < b || a == b;
重载 == 必须重写 EqualsGetHashCode 否则编译器会警告

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:哪些运算符必须成对重载?

  • ==!=
  • <>
  • <=>=
  • truefalse

Q3:implicit 和 explicit 有什么区别?

特性 implicit explicit
转换方式 自动发生 需要显式转换 (Type)x
使用场景 绝对安全,不失数据 可能损失数据或抛异常
示例 int → double double → int(截断)

Q4:运算符重载和扩展方法有什么区别?

:扩展方法是给已有类"追加"实例方法,不能追加运算符。运算符重载只能在定义类时写在类内部,不能用扩展方法的方式给别人的类添加运算符。

Q5:重载 == 时为什么必须重写 EqualsGetHashCode

  • 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),让代码更接近自然语言。本质是静态方法,核心是直观


本文档专为教学编写,重在通俗易懂。建议每学完一个概念就动手敲一遍代码,加深理解。

0
  1. 微信打赏

    qrcode weixin

评论区