skip to content

In Dart, which operators can a class define, and how would you declare + and [] on a coordinate value type?

level: middleimportance: nice to knowfreq 28%

answer

  1. operators are instance methods
  2. the operator keyword
  3. != is built in
  4. += comes free from +
  5. unary minus is operator -()

basics

~10 s

Arithmetic, bitwise, relational, ==, [] and []= are declared as methods with the operator keyword, such as Coordinate operator +(Coordinate other). !=, &&, ||, ?? and ! are built in, and += reuses +.

solid answer

~40 s

Most Dart operators are instance methods with special names, declared with `operator`: `Coordinate operator +(Coordinate other) => Coordinate(x + other.x, y + other.y);` and `double operator [](int index) => ...`. The definable set is `<`, `>`, `<=`, `>=`, `==`, `~`, `-`, `+`, `/`, `~/`, `*`, `%`, `|`, `^`, `&`, `<<`, `>>>`, `>>`, `[]` and `[]=`. Unary minus is `operator -()` with no parameters and can coexist with a binary `operator -(Coordinate other)`. `!=` is always `!(a == b)`, the short-circuiting `&&`, `||` and `??` and prefix `!` cannot be defined, and precedence and associativity are fixed by the language. Compound assignment desugars, so `p += q` means `p = p + q`. Define `[]=` only on mutable types, and overriding `==` still obliges you to override `hashCode`.

code

dart · 25 lines
dart
class Coordinate {
  const Coordinate(this.x, this.y);

  final double x;
  final double y;

  Coordinate operator +(Coordinate other) => Coordinate(x + other.x, y + other.y);
  Coordinate operator -(Coordinate other) => Coordinate(x - other.x, y - other.y);
  Coordinate operator -() => Coordinate(-x, -y); // unary minus
  Coordinate operator *(double factor) => Coordinate(x * factor, y * factor);

  double operator [](int index) => switch (index) {
        0 => x,
        1 => y,
        _ => throw RangeError.range(index, 0, 1, 'index'),
      };
}

void main() {
  var p = const Coordinate(1, 2);
  p += const Coordinate(3, 4); // p = p + Coordinate(3, 4)
  assert(p[0] == 4 && p[1] == 6);
  assert((-p)[1] == -6);
  assert((p * 0.5)[0] == 2);
}

go deeper

for a junior

Recall that operators are declared with the operator keyword and that + on a value type should return a new instance.

for a middle

Explain which operators are definable, why != and += are not, and how unary minus is declared next to binary minus.

for a senior

Judge when operator overloading improves an API and when it hides mutation, surprising precedence or broken hash behaviour.

for a principal

Set conventions for operator use across shared value types, such as only arithmetic-like operators and always immutable results.

## Operators are methods In Dart, most operators are **instance methods with special names**. `a + b` invokes the `+` method on `a` with `b` as the argument, exactly like `a.plus(b)` would, which is why `int`, `double`, `String`, `Duration` and `DateTime` can all respond to `+` differently. You declare one with the built-in identifier `operator` followed by the symbol: ```dart Coordinate operator +(Coordinate other) => Coordinate(x + other.x, y + other.y); ``` ## The definable set | Group | Operators | |---|---| | Relational and equality | `<` `>` `<=` `>=` `==` | | Arithmetic | `+` `-` `*` `/` `~/` `%` | | Bitwise and shift | `&` `\|` `^` `~` `<<` `>>` `>>>` | | Indexing | `[]` `[]=` | Everything else is built in and cannot be declared: - `!=` is always evaluated as `!(a == b)`, so defining `==` gives you both. - `&&`, `||` and `??` short-circuit, so they cannot be ordinary method calls. - Prefix `!`, the conditional `?:`, the cascade `..`, null-aware `?.` and the type operators `is` and `as` are part of the grammar. ## Arity and special forms 1. **Binary operators** take exactly one parameter: `operator *(double factor)`. 2. **Unary minus** is written `operator -()` with **no parameters**. It is a separate member from binary `-`, so a class may have both, as `int` does. Internally its name is `unary-`, exposed as `Symbol.unaryMinus`, but you never write that name in a declaration. 3. **Bitwise complement** `~` is also unary: `operator ~()`. 4. **`[]`** takes one parameter, the index or key: `double operator [](int index)`. 5. **`[]=`** takes two, the index and the value, and is conventionally declared to return `void`; the value of `a[i] = v` as an expression is `v` regardless. ## Compound assignment and increment come for free `p += q` is **not** a separate operator. It desugars to `p = p + q`, and it works for any variable whose static type defines `+`. The same holds for `-=`, `*=`, `~/=`, `<<=` and the rest, and `p[i] += 1` uses both `[]` and `[]=`. Because the result is **assigned back**, `+=` on an immutable value type rebinds the variable to a new object instead of mutating the old one, so the variable cannot be `final`. ## Fixed precedence Declaring an operator does not let you change how it parses. `a + b * c` is always `a + (b * c)`, whatever the classes do. Pick operators whose built-in precedence matches the meaning; overloading `^` to mean "power" reads naturally but binds more loosely than `*`, which surprises readers. ## Relational operators and sorting Defining `<` and `>` does not make a class sortable. `List.sort()` without a comparator expects elements that implement `Comparable` and calls `compareTo`; it never calls your `<`. If a type should both read naturally in comparisons and sort, implement `Comparable<T>` with `compareTo` and define the relational operators in terms of it, so the two can never disagree. The same caution applies to `==`: a `compareTo` that returns `0` for objects that are not `==` confuses sorted collections that treat "compares as zero" as "the same element". ## Design guidance - **Match the mathematical meaning.** `+` for vector addition, `*` by a scalar and unary `-` for negation are expected; `+` meaning "append to a log" is not. - **Keep value types immutable.** Return a new instance from `+` rather than mutating `this`, and skip `[]=` on a class used as a map key or set element; mutating it after insertion breaks hash lookups. - **Return the right type.** `operator ==` must return `bool`. Comparison operators should agree with `==` and with `compareTo` if the class implements `Comparable`. - **Parameter types follow override rules.** When you override an inherited operator, such as `==` from `Object`, the parameter may not be narrowed; declare `operator ==(Object other)` and type-test inside. - **Remember `hashCode`.** `==` is on the definable list, and overriding it without `hashCode` breaks hash-based collections.

  • Can you overload != in Dart to behave differently from ==?
    No. `!=` is not a method; the language evaluates `a != b` as `!(a == b)`. That guarantees the two can never disagree, and it is why the definable list includes `==` but not `!=`.
  • Why does `p += q` fail to compile when p is declared final, even though Coordinate's + returns a new object?
    Compound assignment desugars to `p = p + q`, an assignment to `p` itself. A `final` variable cannot be reassigned, so the compiler rejects it even though `+` never mutates the original object.

saying these in an interview costs you the question

  • You can define != separately to optimise inequality checks.
  • += must be declared as its own operator method.
  • Unary minus needs a special operator name in the declaration.
  • Overloading an operator lets you change its precedence.
  • && and || can be overloaded like any other operator.