Operator Overloading #
C# supports operator overloading. Operators allow for a more concise notation of operations. They should not be overused - we use them mainly when it is intuitive and does not raise doubts, e.g., for our own numeric types. An operator should do what we would expect it to do.
Example #
Operators are defined as public, static members of a type. The method name must be the operation symbol preceded by the operator keyword. Depending on the operation, the method takes one or two parameters.
public struct Complex
{
public double Real { get; }
public double Imaginary { get; }
public Complex(double real, double imaginary)
{
Real = real;
Imaginary = imaginary;
}
public static Complex operator +(Complex a, Complex b)
{
return new Complex(a.Real + b.Real, a.Imaginary + b.Imaginary);
}
public static Complex operator -(Complex a, Complex b)
{
return new Complex(a.Real - b.Real, a.Imaginary - b.Imaginary);
}
public static Complex operator -(Complex a)
{
return new Complex(-a.Real, -a.Imaginary);
}
public static Complex operator *(Complex a, Complex b)
{
return new Complex(
a.Real * b.Real - a.Imaginary * b.Imaginary,
a.Real * b.Imaginary + a.Imaginary * b.Real
);
}
public static bool operator ==(Complex a, Complex b)
{
return a.Real == b.Real && a.Imaginary == b.Imaginary;
}
public static bool operator !=(Complex a, Complex b)
{
return !(a == b);
}
public override bool Equals(object obj)
{
return obj is Complex complex && complex == this;
}
public override int GetHashCode()
{
return HashCode.Combine(Real, Imaginary);
}
}- Some operators must be implemented in pairs:
==and!=,<and>,<=and>= - Compound assignment operators (e.g.,
+=,/=) are implicitly overloaded by overloading their corresponding non-compound operators. From C# 14, it will be possible to define them explicitly. - When overloading the
==operator, you must also override theEqualsandGetHashCodemethods, otherwise the compiler will issue a warning. - It is also worth implementing the
IEquatable<T>andIComparable<T>interfaces. In .NET 7 (C# 11) and newer, when overloading arithmetic operators, it is worth considering the implementation of generic interfaces, e.g.,IAdditionOperators<TSelf, TOther, TResult>,IMultiplyOperators<TSelf, TOther, TResult>, etc. This will allow us to later constrain the generic type, and it is also a form of code documentation.
Operator Inheritance #
Operators defined in classes are not inherited, but operators defined in interfaces as static abstract or static virtual are.
public interface IIncrementable<TSelf> where TSelf : IIncrementable<TSelf>
{
public static abstract TSelf operator ++(TSelf self);
}Defining Conversions #
You can define your own conversions for a type, both explicit and implicit.
- Implicit conversions should always be safe, not cause loss of information, and not throw exceptions.
- Explicit conversions may fail (e.g., by throwing an exception) or may cause loss of information - they should require a conscious decision from the programmer (using a cast).
public struct Complex
{
public double Real { get; }
public double Imaginary { get; }
public Complex(double real, double imaginary)
{
Real = real;
Imaginary = imaginary;
}
public static implicit operator Complex(double d)
{
return new Complex(d, 0.0);
}
public static explicit operator double(Complex c)
{
return c.Real;
}
}Usage:
Complex c = new Complex(3.0, 4.0);
double d = (double)c; // explicit conversion
c = 5.0; // implicit conversion