Перегрузка операторов

Язык Kotlin позволяет нам реализовывать предопределённый набор операторов для наших типов. Эти операторы имеют фиксированное символическое представление (вроде + или *) и фиксированные приоритеты. Для реализации оператора мы предоставляем функцию-член или функцию-расширение с фиксированным именем и с соответствующим типом, т. е. левосторонним типом для бинарных операций или типом аргумента для унарных оперций. Функции, которые перегружают операторы, должны быть отмечены модификатором operator.

Далее мы опишем соглашения, которые регламентируют перегрузку операторов для разных типов операторов.

Унарные операторы

Унарные префиксные операторы

Выражение Транслируется в
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()

Эта таблица демонстрирует, что когда компилятор обрабатывает, к примеру, выражение +a, он оcуществляет следующие действия:

  • Определяется тип выражения a, пусть это будет T.
  • Смотрится функция unaryPlus() с модификатором operator без параметров для приёмника типа Т, т. е. функция-член или функция расширения.
  • Если функция отсутствует или неоднозначная, то это ошибка компиляции.
  • Если функция присутствует и её возвращаемый тип есть R, выражение +a имеет Тип R.

Примечание: эти операции, как и все остальные, оптимизированы для основных типов и не вносят накладных расходов на вызовы этих функций для них.

Например, вы можете перегрузить оператор унарного минуса:

data class Point(val x: Int, val y: Int)

operator fun Point.unaryMinus() = Point(-x, -y)

val point = Point(10, 20)
println(-point)  // выведет "(-10, -20)"

Инкремент и декремент

Выражение Транслируется в
a++ a.inc() + see below
a-- a.dec() + see below

Функции inc() и dec() должны возвращать значение, которое будет присвоено переменной, к которой была применёна операция ++ или -- . Они не должны пытаться изменять сам объект, для которого inc или dec были вызваны.

Компилятор осуществляет следующие шаги для разрешения операторов в постфиксной форме, например для a++:

  • Определяется тип переменной a, пусть это будет T.
  • Смотрится функция inc() с модификатором operator без параметров, применимая для приёмника типа Т.
  • Проверяется, что возвращаемый тип такой функции является подтипом T.

Эффектом вычисления будет:

  • Загружается инициализирующее значение a во временную переменную a0,
  • Результат выполнения a.inc() сохраняется в a,
  • Возвращается a0 как результат вычисления выражения (т.е. значение до инкремента).

Для a-- шаги выполнения полностью аналогичные.

Для префиксной формы ++a или --a разрешение работает подобно, но результатом будет:

  • Присвоение результата вычисления a.inc() непосредственно a,
  • Возвращается новое значение a как общий результат вычисления выражения.

Бинарные операции

Арифметические операции

Выражение Транслируется в
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b), a.mod(b) (устаревшее)
a..b a.rangeTo(b)

Для перечисленных в таблице операций компилятор всего лишь разрешает выражение из колонки Транслируется в.

Отметим, что операция rem поддерживается только начиная с Kotlin 1.1. Kotlin 1.0 использует только операцию mod, которая отмечена как устаревшая в Kotlin 1.1.

Пример

Ниже приведен пример класса Counter, начинающего счёт с заданного значения, которое может быть увеличено с помощью перегруженного оператора +.

data class Counter(val dayIndex: Int) {
    operator fun plus(increment: Int): Counter {
        return Counter(dayIndex + increment)
    }
}

Оператор in

Выражение Транслируется в
a in b b.contains(a)
a !in b !b.contains(a)

Для операций in и !in используется одна и та же процедура, только возвращаемый результат инвертируется.

Оператор доступа по индексу

Выражение Транслируется в
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, ..., i_n] a.get(i_1, ..., i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, ..., i_n] = b a.set(i_1, ..., i_n, b)

Квадратные скобки транслируются в вызов get или set с соответствующим числом аргументов.

Оператор вызова

Выражение Транслируется в
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ..., i_n) a.invoke(i_1, ..., i_n)

Оператор вызова (функции, метода) в круглых скобках транслируется в invoke с соответствующим числом аргументов.

Присвоения с накоплением

Выражение Транслируется в
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.modAssign(b)

Для присваивающих операций, таких как a += b, компилятор осуществляет следующие шаги:

  • Если функция из правой колонки таблицы доступна
    • Если соответствующая бинарная функция (т.е. plus() для plusAssign()) также доступна, то фиксируется ошибка (неоднозначность).
    • Проверяется, что возвращаемое значение функции Unit, в противном случае фиксируется ошибка.
    • Генерируется код для a.plusAssign(b)
  • В противном случае делается попытка сгенерировать код для a = a + b (при этом включается проверка типов: тип выражения a + b должен быть подтипом a).

Отметим: присвоение НЕ ЯВЛЯЕТСЯ выражением в Kotlin.

Операторы равенства и неравенства

Выражение Транслируется в
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))

Отметим: операции === и !== (проверка идентичности) являются неперегружаемыми, поэтому не приводятся никакие соглашения для них.

Операция == имеет специальный смысл: она транслируется в составное выражение, в котором экранируются значения null. null == null - это всегда истина, а x == null для ненулевых x - всегда ложь, и не должно расширяться в x.equals().

Операторы сравнений

Выражение Транслируется в
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

Все сравнения транслируются в вызовы compareTo, от которых требуется возврат значения типа Int.

Инфиксные вызовы именованных функций

Мы можем моделировать вручную инфиксные операции использованием infix function calls.