CodeGym /課程 /C# SELF /C# 的運算子多載

C# 的運算子多載

C# SELF
等級 34 , 課堂 2
開放

1. 前言

你應該已經習慣用像 + 來加數字,或 == 來比大小。這些運算子對內建型別都超好用。但如果你自己寫了一個 class,比如 Vector(向量),你會不會想讓兩個向量加起來看起來很自然,像 vector1 + vector2?還是你想讓兩個 Money(錢)物件比較時可以直接寫 moneyAmount1 == moneyAmount2?這種情境下,C# 就有 運算子多載 這個功能啦。

運算子多載讓你可以自己定義標準運算子(像 +-*/==!=><++-- 等等)遇到你自己寫的 class 或 struct 時要怎麼動作。這樣你的 code 會更直覺、好讀又有表達力,讓你自訂型別也能用大家熟悉的語法。

運算子多載的基本

要多載一個運算子,你要在 class 或 struct 裡宣告一個 static method,用 operator 關鍵字,後面接你要多載的運算子符號。

  • 靜態方法: 運算子多載的方法一定要是 public static
  • 方法名稱: 方法名稱就是 operator,後面接運算子符號(像 operator +operator ==)。
  • 參數: 參數數量看運算子型態(單元或雙元)。
    • 單元運算子(像 +-!++--):只吃一個參數,型別就是你定義的 class/struct。
    • 雙元運算子(像 +-*/==!=):吃兩個參數,至少一個要是你定義的 class/struct。
  • 回傳型別: 運算子要回傳的型別。

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 * doubledouble * 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;
    }

    // 如果多載 == / !=,一定要 override 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)
    {
        // 參考型別要檢查 null
        if (ReferenceEquals(m1, null)) return ReferenceEquals(m2, null);
        return m1.Equals(m2); // 用我們 override 的 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

對於值型別(struct),==!= 預設是做 bitwise 比較,通常沒問題。但對 class(class)來說,== 預設是比參考,所以多載這個運算子可以讓你比內容而不是比參考。

3. 單元運算子多載 (Unary Operators)

單元運算子只吃一個運算元。像 +(單元正號)、-(單元負號)、!(邏輯非)、~(bitwise 非)、++(遞增)、--(遞減)。

範例:多載單元負號 (-)

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)
    {
        // 注意:如果 struct 是 immutable,要回傳新的物件;
        // 如果是 class,可以改自己然後回傳
        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(隱式轉換):當轉換永遠安全、不會丟資料時用(像 intlong)。
  • 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. 限制與建議

不是所有運算子都能多載: 你不能多載像 &&||?.newtypeofisas==(對 string,因為 string 已經有特別的多載)、()(呼叫方法)、=(賦值)等等。

對稱性: 如果你多載雙元運算子,通常也要考慮反過來的順序要不要多載(像 Vector * double)。

契約: 一定要遵守契約,特別是比較運算子(==!=><>=<=)跟 Equals()GetHashCode() 的關係。違反契約會讓你的 code 行為很怪,特別是在集合裡。

可讀性跟直覺: 只有在多載運算子真的讓 code 更直覺、好讀時才多載。如果行為不明顯或會讓人誤會,乾脆寫個有意義的方法(像 Add() 取代 +)。

可變(Mutable)物件: 幫可變型別多載運算子要小心。像 Vector v1 = v2 + v3; 預設是產生新物件,不會改 v2。如果你的運算子會改原本的物件,這樣很容易讓人搞混。

Struct 跟 Class: 運算子多載通常比較常用在 struct,因為 struct 通常代表值(像 PointComplexNumber),而且這些型別做數學運算或比大小很自然。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION