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
- Basic Rules of Operator Overloading
- Practical Code Example: Implementation of the Time Structure
- Execution Result
- Technical Points
- Implementation of IEquatable<T>
- Explicit vs. Implicit
- Aggregation of Logic
- 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
EqualsandGetHashCode: When defining the==operator, it is recommended to appropriately redefineobject.Equalsand 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).
