CodeGym /Courses /C# SELF /Operator Overloading in C#

Operator Overloading in C#

C# SELF
Level 34, Lesson 2
Available

1. Introduction

You're probably used to using operators like + for adding numbers or == for comparing stuff. These operators work great with built-in data types. But what if you make your own class, say, Vector, and you want adding two vectors to look natural, like vector1 + vector2? Or you want comparing two Money objects to work like moneyAmount1 == moneyAmount2? That's exactly what operator overloading in C# is for.

Operator overloading lets you define how standard operators (like +, -, *, /, ==, !=, >, <, ++, -- and a bunch more) should work with instances of your own classes or structs. This makes your code more intuitive, readable, and expressive, letting you use familiar syntax for your custom types.

Operator Overloading Basics

To overload an operator, you declare a static method in your class or struct using the operator keyword, followed by the operator symbol you want to overload.

  • Static method: The operator overload method always has to be public static.
  • Method name: The method name is the operator keyword, followed by the operator symbol itself (like operator +, operator ==).
  • Parameters: The number of parameters depends on the operator type (unary or binary).
    • Unary operators (like +, -, !, ++, --): take one parameter of the class/struct type they're defined in.
    • Binary operators (like +, -, *, /, ==, !=): take two parameters, at least one of which must be the class/struct type they're defined in.
  • Return type: The type the operation returns.

2. Overloading Binary Operators

Binary operators take two operands. The most common ones are arithmetic operators (+, -, *, /) and comparison operators (==, !=, >, <, >=, <=).

Example: Overloading the Addition Operator (+)

public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }

    public Point(int x, int y) => (X, Y) = (x, y);

    // Overloading the + operator
    public static Point operator +(Point p1, Point p2)
    {
        return new Point(p1.X + p2.X, p1.Y + p2.Y);
    }

    public override string ToString() => $"({X}, {Y})";
}

// Usage:
Point point1 = new Point(1, 2);
Point point2 = new Point(3, 4);
Point sumPoint = point1 + point2; // Calls the overloaded + operator

Console.WriteLine($"Sum of points: {sumPoint}"); // Output: Sum of points: (4, 6)

Here we defined that adding two Point objects should return a new Point object, whose coordinates are the sums of the corresponding coordinates of the original points.

Example: Overloading the Multiplication Operator (*) with a Scalar (Number)

public struct Vector
{
    public double X { get; set; }
    public double Y { get; set; }

    public Vector(double x, double y) => (X, Y) = (x, y);

    // Overloading the * operator (vector times number)
    public static Vector operator *(Vector vec, double scalar)
    {
        return new Vector(vec.X * scalar, vec.Y * scalar);
    }

    // Overloading the * operator (number times vector) - for symmetry
    public static Vector operator *(double scalar, Vector vec)
    {
        return new Vector(vec.X * scalar, vec.Y * scalar);
    }

    public override string ToString() => $"<{X}, {Y}>";
}

// Usage:
Vector vec1 = new Vector(2, 3);
Vector scaledVec1 = vec1 * 5;      // Calls Vector * double
Vector scaledVec2 = 5 * vec1;      // Calls double * Vector

Console.WriteLine($"Scaled vector 1: {scaledVec1}"); // Output: Scaled vector 1: <10, 15>
Console.WriteLine($"Scaled vector 2: {scaledVec2}"); // Output: Scaled vector 2: <10, 15>

Notice that for the multiplication operator, we overloaded it twice so it works symmetrically: Vector * double and double * Vector.

Example: Overloading Comparison Operators (== and !=)

When overloading == and != operators, you have to follow an important contract: if you overload one, you must overload the other too. Also, it's highly recommended that these operators behave the same as your Equals() and GetHashCode() methods.

public class Money
{
    public decimal Amount { get; set; }
    public string Currency { get; set; }

    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }

    // Always override Equals and GetHashCode if you overload == / !=
    public override bool Equals(object? obj)
    {
        if (obj is not Money other) return false;
        return Amount == other.Amount && Currency == other.Currency;
    }

    public override int GetHashCode() => HashCode.Combine(Amount, Currency);

    // Overloading the == operator
    public static bool operator ==(Money? m1, Money? m2)
    {
        // Null check for reference types
        if (ReferenceEquals(m1, null)) return ReferenceEquals(m2, null);
        return m1.Equals(m2); // Use our overridden Equals
    }

    // Overloading the != operator (required if you overload ==)
    public static bool operator !=(Money? m1, Money? m2)
    {
        return !(m1 == m2);
    }
}

// Usage:
Money cash1 = new Money(100, "USD");
Money cash2 = new Money(100, "USD");
Money cash3 = new Money(50, "USD");
Money cash4 = new Money(100, "EUR");

Console.WriteLine($"cash1 == cash2: {cash1 == cash2}"); // True
Console.WriteLine($"cash1 == cash3: {cash1 == cash3}"); // False
Console.WriteLine($"cash1 == cash4: {cash1 == cash4}"); // False

For value types (structs), the == and != operators by default do bitwise comparison, which is usually fine. But for classes (class), by default == compares references, so overloading it is super useful to make sure you get value comparison.

3. Overloading Unary Operators

