0Pricing
C# Academy · レッスン

演算子オーバーロードのベストプラクティス

オーバーロードした演算子を直感的に保ちます。

「演算子オーバーロードのベストプラクティス」は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フィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. 算術演算子のオーバーロード
  2. 比較演算子のオーバーロード
  3. ユーザー定義変換
  4. 演算子オーバーロードのベストプラクティス
← C# Academyに戻る