操作符重載

C# 允許型別把某些運算符定義成特殊方法。對 CLR 來說,+-== 這些符號本身不是魔法;真正被記錄在 metadata 裡的是一組有特殊命名規則的方法。

最基本的規則是:

  • 必須是 public static
  • 方法名由編譯器映射成對應的 op_*
  • 至少有一個參數或回傳型別必須是目前型別

例如 +

public sealed class Complex
{
    public static Complex operator +(Complex c1, Complex c2)
    {
        return null;
    }
}

編譯後,概念上會變成類似下面的 IL 入口:

.method public hidebysig specialname static
        class CLR_Ch8.Complex op_Addition(
            class CLR_Ch8.Complex c1,
            class CLR_Ch8.Complex c2) cil managed
{
    IL_0000: ldnull
    IL_0001: ret
}

所以當編譯器看到 a + b,它其實是在找能不能綁到某個 op_Addition(...)

常見對應名稱

理解 CLR 命名之後,看運算符重載就沒那麼神祕了。幾個常見對應如下。

一元運算符

C#特殊方法
+xop_UnaryPlus
-xop_UnaryNegation
!xop_LogicalNot
~xop_OnesComplement
++x / x++op_Increment
--x / x--op_Decrement
trueop_True
falseop_False

二元運算符

C#特殊方法
+op_Addition
-op_Subtraction
*op_Multiply
/op_Division
%op_Modulus
&op_BitwiseAnd
|op_BitwiseOr
^op_ExclusiveOr
<<op_LeftShift
>>op_RightShift
==op_Equality
!=op_Inequality
<op_LessThan
>op_GreaterThan
<=op_LessThanOrEqual
>=op_GreaterThanOrEqual

什麼情況適合重載

從語法上來說能做,不代表設計上就該做。運算符重載最適合:

  • 數值型別
  • 向量 / 矩陣 / 複數
  • 明確具有數學語意的值物件

如果型別本身不是以「數學運算」為核心語意,硬塞運算符通常只會讓 API 變得更難猜。

一個很實用的判斷標準是:

  • 使用者能不能不看文件,就大致猜到 +-== 的意思
  • 這個語意是否穩定、對稱,而且不容易產生副作用

轉換操作符

除了 + 這類運算符,C# 還允許型別定義自訂型別轉換。這其實也是另一組特殊方法。

如果來源型別或目標型別不是編譯器內建能直接處理的轉換,編譯器就會去找對應的 conversion operator。

implicitexplicit

  • implicit:不需要呼叫端手動轉型
  • explicit:需要呼叫端寫出 cast

原則很簡單:

  • 不會造成資訊遺失、也不容易讓人驚訝的轉換,才適合 implicit
  • 可能失真、縮窄、或有語意風險的轉換,用 explicit
public sealed class Rational
{
    public Rational(int num) { }
    public Rational(float num) { }

    public int ToInt() => default;
    public float ToFloat() => default;

    public static implicit operator Rational(int num)
    {
        return new Rational(num);
    }

    public static implicit operator Rational(float num)
    {
        return new Rational(num);
    }

    public static explicit operator int(Rational r)
    {
        return r.ToInt();
    }

    public static explicit operator float(Rational r)
    {
        return r.ToFloat();
    }
}

編譯器如何處理轉換

當編譯器看到:

Rational r = 10;
int n = (int)r;

它不是直接「懂得」怎麼把 int 變成 Rational,而是會去找:

  • implicit operator Rational(int num)
  • explicit operator int(Rational r)

找到之後,再在 IL 中生成對應的方法呼叫。

換句話說,自訂轉換的本質不是語言層面的新型別系統,而是由編譯器協助插入的特殊方法呼叫

is / as 不會使用自訂轉換

這點很重要,也很容易踩錯。

即使你定義了 implicitexplicit operator,isas 也不會因此去呼叫它們。

它們檢查的是:

  • 參考轉換是否成立
  • 裝箱 / 拆箱語意是否成立
  • 執行時型別相容性是否成立

而不是「這個型別有沒有自訂寫一個轉換方法」。

小結

操作符方法其實可以濃縮成三句話:

  • 運算符重載的底層就是一組 op_* 特殊方法。
  • implicit 用於安全自然的轉換,explicit 用於可能失真的轉換。
  • is / as 不會理會你自訂的 conversion operator。

真正值得追求的不是「把語法玩得多像內建型別」,而是讓型別在語意上真的值得這樣使用。

參考資料

  • 《CLR via C#》