Operator Overloading in C#
Vaibhav • September 11, 2025
In previous chapters, we’ve explored how to define methods, work with classes, and build expressive logic using
control flow and data types. As you start designing your own types - especially ones that represent mathematical
or logical entities - you may want them to behave like built-in types. For example, wouldn’t it be nice if your
Vector
class could use the +
operator to add
two vectors? That’s exactly what operator overloading allows you to do.
In this article, we’ll explore what operator overloading is, how it works in C#, how to implement it in your own types, and how to use it responsibly. We’ll build on concepts you already know - like methods, classes, and value types - and show how operator overloading can make your code more intuitive, readable, and powerful.
What Is Operator Overloading?
Operator overloading lets you redefine the behavior of operators (like +
, -
, ==
, <
)
for your own types. Instead of writing verbose method calls like Add(v1, v2)
,
you can write v1 + v2
- just like you would with integers or doubles.
This makes your types feel natural and consistent with the rest of the language. It’s especially useful for mathematical types (vectors, matrices, complex numbers), domain models (money, dates), and custom logic (permissions, flags).
Operator overloading is syntactic sugar. Under the hood, it’s just a static method with a special name and signature.
Basic Syntax of Operator Overloading
To overload an operator, you define a public static method using the operator
keyword. The method must take the correct number and type of parameters,
and return the appropriate result.
public static ReturnType operator OperatorSymbol(Type1 operand1, Type2 operand2)
{
// logic
}
For example, to overload the +
operator for a Vector
class:
public class Vector
{
public int X { get; }
public int Y { get; }
public Vector(int x, int y)
{
X = x;
Y = y;
}
public static Vector operator +(Vector a, Vector b)
{
return new Vector(a.X + b.X, a.Y + b.Y);
}
}
Now you can write:
Vector v1 = new(2, 3);
Vector v2 = new(4, 1);
Vector result = v1 + v2; // result is (6, 4)
This feels natural and mirrors how you’d add numbers - but it works with your custom type.
Supported Operators
C# allows you to overload many operators, including:
- Arithmetic:
+
,-
,*
,/
,%
- Comparison:
==
,!=
,<
,>
,<=
,>=
- Unary:
+
,-
,!
,~
- Increment/Decrement:
++
,--
- Logical:
&&
,||
(with conditions)
You cannot overload assignment (=
), member access (.
), or conditional (?:
) operators.
Overloading Equality Operators
Overloading ==
and !=
requires extra care. You
must also override Equals()
and GetHashCode()
to ensure consistency.
public class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
public static bool operator ==(Point a, Point b)
{
return a.X == b.X && a.Y == b.Y;
}
public static bool operator !=(Point a, Point b)
{
return !(a == b);
}
public override bool Equals(object obj)
{
return obj is Point p && this == p;
}
public override int GetHashCode()
{
return HashCode.Combine(X, Y);
}
}
This ensures that equality works consistently across operators, methods, and collections.
Always override Equals()
and GetHashCode()
when overloading ==
and !=
.
Unary Operators
You can also overload unary operators like -
and !
. For example:
public class Score
{
public int Value { get; }
public Score(int value)
{
Value = value;
}
public static Score operator -(Score s)
{
return new Score(-s.Value);
}
}
Now you can write:
Score s = new(10);
Score neg = -s; // neg.Value == -10
This mirrors how unary minus works with numbers - but applies to your custom type.
Increment and Decrement Operators
You can overload ++
and --
to support
incrementing and decrementing:
public class Counter
{
public int Value { get; private set; }
public Counter(int value)
{
Value = value;
}
public static Counter operator ++(Counter c)
{
return new Counter(c.Value + 1);
}
public static Counter operator --(Counter c)
{
return new Counter(c.Value - 1);
}
}
This lets you write:
Counter c = new(5);
c = ++c; // c.Value == 6
Note that these operators return a new instance - not modify the original. This preserves immutability.
Operator Overloading and Immutability
Most operator overloads return a new instance of the type - rather than modifying the original. This aligns with functional programming principles and avoids side effects.
For example, when you write v1 + v2
, you expect a new vector - not a mutated
one. This makes your code safer and easier to reason about.
Using Operator Overloading in Practice
Let’s build a simple Money
type that supports addition and comparison:
public class Money
{
public decimal Amount { get; }
public Money(decimal amount)
{
Amount = amount;
}
public static Money operator +(Money a, Money b)
{
return new Money(a.Amount + b.Amount);
}
public static bool operator >(Money a, Money b)
{
return a.Amount > b.Amount;
}
public static bool operator <(Money a, Money b)
{
return a.Amount < b.Amount;
}
}
Now you can write:
Money m1 = new(100);
Money m2 = new(50);
Money total = m1 + m2; // total.Amount == 150
if (m1 > m2)
Console.WriteLine("m1 is greater");
This makes your domain logic more expressive and readable.
Common Mistakes and How to Avoid Them
Operator overloading is powerful - but it can be misused. Here are some common mistakes:
- Overloading operators with unexpected behavior - which confuses users.
- Mutating objects instead of returning new ones - which breaks immutability.
- Forgetting to override
Equals()
andGetHashCode()
- which causes bugs in collections. - Overloading too many operators - which makes the type hard to understand.
Use operator overloading when it improves clarity - not just because you can.
The string
type overloads the +
operator
to support concatenation - that’s why you can write "Hello" + "World"
.
Design Tips for Operator Overloading
Operator overloading is a design decision. Use it to make your types feel natural and intuitive:
- Use it for mathematical or logical types.
- Keep behavior consistent with built-in types.
- Document your overloads clearly.
- Avoid overloading operators with non-obvious behavior.
These tips help you write clean, maintainable code - especially in libraries and shared APIs.
Summary
Operator overloading lets you define custom behavior for operators like +
,
==
, and <
in your own types. You’ve learned
how to define operator overloads using static methods, how to use them with arithmetic, comparison, and unary
operators, how to preserve immutability, and how to avoid common mistakes.
By using operator overloading thoughtfully, you make your types more expressive, intuitive, and consistent with the rest of the language. In the next article, we’ll explore Indexers - a feature that lets you use array-like syntax to access elements in your own types.