Operators are a fundamental aspect of programming, and they are used to perform various actions, such as addition, division, multiplication, etc. However, it is also possible to expand the capabilities of these operators by implementing them for custom types in our code through “Operator Overloading”. While operators are typically used with basic data types, such as integers, longs, decimals, and booleans, this technique allows us to create custom operator logic for custom data types. so, let’s explore how to put this into practice in real-world scenarios.
Operator Overloading
In C#, this technique allows developers to redefine the behavior of operators when they are used with custom data types. It enables you to override the default implementation of an operator for a given class or struct.
For example, you can overload the “+” operator for a custom class, such that it performs a custom logic that defines the concatenation of two instances.
As you might expect, this feature can help you write more readable and expressive code, as well as simplify certain operations on custom data types.
in C#, as stated in the documentation you can override the following operators:
Syntax
The syntax for overriding an operator is simple and straightforward, and it is well documented on their website. once you select the class/struct to which you want to apply the operator for it, in its body, you declare a function with a special syntax:
- first, you start with the “public” keyword.
- then followed by the “static” keyword.
- then you specify the desired output type, the result of the operator logic.
- after that, we use the “operator” keyword
- then you set the operator you want to override
- followed then by at least one parameter
Note: one parameter must have the type of the class/struct you are using to override the operator for
Practical example
Here are two examples of how I have frequently used “operator overloading” to greatly simplify my code
1- Discounts
if you are working with a domain model that includes a “Discount” value object you can leverage the technique to make your code clear.
let’s say you have a Discount model like this:
/// <summary>
/// the discount model
/// </summary>
public readonly struct Discount
{
public Discount(decimal value, ValueType type)
{
Value = value;
Type = type;
}
/// <summary>
/// the value of the discount
/// </summary>
public decimal Value { get; }
/// <summary>
/// the type of the discount
/// </summary>
public ValueType Type { get; }
/// <summary>
/// calculate the amount of the discount base on the given amount, and return the calculated value,
/// </summary>
/// <param name="amount">the amount to calculate the discount for</param>
public float Calculate(decimal amount)
=> Type switch
{
ValueType.AMOUNT => Value,
_ => amount * Value / 100,
};
/// <summary>
/// create a discount with a % value
/// </summary>
/// <param name="value">the discount value</param>
/// <returns>the discount instance</returns>
public static Discount Get(decimal value)
=> Get(value, ValueType.PERCENTAGE);
/// <summary>
/// create a discount with a % value
/// </summary>
/// <param name="value">the discount value</param>
/// <returns>the discount instance</returns>
public static Discount Get(decimal value, ValueType type)
=> new(value, type);
}
/// <summary>
/// the type of a value
/// </summary>
public enum ValueType : byte
{
/// <summary>
/// a percentage value
/// </summary>
PERCENTAGE,
/// <summary>
/// an amount
/// </summary>
AMOUNT,
}
and normally you will use it as follow
// you will have the amount and discount stored somewhere
var amount = 1200m;
var discount = Discount.Get(10);
// then you calculate the discount and reduce its value from the amount
var discountValue = discount.Calculate(amount); // 120
var totalToPay = amount - discountValue; // 1200 - 120 = 1080
as you can see here we first calculate the discount value on the amount and then we reduce it from the amount value.
now let’s override the “-” operator for the discount type, so on the Discount type we will add the following code:
after adding this code, we will be able to modify the previous usage of the discount type to be as follow
// you will have the amount and discount stored somewhere
var amount = 1200m;
var discount = Discount.Get(10);
// then you reduce the discount from the amount directly
var totalToPay = amount - discount; // 1080
now it more clear & clean, as you can see on the operator logic we have added a check for the possibility of the discount value being null, and in case of the amount is negative we can ignore the calculation and more.
2- Taxes
another example, is taxes, you can also use the same technique to make your code more readable, let’s see the following:
/// <summary>
/// the Tax model
/// </summary>
public readonly struct Tax
{
public Tax(string name, decimal value, ValueType type)
{
Name = name;
Value = value;
Type = type;
}
/// <summary>
/// the name of the TAX
/// </summary>
public string Name { get; }
/// <summary>
/// the value of the Tax
/// </summary>
public decimal Value { get; }
/// <summary>
/// the type of the Tax
/// </summary>
public ValueType Type { get; }
/// <summary>
/// this function will calculate the amount of the Tax base on the given amount, and return the calculated value,
/// </summary>
/// <param name="amount">the amount to calculate the Tax for</param>
public decimal Calculate(decimal amount)
=> Type switch
{
ValueType.AMOUNT => Value,
_ => amount * Value / 100,
};
/// <summary>
/// create a Tax with a % value
/// </summary>
/// <param name="value">the Tax value</param>
/// <returns>the Tax instance</returns>
public static Tax Of(string name, decimal value)
=> Of(name, value, ValueType.PERCENTAGE);
/// <summary>
/// create a Tax with a % value
/// </summary>
/// <param name="value">the Tax value</param>
/// <returns>the Tax instance</returns>
public static Tax Of(string name, decimal value, ValueType type)
=> new(name, value, type);
public static decimal operator +(decimal amount, Tax? tax)
{
if (amount <= 0 || tax is null)
return amount;
return amount + tax.Value.Calculate(amount);
}
}
then we will use it as follow:
// you will have the amount and Tax stored somewhere
var amount = 1200m;
var tax = Tax.Of("VAT", 20);
// then you add the tax directly to the amount
var totalToPay = amount + tax; // 1440
In conclusion, when working with types that define a value, such as a discount or tax type, it can be beneficial to use operator overloading to simplify the logic surrounding arithmetic operations. This technique will allow you to redefine the behavior of operators when they are used with these custom types, making the code more readable and expressive. With the ability to define different overloads for different operators and types to support variety of cases.
Overall, I highly recommend using operator overloading whenever possible as it can greatly reduce code complexity and make it more understandable.