[C#] Intuitive Handling of Custom Classes with Operator Overloading

In C#, the behavior of standard operators (such as ==, +, <) can be independently defined for classes and structures. This is called “Operator Overloading.”

By implementing this appropriately, even custom types can be compared and calculated using natural syntax, similar to numbers and strings. This article explains how to implement equality comparison, magnitude comparison, and type conversion operators, using a Time structure that manages time as a subject.

目次

Table of Contents

  1. Basic Rules of Operator Overloading
  2. Practical Code Example: Implementation of the Time Structure
  3. Execution Result
  4. Technical Points
    1. Implementation of IEquatable<T>
    2. Explicit vs. Implicit
    3. Aggregation of Logic
  5. Summary

Basic Rules of Operator Overloading

When defining operators, the following rules must be followed:

  • Define as public static: Operators are associated with the type itself, not a specific instance.
  • Define paired operators: If == is defined, != must also be defined. Similarly, < and > must be implemented as a set.
  • Override Equals and GetHashCode: When defining the == operator, it is recommended to appropriately redefine object.Equals and other methods to maintain consistency.

Practical Code Example: Implementation of the Time Structure

The following code is an example of implementing operator overloading and type conversion for a Time structure that possesses hour, minute, and second data. To simplify the comparison logic, a technique is used where the time is converted into an integer value (HHMMSS format) for comparison.

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. Using equality operators (==, !=)
            if (tm1 == tm2)
            {
                Console.WriteLine("tm1 == tm2 : Equal");
            }

            if (tm1 != tm3)
            {
                Console.WriteLine("tm1 != tm3 : Not equal");
            }

            // 2. Using comparison operators (<, >)
            if (tm1 < tm3)
            {
                Console.WriteLine("tm1 < tm3  : tm1 is earlier");
            }

            // 3. Using explicit type conversion (Time <-> TimeSpan)
            TimeSpan ts = (TimeSpan)tm1;
            Console.WriteLine($"TimeSpan Conversion: {ts}");

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

    // Best practice: make it a readonly struct and implement comparison interfaces for immutability
    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;
        }

        // Helper method to convert time to an integer for comparison
        // Example: 13:14:05 -> 131405
        private int ToInt32() => Hour * 10000 + Minute * 100 + Second;

        // --- Overriding Equals and 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();
        }

        // --- Operator Overloading ---

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

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

        // Comparison operators (<, >)
        // In C#, if < is defined, > must also be defined
        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();
        }

        // --- Type Conversion Operators (explicit) ---

        // Explicit conversion from Time -> TimeSpan
        public static explicit operator TimeSpan(Time time)
        {
            return new TimeSpan(time.Hour, time.Minute, time.Second);
        }

        // Explicit conversion from TimeSpan -> Time
        public static explicit operator Time(TimeSpan timeSpan)
        {
            return new Time(timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds);
        }

        // Implementation of IComparable (Required for sorting, etc.)
        public int CompareTo(Time other)
        {
            return this.ToInt32().CompareTo(other.ToInt32());
        }
    }
}

Execution Result

tm1 == tm2 : Equal
tm1 != tm3 : Not equal
tm1 < tm3  : tm1 is earlier
TimeSpan Conversion: 13:14:05
Time Restoration: 13:14:5

Technical Points

1. Implementation of IEquatable<T>

When overriding Equals in a structure (value type), implementing the IEquatable<T> interface is strongly recommended. This prevents boxing (conversion to object type) and improves performance.

2. Explicit vs. Implicit

The code example used explicit operator. This represents a conversion that requires writing a cast (Type) explicitly. Generally, explicit is used for conversions where information loss or exceptions might occur, while implicit is used for safe conversions (e.g., int to long).

3. Aggregation of Logic

By using common logic (in this example, converting to a number via ToInt32()) within the comparison operators (<, >, ==), implementation errors can be prevented, and maintainability is enhanced.

Summary

Utilizing operator overloading reduces the amount of code required for custom classes and results in more readable programs. This is an extremely effective means when creating objects that are treated as “values” (Value Objects).

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

この記事を書いた人

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

目次