Operator Overloading
What Is Operator Overloading?
Operator overloading allows you to define custom behavior for standard operators like +, -, *, ==, <, >, [], and []= when they are used with your own classes. Instead of calling a method like vector.add(other), you can write the much more natural vector + other.
In Dart, operators are actually methods with special names. When you write a + b, Dart calls a.+(b) under the hood. By overriding these special methods in your class, you control what happens when an operator is used with your objects.
The operator Keyword
To overload an operator, you define a method using the operator keyword followed by the operator symbol. The method takes the right-hand operand as its parameter (for binary operators) or no parameters (for unary operators like - used as negation).
Basic Syntax
class MyClass {
// Binary operator: a + b
MyClass operator +(MyClass other) {
// return a new MyClass combining this and other
}
// Unary operator: -a
MyClass operator -() {
// return a negated version
}
// Comparison operator: a == b
@override
bool operator ==(Object other) {
// return true if equal
}
// Index operator: a[index]
int operator [](int index) {
// return element at index
}
// Index assignment: a[index] = value
void operator []=(int index, int value) {
// set element at index
}
}
Overridable Operators in Dart
Dart allows you to override the following operators:
- Arithmetic:
+,-,*,/,~/(integer division),%(modulo), unary- - Comparison:
==,<,>,<=,>= - Bitwise:
&,|,^,~(bitwise NOT),<<,>> - Index:
[],[]=
= (assignment), &&, ||, !, is, is!, as, ??, or ?. -- these are built into the language and cannot be customized.Practical Example: Vector2D Class
Let’s build a 2D vector class that supports arithmetic operations. This is one of the most common use cases for operator overloading in game development and graphics programming.
Vector2D with Operator Overloading
class Vector2D {
final double x;
final double y;
const Vector2D(this.x, this.y);
// Vector addition: v1 + v2
Vector2D operator +(Vector2D other) {
return Vector2D(x + other.x, y + other.y);
}
// Vector subtraction: v1 - v2
Vector2D operator -(Vector2D other) {
return Vector2D(x - other.x, y - other.y);
}
// Scalar multiplication: v * scalar
Vector2D operator *(double scalar) {
return Vector2D(x * scalar, y * scalar);
}
// Scalar division: v / scalar
Vector2D operator /(double scalar) {
if (scalar == 0) throw ArgumentError('Cannot divide by zero');
return Vector2D(x / scalar, y / scalar);
}
// Negation: -v
Vector2D operator -() {
return Vector2D(-x, -y);
}
// Equality: v1 == v2
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Vector2D && other.x == x && other.y == y;
}
@override
int get hashCode => Object.hash(x, y);
// Magnitude (length) of the vector
double get magnitude => (x * x + y * y).sqrt();
@override
String toString() => 'Vector2D($x, $y)';
}
void main() {
final v1 = Vector2D(3, 4);
final v2 = Vector2D(1, 2);
print(v1 + v2); // Vector2D(4.0, 6.0)
print(v1 - v2); // Vector2D(2.0, 2.0)
print(v1 * 2); // Vector2D(6.0, 8.0)
print(v1 / 2); // Vector2D(1.5, 2.0)
print(-v1); // Vector2D(-3.0, -4.0)
// Chaining operators
final result = (v1 + v2) * 3;
print(result); // Vector2D(12.0, 18.0)
}
Vector2D instance rather than modifying the existing one. This makes the class immutable, which is a best practice for value objects. Immutable objects are safer in concurrent code and easier to reason about.Practical Example: Money Class
In financial applications, representing money as a class with operator overloading prevents common floating-point errors and enforces currency safety.
Money Class with Safe Arithmetic
class Money {
final int _cents; // Store as cents to avoid floating-point issues
final String currency;
const Money(this._cents, this.currency);
// Factory from dollars
factory Money.fromDollars(double dollars, String currency) {
return Money((dollars * 100).round(), currency);
}
double get dollars => _cents / 100;
// Addition: m1 + m2
Money operator +(Money other) {
_checkCurrency(other);
return Money(_cents + other._cents, currency);
}
// Subtraction: m1 - m2
Money operator -(Money other) {
_checkCurrency(other);
return Money(_cents - other._cents, currency);
}
// Scalar multiplication: money * quantity
Money operator *(int multiplier) {
return Money(_cents * multiplier, currency);
}
// Comparison operators
bool operator <(Money other) {
_checkCurrency(other);
return _cents < other._cents;
}
bool operator >(Money other) {
_checkCurrency(other);
return _cents > other._cents;
}
bool operator <=(Money other) {
_checkCurrency(other);
return _cents <= other._cents;
}
bool operator >=(Money other) {
_checkCurrency(other);
return _cents >= other._cents;
}
// Negation: -money
Money operator -() {
return Money(-_cents, currency);
}
void _checkCurrency(Money other) {
if (currency != other.currency) {
throw ArgumentError(
'Cannot operate on different currencies: $currency vs ${other.currency}',
);
}
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Money && other._cents == _cents && other.currency == currency;
}
@override
int get hashCode => Object.hash(_cents, currency);
@override
String toString() => '${currency} ${dollars.toStringAsFixed(2)}';
}
void main() {
final price = Money.fromDollars(29.99, 'USD');
final tax = Money.fromDollars(2.40, 'USD');
final total = price + tax;
print(total); // USD 32.39
print(price * 3); // USD 89.97
print(price > tax); // true
print(price + tax); // USD 32.39
// Currency mismatch throws an error
final euro = Money.fromDollars(25.00, 'EUR');
// price + euro; // ERROR: Cannot operate on different currencies
}
_checkCurrency method ensures you never accidentally mix USD and EUR in calculations. This is a great example of how operator overloading can enforce business rules at compile time rather than relying on developer discipline.Index Operators: [] and []=
The index operators let your objects behave like lists or maps. This is useful for matrix classes, custom collections, or configuration objects.
Matrix Class with Index Operators
class Matrix {
final List<List<double>> _data;
final int rows;
final int cols;
Matrix(this.rows, this.cols)
: _data = List.generate(rows, (_) => List.filled(cols, 0.0));
Matrix.fromData(this._data)
: rows = _data.length,
cols = _data.isEmpty ? 0 : _data[0].length;
// Access a row: matrix[row]
List<double> operator [](int row) {
if (row < 0 || row >= rows) {
throw RangeError('Row $row is out of range [0, $rows)');
}
return _data[row];
}
// Matrix addition: m1 + m2
Matrix operator +(Matrix other) {
if (rows != other.rows || cols != other.cols) {
throw ArgumentError('Matrices must have the same dimensions');
}
final result = Matrix(rows, cols);
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
result[i][j] = _data[i][j] + other._data[i][j];
}
}
return result;
}
// Scalar multiplication: matrix * scalar
Matrix operator *(double scalar) {
final result = Matrix(rows, cols);
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
result[i][j] = _data[i][j] * scalar;
}
}
return result;
}
@override
String toString() {
return _data.map((row) => row.map((v) => v.toStringAsFixed(1)).join('\t')).join('\n');
}
}
void main() {
final m1 = Matrix.fromData([
[1, 2, 3],
[4, 5, 6],
]);
final m2 = Matrix.fromData([
[7, 8, 9],
[10, 11, 12],
]);
// Access elements
print(m1[0][1]); // 2.0 -- row 0, column 1
// Set elements
m1[0][2] = 99;
print(m1[0][2]); // 99.0
// Matrix addition
final sum = m1 + m2;
print(sum);
// 108.0 10.0 12.0
// 14.0 16.0 18.0
// Scalar multiplication
final scaled = m2 * 2;
print(scaled);
// 14.0 16.0 18.0
// 20.0 22.0 24.0
}
Comparison Operators for Sorting
Overloading comparison operators like < and > makes your objects sortable and usable with Dart’s built-in sorting and comparison utilities.
Temperature Class with Comparisons
class Temperature implements Comparable<Temperature> {
final double celsius;
const Temperature(this.celsius);
factory Temperature.fromFahrenheit(double f) {
return Temperature((f - 32) * 5 / 9);
}
double get fahrenheit => celsius * 9 / 5 + 32;
// Comparison operators
bool operator <(Temperature other) => celsius < other.celsius;
bool operator >(Temperature other) => celsius > other.celsius;
bool operator <=(Temperature other) => celsius <= other.celsius;
bool operator >=(Temperature other) => celsius >= other.celsius;
// Arithmetic: adding/subtracting temperature differences
Temperature operator +(Temperature other) => Temperature(celsius + other.celsius);
Temperature operator -(Temperature other) => Temperature(celsius - other.celsius);
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Temperature && other.celsius == celsius;
}
@override
int get hashCode => celsius.hashCode;
// Required by Comparable interface
@override
int compareTo(Temperature other) => celsius.compareTo(other.celsius);
@override
String toString() => '${celsius.toStringAsFixed(1)}°C';
}
void main() {
final temps = [
Temperature(100),
Temperature(0),
Temperature(37),
Temperature.fromFahrenheit(212),
];
// Sorting works because we implemented Comparable and <
temps.sort();
print(temps); // [0.0°C, 37.0°C, 100.0°C, 100.0°C]
final boiling = Temperature(100);
final freezing = Temperature(0);
print(boiling > freezing); // true
print(boiling == Temperature(100)); // true
}
Comparable interface. This lets your class work with List.sort(), SplayTreeSet, and other standard library utilities that rely on compareTo().Real-World Example: Color Blending
Here is a practical example that shows operator overloading in a Flutter-like context -- a color class that supports blending and manipulation through operators.
Color Class with Operator Overloading
class AppColor {
final int r, g, b;
final double opacity;
const AppColor(this.r, this.g, this.b, [this.opacity = 1.0])
: assert(r >= 0 && r <= 255),
assert(g >= 0 && g <= 255),
assert(b >= 0 && b <= 255),
assert(opacity >= 0.0 && opacity <= 1.0);
// Blend two colors: color1 + color2
AppColor operator +(AppColor other) {
return AppColor(
((r + other.r) / 2).round().clamp(0, 255),
((g + other.g) / 2).round().clamp(0, 255),
((b + other.b) / 2).round().clamp(0, 255),
((opacity + other.opacity) / 2).clamp(0.0, 1.0),
);
}
// Darken: color * factor (0.0 to 1.0)
AppColor operator *(double factor) {
return AppColor(
(r * factor).round().clamp(0, 255),
(g * factor).round().clamp(0, 255),
(b * factor).round().clamp(0, 255),
opacity,
);
}
// Invert: ~color
AppColor operator ~() {
return AppColor(255 - r, 255 - g, 255 - b, opacity);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is AppColor &&
other.r == r &&
other.g == g &&
other.b == b &&
other.opacity == opacity;
}
@override
int get hashCode => Object.hash(r, g, b, opacity);
@override
String toString() => 'AppColor($r, $g, $b, ${opacity.toStringAsFixed(2)})';
}
void main() {
final red = AppColor(255, 0, 0);
final blue = AppColor(0, 0, 255);
// Blend colors
final purple = red + blue;
print(purple); // AppColor(128, 0, 128, 1.00)
// Darken
final darkRed = red * 0.5;
print(darkRed); // AppColor(128, 0, 0, 1.00)
// Invert
final invertedRed = ~red;
print(invertedRed); // AppColor(0, 255, 255, 1.00)
}
+ on your class does not feel like “adding” or “combining”, use a named method instead. Confusing operator semantics make code harder to understand. A good rule: if you have to explain what the operator does, use a method name.operator keyword. It makes classes like vectors, money, matrices, and colors more natural to use. Always return new immutable instances, validate inputs, and only overload operators when the meaning is clear and intuitive.