【C#】演算子のオーバーロードで自作クラスを直感的に扱う

C#では、クラスや構造体に対して標準の演算子(==+< など)の動作を独自に定義することができます。これを「演算子のオーバーロード」と呼びます。

適切に実装することで、自作の型であっても数値や文字列と同じように自然な記法で比較や計算が可能になります。今回は、時刻を管理するTime構造体を題材に、等価比較、大小比較、および型変換演算子の実装方法について解説します。


目次

演算子オーバーロードの基本ルール

演算子を定義する際は、以下のルールに従う必要があります。

  1. public static で定義する: 演算子は特定のインスタンスではなく、型全体に関連付けられます。
  2. 対になる演算子を定義する: == を定義する場合、必ず != も定義する必要があります。同様に <> もセットで実装します。
  3. EqualsGetHashCode をオーバーライドする: == 演算子を定義する場合、一貫性を保つために object.Equals メソッドなども適切に再定義することが推奨されます。

実践的なコード例:Time構造体の実装

以下のコードは、時・分・秒を持つ Time 構造体に対し、演算子のオーバーロードと型変換を実装した例です。比較ロジックを簡略化するため、時刻を整数値(HHMMSS形式)に変換して比較するテクニックを使用しています。

using System;

namespace CustomTypes
{
    class Program
    {
        static void Main()
        {
            var tm1 = new Time(13, 14, 5);  // 13:14:05
            var tm2 = new Time(13, 14, 5);  // 13:14:05
            var tm3 = new Time(13, 16, 21); // 13:16:21

            // 1. 等価演算子 (==, !=) の利用
            if (tm1 == tm2)
            {
                Console.WriteLine("tm1 == tm2 : 等しいです");
            }

            if (tm1 != tm3)
            {
                Console.WriteLine("tm1 != tm3 : 等しくありません");
            }

            // 2. 比較演算子 (<, >) の利用
            if (tm1 < tm3)
            {
                Console.WriteLine("tm1 < tm3  : tm1の方が前の時刻です");
            }

            // 3. 明示的な型変換 (Time <-> TimeSpan) の利用
            TimeSpan ts = (TimeSpan)tm1;
            Console.WriteLine($"TimeSpan変換: {ts}");

            Time tmFromTs = (Time)ts;
            Console.WriteLine($"Time復元: {tmFromTs.Hour}:{tmFromTs.Minute}:{tmFromTs.Second}");
        }
    }

    // 不変性を保つため readonly struct とし、比較用インターフェースも実装するのがベストプラクティス
    public readonly struct Time : IEquatable<Time>, IComparable<Time>
    {
        public int Hour { get; }
        public int Minute { get; }
        public int Second { get; }

        public Time(int hour, int minute = 0, int second = 0)
        {
            Hour = hour;
            Minute = minute;
            Second = second;
        }

        // 比較用に時刻を整数値に変換するヘルパーメソッド
        // 例: 13:14:05 -> 131405
        private int ToInt32() => Hour * 10000 + Minute * 100 + Second;

        // --- Equals と GetHashCode のオーバーライド ---
        
        public override bool Equals(object obj)
        {
            return obj is Time other && Equals(other);
        }

        public bool Equals(Time other)
        {
            return this.ToInt32() == other.ToInt32();
        }

        public override int GetHashCode()
        {
            return ToInt32().GetHashCode();
        }

        // --- 演算子のオーバーロード ---

        // 等価演算子 (==, !=)
        public static bool operator ==(Time left, Time right)
        {
            return left.Equals(right);
        }

        public static bool operator !=(Time left, Time right)
        {
            return !left.Equals(right);
        }

        // 比較演算子 (<, >)
        // ※C#では < を定義する場合 > も定義する必要があります
        public static bool operator <(Time left, Time right)
        {
            return left.ToInt32() < right.ToInt32();
        }

        public static bool operator >(Time left, Time right)
        {
            return left.ToInt32() > right.ToInt32();
        }

        // --- 型変換演算子 (explicit) ---

        // Time -> TimeSpan への明示的変換
        public static explicit operator TimeSpan(Time time)
        {
            return new TimeSpan(time.Hour, time.Minute, time.Second);
        }

        // TimeSpan -> Time への明示的変換
        public static explicit operator Time(TimeSpan timeSpan)
        {
            return new Time(timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds);
        }

        // IComparableの実装(ソートなどで必要)
        public int CompareTo(Time other)
        {
            return this.ToInt32().CompareTo(other.ToInt32());
        }
    }
}

実行結果

tm1 == tm2 : 等しいです
tm1 != tm3 : 等しくありません
tm1 < tm3  : tm1の方が前の時刻です
TimeSpan変換: 13:14:05
Time復元: 13:14:5

技術的なポイント

1. IEquatable<T> の実装

構造体(値型)で Equals をオーバーライドする場合、ボクシング(object型への変換)を防ぎパフォーマンスを向上させるために、IEquatable<T> インターフェースを実装することが強く推奨されます。

2. Explicit(明示的)と Implicit(暗黙的)

コード例では explicit operator を使用しました。これはキャスト (Type) を明示的に書く必要がある変換です。情報の欠落や例外が発生する可能性がある変換には explicit を使用し、安全に変換できる場合(例:intからlong)には implicit を使用するのが一般的です。

3. ロジックの集約

比較演算子 (<, >) や等価演算子 (==) の中身で、共通のロジック(今回の例では ToInt32() による数値化)を使用することで、実装ミスを防ぎメンテナンス性を高めることができます。

まとめ

演算子のオーバーロードを利用することで、自作クラスのコード記述量が減り、読みやすいプログラムになります。特に「値」として扱いたいオブジェクト(Value Object)を作成する際には、非常に有効な手段となります。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次