CodeGym /课程 /C# SELF /C#中的操作符重载

C#中的操作符重载

C# SELF
第 34 级, 课程 2
可用

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),而且数学运算或值比较很自然。

2
任务
C# SELF, 第 34 级, 课程 2
已锁定
重载二元加法运算符
重载二元加法运算符
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION