演算子オーバーロードのベストプラクティス
オーバーロードした演算子を直感的に保ちます。
「演算子オーバーロードのベストプラクティス」はCoddyKit上の無料C# Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応の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 もオーバーライドし、すべての等価性判定で結果が一致するようにすることです。ここでの不一致は、演算子に関する最も一般的なバグの1つです。
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));
}
}境界ケースをドキュメント化する
ゼロ除算、クラスの null オペランド、オーバーフローなど、演算子が特殊な値をどのように扱うかを明確にしてください。予測可能な処理にすることで、呼び出し側が思いがけない動作に遭遇するのを防げます。
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と一貫させます。- 名前付きの代替手段を提供し、値のように扱える型を優先します。
よくある質問
「演算子オーバーロードのベストプラクティス」レッスンは無料ですか?
はい。「演算子オーバーロードのベストプラクティス」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、C# Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 C# Academyコースには全4レッスンが含まれています。
「演算子オーバーロードのベストプラクティス」で何を学びますか?
オーバーロードした演算子を直感的に保ちます。 ブラウザで直接実行するハンズオンコードでC# Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
C# Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのC# Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。
「演算子オーバーロードのベストプラクティス」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このC# Academyレッスンでコードを書いて実行できますか?
はい。すべてのC# Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- 算術演算子のオーバーロード
- 比較演算子のオーバーロード
- ユーザー定義変換
- 演算子オーバーロードのベストプラクティス