減法運算 (-) 在 C# 中展現豐富的多型行為:支援數值減法、列舉差值計算、委派移除,甚至隱含字串操作 (透過多載)。理解各類型語意差異、溢位邊界與一元負號特性,對正確建模領域邏輯至關重要。
支援減法的類型總覽
| 類型分類 | 支援形式 | 語意 | 結果類型 |
| 整數/浮點數/decimal | 二元 a - b | 數值減法 | 較大類型 |
| 列舉 | E - E, E - U | 差值/減去基底值 | 基底型別 / E |
| 委派 | D - D | 移除調用清單 | D |
| TimeSpan/DateTime | 二元減法 | 時間差/時間點減去間隔 | TimeSpan / DateTime |
| 指標 (unsafe) | T* - T*, T* - n | 元素差值/指標減去位移 | ptrdiff_t / T* |
數值減法:溢位與特殊值
整數溢位
int min = int.MinValue; // -2147483648
int max = int.MaxValue; // 2147483647
// unchecked (預設):環繞
var wrap1 = min - 1; // 2147483647 (MaxValue)
var wrap2 = max - (-1); // -2147483648 (MinValue)
// checked:拋出 OverflowException
checked { min - 1; } // Exception
// 長整數避免溢位
long safe = (long)min - 1; // -2147483649L
浮點數特殊值 (IEEE 754)
| a - b | 結果 | 說明 |
| ∞ - ∞ | NaN | 不定式 |
| ∞ - 正常 | ∞ | 保持無窮大 |
| 正常 - ∞ | -∞ | 翻轉符號 |
| 0 - 0 | +0.0 | 正零 (IEEE 規則) |
| -0 - +0 | -0.0 | 負零 |
| NaN - 任何 | NaN | 傳播 |
decimal 減法
decimal a = 10.5m, b = 3.2m;
decimal c = a - b; // 7.3m (精確)
// 溢位例外
decimal min = decimal.MinValue;
min - 1; // OverflowException (無 Infinity)
一元負號 (-x) 特性
int x = 5;
int neg = -x; // -5
// 陷阱:最小值取負溢位
int min = int.MinValue; // -2147483648
int negMin = -min; // -2147483648 (環繞,結果仍為 MinValue!)
checked { -min; } // OverflowException
// 浮點數:精確翻轉符號
double d = -0.0;
double negD = -d; // +0.0
double nan = double.NaN;
-nan; // NaN (符號位翻轉,但仍為 NaN)
列舉類型減法:兩種形式
enum Priority { Low = 1, Medium = 10, High = 100 }
// 1. E - E → 基底型別 (差值)
int diff = Priority.High - Priority.Low; // 99 (int)
// 2. E - U → E (減去基底值)
Priority p = Priority.High - 50; // 值 50 (但無對應列舉成員)
// 實際編譯為:(Priority)((int)Priority.High - 50)
// 底層機制
// E - E : (U)((U)x - (U)y) → 基底型別
// E - U : (E)((U)x - y) → E
實戰:列舉旗標移除 (實際用 & 運算)
[Flags] enum Perm { Read = 1, Write = 2, Exec = 4 }
Perm p = Perm.Read | Perm.Write | Perm Exec; // 7
// 錯誤:減法非旗標移除
// p = p - Perm.Write; // p = 5 (Read|Exec) 但語意錯誤
// 正確:位元 AND 移除
p &= ~Perm.Write; // p = 5 (Read|Exec)
委派減法:移除調用清單
Action a1 = () => Console.WriteLine("A");
Action a2 = () => Console.WriteLine("B");
Action a3 = () => Console.WriteLine("C");
Action multi = a1 + a2 + a3; // 調用清單: [a1, a2, a3]
// 移除單一委派
multi -= a2; // 清單: [a1, a3]
// 移除不存在的委派:無效果,回傳原清單
multi -= () => Console.WriteLine("X"); // 仍為 [a1, a3]
// 移除多播委派片段
Action pair = a1 + a2;
multi = a1 + a2 + a3;
multi -= pair; // 清單: [a3]
// 完全移除 → null
multi -= multi; // null
multi -= a1; // null (無例外)
關鍵語意
- 尋找最後出現的匹配調用清單並移除
- 不匹配則無操作 (不拋出例外)
- 結果可能為
null (空委派)
字串減法:不支援 (但可模擬)
// C# 字串無內建減法
// "hello" - "ell" // 編譯錯誤
// 模擬:移除子字串
string Remove(string source, string remove)
=> source.Replace(remove, "");
// 或使用 Regex
string RemovePattern(string source, string pattern)
=> Regex.Replace(source, pattern, "");
DateTime/TimeSpan 減法
DateTime now = DateTime.Now;
DateTime past = now.AddDays(-7);
TimeSpan diff = now - past; // 7 天
DateTime future = now + diff; // 回到 now
// TimeSpan - TimeSpan
TimeSpan t1 = TimeSpan.FromHours(5);
TimeSpan t2 = TimeSpan.FromHours(2);
TimeSpan t3 = t1 - t2; // 3 小時
使用者定義型別多載模式
public readonly struct Money : IEquatable
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{ Amount = amount; Currency = currency; }
public static Money operator -(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException("貨幣不匹配");
return new Money(a.Amount - b.Amount, a.Currency);
}
public static Money operator -(Money a) // 一元負號
=> new Money(-a.Amount, a.Currency);
// 隱含轉換輔助
public static implicit operator Money((decimal Amount, string Currency) t)
=> new Money(t.Amount, t.Currency);
}
效能特性
| 類型 | 指令 | 延遲 | 備註 |
| 整數 | SUB | 1 週期 | 極快 |
| 浮點數 | SUBSS/SUBSD | 3-5 週期 | SIMD 友善 |
| decimal | 軟體模擬 | ~50-100 週期 | 128 位整數減法 |
| 委派 | 方法呼叫 | ~微秒級 | 清單搜尋+配置新陣列 |
最佳實踐總結
- 整數減法溢位風險 —
MinValue - 1 環繞,用 checked 或 long
- 浮點數 ∞ - ∞ = NaN — 檢查特殊值避免污染計算
- 列舉減法語意特殊 —
E-E 回傳基底型別差值,E-U 回傳列舉
- 委派減法移除最後匹配 — 非存在無錯誤,結果可能為 null
- 旗標移除用位元運算 —
flags &= ~Flag,非減法
- TimeSpan/DateTime 減法型別安全 — 編譯器保證時間維度正確
- 自訂型別多載減法 — 成對多載二元/一元,驗證領域不變量
精通減法運算在各類型上的語意邊界,能精確建模領域邏輯並避免溢位、精度遺失與語意錯誤。