ZetCode

C# operator overloading

last modified October 16, 2026

In this article we show how to overload operators in C#.

Operator overloading allows us to define the behaviour of operators such as +, ==, or < for our own types. For instance, we can add two vectors with v1 + v2 instead of calling v1.Add(v2). The built-in types use this mechanism too; for example, the + operator concatenates strings and the - operator subtracts two DateTime values.

An operator is defined as a public static method with the operator keyword. At least one of its parameters must be of the containing type.

public static Vector operator +(Vector a, Vector b)

Operator overloading should be used only where the meaning of the operator is obvious, typically for mathematical types such as vectors, complex numbers, fractions, or money. A surprising operator makes the code harder to read.

C# arithmetic operators

In the first example, we define arithmetic operators for a two-dimensional vector.

Program.cs
var v1 = new Vector(2, 3);
var v2 = new Vector(4, -1);

Console.WriteLine(v1 + v2);
Console.WriteLine(v1 - v2);
Console.WriteLine(v1 * 3);
Console.WriteLine(3 * v1);
Console.WriteLine(-v1);

var v3 = v1;
v3 += v2;
Console.WriteLine(v3);

readonly record struct Vector(double X, double Y)
{
    public static Vector operator +(Vector a, Vector b) =>
        new(a.X + b.X, a.Y + b.Y);

    public static Vector operator -(Vector a, Vector b) =>
        new(a.X - b.X, a.Y - b.Y);

    public static Vector operator -(Vector v) => new(-v.X, -v.Y);

    public static Vector operator *(Vector v, double k) => new(v.X * k, v.Y * k);

    public static Vector operator *(double k, Vector v) => v * k;

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

The Vector is an immutable record struct. We define binary + and - operators, the unary minus operator, and multiplication by a scalar.

public static Vector operator -(Vector v) => new(-v.X, -v.Y);

A unary operator has a single parameter.

public static Vector operator *(Vector v, double k) => new(v.X * k, v.Y * k);

public static Vector operator *(double k, Vector v) => v * k;

The operands of an operator are not swapped automatically. To support both v1 * 3 and 3 * v1, we define two overloads.

v3 += v2;

When a binary operator is defined, the corresponding compound assignment operator works automatically; v3 += v2 is evaluated as v3 = v3 + v2.

$ dotnet run
(6, 2)
(-2, 4)
(6, 9)
(6, 9)
(-2, -3)
(6, 2)

C# equality operators

The == and != operators must be overloaded in pairs. When we define them, we should also override the Equals and GetHashCode methods, so that all ways of comparing the objects give the same results.

Program.cs
var m1 = new Money(100m, "EUR");
var m2 = new Money(100m, "EUR");
var m3 = new Money(100m, "USD");

Console.WriteLine(m1 == m2);
Console.WriteLine(m1 != m3);
Console.WriteLine(m1.Equals(m2));
Console.WriteLine(ReferenceEquals(m1, m2));

var set = new HashSet<Money> { m1, m2, m3 };
Console.WriteLine(set.Count);

class Money(decimal amount, string currency) : IEquatable<Money>
{
    public decimal Amount => amount;
    public string Currency => currency;

    public bool Equals(Money? other) =>
        other is not null && Amount == other.Amount && Currency == other.Currency;

    public override bool Equals(object? obj) => Equals(obj as Money);

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

    public static bool operator ==(Money? a, Money? b) =>
        a is null ? b is null : a.Equals(b);

    public static bool operator !=(Money? a, Money? b) => !(a == b);
}

By default, the == operator compares references of class objects. We change it to compare the values.

public static bool operator ==(Money? a, Money? b) =>
    a is null ? b is null : a.Equals(b);

The operator handles null operands. Inside the operator, we use the is null pattern instead of == null, which would call the operator recursively.

var set = new HashSet<Money> { m1, m2, m3 };

Thanks to the GetHashCode override, the hash set treats the two equal objects as one element.

$ dotnet run
True
True
True
False
2

Records generate the equality operators, Equals, and GetHashCode automatically. For value-like types, a record is usually a better choice than writing the members by hand.

C# comparison operators

The comparison operators must be overloaded in pairs as well: < with > and <= with >=. They are usually implemented on top of the IComparable<T> interface.

Program.cs
var v1 = Version.Parse("1.10.0");
var v2 = Version.Parse("1.9.3");

Console.WriteLine(v1 > v2);
Console.WriteLine(v1 <= v2);

List<Version> versions = [v1, v2, Version.Parse("0.12.1"), Version.Parse("1.9.12")];
versions.Sort();

Console.WriteLine(string.Join(", ", versions));

record Version(int Major, int Minor, int Patch) : IComparable<Version>
{
    public static Version Parse(string s)
    {
        var parts = s.Split('.').Select(int.Parse).ToArray();
        return new Version(parts[0], parts[1], parts[2]);
    }

    public int CompareTo(Version? other)
    {
        if (other is null) return 1;

        int c = Major.CompareTo(other.Major);
        if (c != 0) return c;

        c = Minor.CompareTo(other.Minor);
        return c != 0 ? c : Patch.CompareTo(other.Patch);
    }

    public static bool operator <(Version a, Version b) => a.CompareTo(b) < 0;
    public static bool operator >(Version a, Version b) => a.CompareTo(b) > 0;
    public static bool operator <=(Version a, Version b) => a.CompareTo(b) <= 0;
    public static bool operator >=(Version a, Version b) => a.CompareTo(b) >= 0;

    public override string ToString() => $"{Major}.{Minor}.{Patch}";
}

We define a simple version type. Comparing version strings as text would give wrong results; "1.10.0" is smaller than "1.9.3" in text order.

$ dotnet run
True
False
0.12.1, 1.9.3, 1.9.12, 1.10.0

The Sort method uses the IComparable<T> implementation; the operators use it too.

C# conversion operators

Conversion operators define how a type is converted to or from another type. An implicit conversion is applied automatically; it should never lose information or throw an exception. An explicit conversion requires a cast.

Program.cs
Celsius c = new(25);
Fahrenheit f = c;

Console.WriteLine(f);

var c2 = (Celsius) new Fahrenheit(98.6);
Console.WriteLine(c2);

double degrees = (double) c2;
Console.WriteLine(degrees);

readonly record struct Celsius(double Degrees)
{
    public static implicit operator Fahrenheit(Celsius c) =>
        new(c.Degrees * 9 / 5 + 32);

    public static explicit operator double(Celsius c) => c.Degrees;

    public override string ToString() => $"{Degrees:F1} °C";
}

readonly record struct Fahrenheit(double Degrees)
{
    public static explicit operator Celsius(Fahrenheit f) =>
        new((f.Degrees - 32) * 5 / 9);

    public override string ToString() => $"{Degrees:F1} °F";
}

We define conversions between temperature scales.

Celsius c = new(25);
Fahrenheit f = c;

The conversion from Celsius to Fahrenheit is implicit.

var c2 = (Celsius) new Fahrenheit(98.6);

The opposite conversion is explicit; we must use a cast. We make it explicit for the purposes of the demonstration.

$ dotnet run
77.0 °F
37.0 °C
37

C# user-defined compound assignment

Since C# 14, a class can define compound assignment operators, such as +=, and the increment and decrement operators as instance methods. They modify the object in place instead of creating a new one, which is useful for mutable types that hold a lot of data.

Program.cs
var c = new Counter();

c += 5;
c++;
c += 10;

Console.WriteLine(c.Value);

class Counter
{
    public int Value { get; private set; }

    public void operator +=(int n)
    {
        Value += n;
    }

    public void operator ++()
    {
        Value++;
    }
}

The operators are instance methods that return void.

$ dotnet run
16

Overloadable operators

The following table summarizes which operators can be overloaded.

OperatorsNote
+x, -x, !x, ~x, ++, --, true, falseunary operators
x + y, x - y, x * y, x / y, x % yarithmetic operators
x & y, x | y, x ^ y, x << y, x >> y, x >>> ybitwise and shift operators
x == y, x != y, x < y, x > y, x <= y, x >= ymust be overloaded in pairs
+=, -=, *=, ...derived from binary operators; can be defined explicitly since C# 14
&&, ||cannot be overloaded directly; evaluated with &, |, true, and false
a[i]defined with indexers
=, ., ?:, ??, =>, new, is, as, typeofcannot be overloaded

Source

Operator overloading

In this article we have overloaded operators for custom types in C#.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.