- Home
- .NET tutorials
- C# ToString(), operators and indexers - Are you using these?
C# ToString(), operators and indexers - Are you using these?
Published: Monday 13 July 2026
Get version-accurate .NET & C# AI answers.
Most C# developers don't know you can override ToString(), operators and indexers in classes.
Overriding ToString()
By default, calling ToString() on a class outputs the full type name. Something like YourNamespace.Money. That is rarely useful.
You can override it to return something meaningful instead. In the case of a Money class, a good approach is to format the amount with the currency symbol.
// Money.cs
public class Money
{
public decimal Amount { get; private set; }
public string CurrencyCode { get; private set; }
public string CurrencySymbol { get; private set; }
public Money(decimal amount, string currencyCode, string currencySymbol)
{
Amount = amount;
CurrencyCode = currencyCode;
CurrencySymbol = currencySymbol;
}
public override string ToString()
{
return $"{CurrencySymbol}{Amount:0.00}";
}
}Creating an instance and calling ToString() now returns a readable string such as $100.00 rather than the type name. This is particularly useful when printing to the console or writing to logs.
Overloading operators
As well as ToString(), you can also overload operators in a class. This lets you define what happens when you use symbols like +, -, == and != on your own types.
Addition and subtraction operators
To add overloaded + and - operators to the Money class, you use the operator keyword followed by the symbol:
// Money.cs
public class Money
{
...
public static Money operator+ (Money a, Money b)
{
return new Money(a.Amount + b.Amount, a.CurrencyCode, a.CurrencySymbol);
}
public static Money operator -(Money a, Money b)
{
return new Money(a.Amount - b.Amount, a.CurrencyCode, a.CurrencySymbol);
}
}One thing worth bearing in mind. When you implement + and -, you might be tempted to add the amounts together regardless of currency. But adding £10 to $25 does not really make sense. You could throw an exception if the currency codes do not match, to protect against that.
With these operators in place, you can add and subtract Money instances directly:
var cost = new Money(100, "USD", "$");
var cost2 = new Money(40, "USD", "$");
Console.WriteLine(cost + cost2); // $140.00
Console.WriteLine(cost - cost2); // $60.00Equality operators
You can also overload the == and != operators to compare two Money instances by value:
// Money.cs
public class Money
{
...
public static bool operator == (Money a, Money b)
{
if (a.Amount == b.Amount
&& a.CurrencyCode == b.CurrencyCode
&& a.CurrencySymbol == b.CurrencySymbol)
{
return true;
}
return false;
}
public static bool operator !=(Money a, Money b)
{
if (a.Amount != b.Amount
|| a.CurrencyCode != b.CurrencyCode
|| a.CurrencySymbol != b.CurrencySymbol)
{
return true;
}
return false;
}
}You can simplify != to just return !(a == b), which delegates to your == operator so you are not duplicating logic:
// Money.cs
public class Money
{
...
public static bool operator == (Money a, Money b)
{
if (a.Amount == b.Amount
&& a.CurrencyCode == b.CurrencyCode
&& a.CurrencySymbol == b.CurrencySymbol)
{
return true;
}
return false;
}
public static bool operator !=(Money a, Money b)
{
return !(a == b);
}
}Do not do it the other way round as well, where == also calls !=. You will end up with the two operators calling each other in a loop, which causes a StackOverflow exception and crashes your application.
Overriding Equals and GetHashCode
When you use == and != operators, it is recommended that you also override Equals and GetHashCode.
Without overriding Equals, C# compares by reference, so two separate Money objects with identical values will not be equal, because they are different instances in memory. Overriding it lets you compare by value instead:
// Money.cs
public class Money
{
...
public override bool Equals(object? obj)
{
if (obj is not Money money)
{
return false;
}
return Amount == money.Amount && CurrencyCode == money.CurrencyCode &&
money.CurrencySymbol == CurrencySymbol;
}
}If you override Equals, you must also override GetHashCode. The rule is if two objects are considered equal, they must return the same hash code. If you do not, things break in dictionaries and hash sets. Your objects will not be found even when they should be. HashCode.Combine is the cleanest way to do this:
// Money.cs
public class Money
{
...
public override int GetHashCode()
{
return HashCode.Combine(Amount, CurrencyCode, CurrencySymbol);
}
}Comparison operators
You can also define less than, greater than and their equivalents. If you define one, you must also define the other. For example, if you add < you must also add >:
// Money.cs
public class Money
{
...
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;
}
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;
}
}The same pairing rule applies to == and !=. If you define one, the compiler requires you to define the other.
Indexers
An indexer allows instances of a class to be accessed using square bracket syntax, in the same way you would access an array or a list.
Here is a BankAccount class that holds a list of Money instances and exposes three indexers:
// BankAccount.cs
public class BankAccount
{
public IList<Money> Money { get; } = [];
public void Add(Money money)
{
Money.Add(money);
}
public Money this[int index]
{
get
{
return Money[index];
}
}
public IList<Money> this[string currencyCode]
{
get
{
return [.. Money.Where(x => x.CurrencyCode == currencyCode)];
}
}
public IList<Money> this[string currencyCode, decimal amount]
{
get
{
return [.. Money.Where(x => x.CurrencyCode == currencyCode &&
x.Amount == amount)];
}
}
}You can define multiple indexers on the same class, as long as each one has a different parameter signature. The compiler tells them apart by the parameter type, not the return type. In the example above, we have an indexer that takes an int to look up by position, one that takes a string to filter by currency code, and one that takes both a string and a decimal to filter by currency code and amount.
Using the BankAccount class looks like this:
var account = new BankAccount();
account.Add(new Money(100, "USD", "$"));
account.Add(new Money(40, "USD", "$"));
Console.WriteLine(account[1]); // $40.00
var usdAmounts = account["USD"];
foreach (var money in usdAmounts)
{
Console.WriteLine(money); // $100.00, then $40.00
}Watch the video
Watch the video where we demonstrate these C# features and how you can add them to your class.
Related tutorials