运算符重载最佳实践
让重载运算符符合直觉。
运算符重载最佳实践 是 CoddyKit 上的免费 C# Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。
运算符重载的最佳实践
运算符重载是一件锋利的工具。使用得当,它能让值类型直观易用;使用不当,则会让代码晦涩难懂。本课介绍一些能让重载运算符保持可预测性的原则。
仅在含义明确时重载
只有当某个运算符对您的类型而言含义明确时,才重载它。向量上的 + 显然表示逐分量相加;而在 Order 上使用 + 只会让读者感到困惑。
using System;
public struct Vector2
{
public int X, Y;
public Vector2(int x, int y) { X = x; Y = y; }
// + means adding components: obvious and expected
public static Vector2 operator +(Vector2 a, Vector2 b)
=> new Vector2(a.X + b.X, a.Y + b.Y);
public override string ToString() => "(" + X + ", " + Y + ")";
}
public class Program
{
public static void Main()
{
Console.WriteLine(new Vector2(1, 2) + new Vector2(3, 4));
}
}保持符合数学直觉
让运算符符合直觉:当数学运算满足交换律时,a + b 应等于 b + a,而且 + 不应暗中执行减法。这些意外行为会导致隐蔽的错误。
using System;
public struct Money
{
public decimal Amount;
public Money(decimal a) { Amount = a; }
public static Money operator +(Money a, Money b) => new Money(a.Amount + b.Amount);
public override string ToString() => "$" + Amount;
}
public class Program
{
public static void Main()
{
var x = new Money(3);
var y = new Money(5);
Console.WriteLine((x + y).Amount == (y + x).Amount); // commutative
}
}保持对称性和配套关系
语言会为某些运算符提供配套关系,但您也应保持逻辑上的对称性:如果添加了 +,请考虑添加 -;如果添加了 <,也应提供 > 以及等于运算符的变体。不完整的运算符集合会让用户感到挫败。
using System;
public struct Length
{
public int Mm;
public Length(int mm) { Mm = mm; }
public static Length operator +(Length a, Length b) => new Length(a.Mm + b.Mm);
public static Length operator -(Length a, Length b) => new Length(a.Mm - b.Mm);
public override string ToString() => Mm + "mm";
}
public class Program
{
public static void Main()
{
Console.WriteLine(new Length(50) + new Length(20));
Console.WriteLine(new Length(50) - new Length(20));
}
}不要修改操作数
运算符应当是纯的:计算并返回一个新值,同时保持操作数不变。在 + 中修改操作数会违背读者的所有预期。
using System;
public struct Vector2
{
public int X, Y;
public Vector2(int x, int y) { X = x; Y = y; }
public static Vector2 operator +(Vector2 a, Vector2 b)
=> new Vector2(a.X + b.X, a.Y + b.Y); // returns new, mutates nothing
public override string ToString() => "(" + X + ", " + Y + ")";
}
public class Program
{
public static void Main()
{
var a = new Vector2(1, 1);
var unused = a + new Vector2(9, 9);
Console.WriteLine("a is still " + a);
}
}让 == 与 Equals 保持一致
一条反复强调的最佳实践是:每当重载 == 时,都应重写 Equals 和 GetHashCode,让所有相等性判断路径保持一致。不一致是最常见的运算符错误之一。
using System;
public struct Point
{
public int X, Y;
public Point(int x, int y) { X = x; Y = y; }
public override bool Equals(object obj) => obj is Point p && p.X == X && p.Y == Y;
public override int GetHashCode() => HashCode.Combine(X, Y);
public static bool operator ==(Point a, Point b) => a.Equals(b);
public static bool operator !=(Point a, Point b) => !a.Equals(b);
}
public class Program
{
public static void Main()
{
Console.WriteLine(new Point(1, 1) == new Point(1, 1));
}
}提供命名替代方案
某些语言和分析工具无法使用运算符。除了 + 之外,再提供一个名为 Add 的方法,可以改善互操作性和可发现性。
using System;
public struct Vector2
{
public int X, Y;
public Vector2(int x, int y) { X = x; Y = y; }
public Vector2 Add(Vector2 other) => new Vector2(X + other.X, Y + other.Y);
public static Vector2 operator +(Vector2 a, Vector2 b) => a.Add(b);
public override string ToString() => "(" + X + ", " + Y + ")";
}
public class Program
{
public static void Main()
{
var v = new Vector2(1, 2);
Console.WriteLine(v.Add(new Vector2(3, 4)));
Console.WriteLine(v + new Vector2(3, 4));
}
}优先在类似值的类型上使用运算符
运算符用于不可变的、类似值的类型(结构体或类似记录的类)时会很自然。对于可变且强调身份的类型,命名方法通常能更好地表达意图。
using System;
public readonly struct Rational
{
public readonly int Num, Den;
public Rational(int n, int d) { Num = n; Den = d; }
public static Rational operator *(Rational a, Rational b)
=> new Rational(a.Num * b.Num, a.Den * b.Den);
public override string ToString() => Num + "/" + Den;
}
public class Program
{
public static void Main()
{
Console.WriteLine(new Rational(1, 2) * new Rational(2, 3));
}
}记录边界情况
请明确说明运算符如何处理特殊值:除以零、类类型的空操作数或溢出。可预测的处理方式可以避免调用方措手不及。
using System;
public struct SafeDiv
{
public int Value;
public SafeDiv(int v) { Value = v; }
// Documented behavior: dividing by zero yields zero
public static SafeDiv operator /(SafeDiv a, SafeDiv b)
=> new SafeDiv(b.Value == 0 ? 0 : a.Value / b.Value);
public override string ToString() => Value.ToString();
}
public class Program
{
public static void Main()
{
Console.WriteLine(new SafeDiv(10) / new SafeDiv(0));
Console.WriteLine(new SafeDiv(10) / new SafeDiv(2));
}
}严谨的运算符集合
完善的数值类型应结合少量直观的运算符、一致的相等性判断和不可变性。这样得到的类型表现得像内置数字一样,使用起来也十分愉快。
using System;
public readonly struct Vec
{
public readonly double X, Y;
public Vec(double x, double y) { X = x; Y = y; }
public static Vec operator +(Vec a, Vec b) => new Vec(a.X + b.X, a.Y + b.Y);
public static Vec operator -(Vec a, Vec b) => new Vec(a.X - b.X, a.Y - b.Y);
public static Vec operator *(Vec a, double k) => new Vec(a.X * k, a.Y * k);
public override string ToString() => "(" + X + ", " + Y + ")";
}
public class Program
{
public static void Main()
{
Console.WriteLine((new Vec(1, 2) + new Vec(3, 4)) * 2);
}
}亲自试一试
综合运用这些实践:创建一个不可变的值类型、直观的运算符、命名替代方案以及一致的相等性判断。
using System;
public readonly struct Meters : IEquatable<Meters>
{
public readonly double Value;
public Meters(double v) { Value = v; }
public Meters Add(Meters other) => new Meters(Value + other.Value);
public static Meters operator +(Meters a, Meters b) => a.Add(b);
public bool Equals(Meters other) => Value == other.Value;
public override bool Equals(object obj) => obj is Meters m && Equals(m);
public override int GetHashCode() => Value.GetHashCode();
public static bool operator ==(Meters a, Meters b) => a.Equals(b);
public static bool operator !=(Meters a, Meters b) => !a.Equals(b);
public override string ToString() => Value + "m";
}
public class Program
{
public static void Main()
{
Console.WriteLine(new Meters(3) + new Meters(4));
Console.WriteLine(new Meters(7) == new Meters(7));
}
}快速检查
应用运算符重载的最佳实践。
回顾
优秀的运算符重载应当严谨且可预测。
- 仅在含义明确时重载。
- 保持符合数学直觉,并保持对称性。
- 保持纯性:返回新值,绝不修改操作数。
- 让
==与Equals/GetHashCode保持一致。 - 提供命名替代方案,并优先选择类似值的类型。
常见问题解答
「运算符重载最佳实践」课时是免费的吗?
是的 — 「运算符重载最佳实践」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。
「运算符重载最佳实践」这节课中我会学到什么?
让重载运算符符合直觉。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 C# Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「运算符重载最佳实践」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 C# Academy 课中编写并运行代码吗?
能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。