Unary operators work with a single operand. Examples: + (unary plus), - (unary minus), ! (logical NOT), ~ (bitwise NOT), ++ (increment), -- (decrement).

Example: Overloading the Unary Minus (-)

public struct Vector3D
{
    public double X, Y, Z;

    public Vector3D(double x, double y, double z) => (X, Y, Z) = (x, y, z);

    // Overloading the unary - operator
    public static Vector3D operator -(Vector3D vec)
    {
        return new Vector3D(-vec.X, -vec.Y, -vec.Z);
    }

    public override string ToString() => $"<{X}, {Y}, {Z}>";
}

// Usage:
Vector3D originalVec = new Vector3D(1, -2, 3);
Vector3D invertedVec = -originalVec; // Calls the overloaded unary - operator

Console.WriteLine($"Original vector: {originalVec}"); // Output: Original vector: <1, -2, 3>
Console.WriteLine($"Inverted vector: {invertedVec}"); // Output: Inverted vector: <-1, 2, -3>

Example: Overloading Increment (++) and Decrement (--) Operators

These operators change the operand and return the changed value.

public struct Counter
{
    public int Value { get; set; }

    public Counter(int value) => Value = value;

    // Overloading the ++ operator
    public static Counter operator ++(Counter c)
    {
        // Important: return a NEW object if the struct is immutable,
        // or change the current one and return it if it's a class
        return new Counter(c.Value + 1);
    }

    // Overloading the -- operator
    public static Counter operator --(Counter c)
    {
        return new Counter(c.Value - 1);
    }

    public override string ToString() => $"[Counter: {Value}]";
}

// Usage:
Counter myCounter = new Counter(5);
myCounter++; // Postfix increment
Console.WriteLine(myCounter); // Output: [Counter: 6]

++myCounter; // Prefix increment
Console.WriteLine(myCounter); // Output: [Counter: 7]

myCounter--;
Console.WriteLine(myCounter); // Output: [Counter: 6]

The overloaded ++ and -- operators are always prefix (they change first, then return). The compiler automatically generates the right behavior for the postfix form.

4. Overloading Type Conversion Operators

You can define how instances of your type can be explicitly or implicitly converted to another type, and vice versa.

  • implicit (implicit conversion): Used when the conversion is always safe and doesn't lose data (like int to long).
  • explicit (explicit conversion): Used when the conversion might lose data or throw an error, and needs to be specified explicitly ((Type)obj).

Example: Implicit Conversion from Score to int

public struct Score
{
    public int Points { get; set; }
    public Score(int points) => Points = points;

    // Implicit conversion from Score to int
    public static implicit operator int(Score s)
    {
        return s.Points;
    }
}

// Usage:
Score examScore = new Score(95);
int scoreValue = examScore; // Implicit conversion
Console.WriteLine($"Points: {scoreValue}"); // Output: Points: 95

Example: Explicit Conversion from Celsius to Fahrenheit

public struct Celsius
{
    public double Degrees { get; set; }
    public Celsius(double degrees) => Degrees = degrees;
}

public struct Fahrenheit
{
    public double Degrees { get; set; }
    public Fahrenheit(double degrees) => Degrees = degrees;

    // Explicit conversion from Fahrenheit to Celsius
    public static explicit operator Celsius(Fahrenheit f)
    {
        return new Celsius((f.Degrees - 32) * 5 / 9);
    }

    // Explicit conversion from Celsius to Fahrenheit
    public static explicit operator Fahrenheit(Celsius c)
    {
        return new Fahrenheit(c.Degrees * 9 / 5 + 32);
    }
}

// Usage:
Celsius c = new Celsius(25);
Fahrenheit f = (Fahrenheit)c; // Explicit conversion
Console.WriteLine($"25°C = {f.Degrees}°F"); // Output: 25°C = 77°F

Fahrenheit f2 = new Fahrenheit(212);
Celsius c2 = (Celsius)f2; // Explicit conversion
Console.WriteLine($"212°F = {c2.Degrees}°C"); // Output: 212°F = 100°C

5. Limitations and Recommendations

Not all operators can be overloaded: You can't overload operators like &&, ||, ?., new, typeof, is, as, == (for string, because it's already overloaded in the string class and has special behavior), () (method call), = (assignment) and some others.

Symmetry: If you overload a binary operator, it's often a good idea to overload it for the reverse order of operands too, if that makes sense (like in the Vector * double example).

Contracts: Always follow the contracts, especially for comparison operators (==, !=, >, <, >=, <=) and how they relate to Equals() and GetHashCode(). Breaking these contracts can lead to weird and buggy behavior, especially in collections.

Readability and Intuitiveness: Only overload operators when it makes your code more intuitive and readable. If the operator's behavior isn't obvious or could be confusing, it's better to use regular methods with clear names (like Add() instead of +).

Mutable objects: Be careful when overloading operators for mutable types. For example, Vector v1 = v2 + v3; by default creates a new object, not changing v2. If your operator changes the existing object, that might not be obvious.

Structs vs Classes: Operator overloading is more common for structs, which are often value types (like Point, ComplexNumber), and for which math operations or value comparisons feel natural.

2
Task
C# SELF, level 34, lesson 2
Locked
Overloading the binary addition operator
Overloading the binary addition operator
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION