Перегрузка оператора в Котлине

Перегрузка оператора в Котлине

1. обзор

В этом руководстве мы поговорим о соглашениях, которые Kotlin предоставляет для поддержки перегрузки операторов.

2. Ключевое словоoperator

В Java операторы привязаны к конкретным типам Java. Например,String и числовые типы в Java могут использовать оператор + для объединения и сложения соответственно. Ни один другой тип Java не может повторно использовать этот оператор для собственной выгоды. Kotlin, напротив, предоставляет набор соглашений для поддержки ограниченногоOperator Overloading.

Начнем с простогоdata class:

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

Мы собираемся расширить этот класс данных с помощью нескольких операторов.

Чтобы превратить функцию Kotlin с предопределенным именем в операторwe should mark the function with the operator modifier., например, мы можем перегрузить оператор“+”:

operator fun Point.plus(other: Point) = Point(x + other.x, y + other.y)

Таким образом, мы можем добавить дваPoints к“+”:

>> val p1 = Point(0, 1)
>> val p2 = Point(1, 2)
>> println(p1 + p2)
Point(x=1, y=3)

3. Перегрузка для унарных операций

Unary operations are those that work on just one operand. Например,-a, a++ или!a - это унарные операции. Как правило, функции, которые собираются перегружать унарные операторы, не принимают параметров.

3.1. Унарий Плюс

Как насчет создания какого-нибудьShape с несколькимиPoints:

val s = shape {
    +Point(0, 0)
    +Point(1, 1)
    +Point(2, 2)
    +Point(3, 4)
}

В Kotlin это вполне возможно с операторной функциейunaryPlus.

ПосколькуShape - это просто наборPoints, тогда мы можем написать класс, обернув несколькоPoints с возможностью добавления дополнительных:

class Shape {
    private val points = mutableListOf()

    operator fun Point.unaryPlus() {
        points.add(this)
    }
}

И обратите внимание, что синтаксисshape \{…} дал нам использованиеLambda сReceivers:

fun shape(init: Shape.() -> Unit): Shape {
    val shape = Shape()
    shape.init()

    return shape
}

3.2. Унарный минус

Предположим, у нас естьPoint с именем“p”, и мы собираемся отменить его координацию, используя что-то вроде“-p”. Затем все, что нам нужно сделать, это определить операторную функцию с именемunaryMinus наPoint:

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

Затем, каждый раз, когда мы добавляем префикс“-“ перед экземпляромPoint, компилятор переводит его в вызов функцииunaryMinus:

>> val p = Point(4, 2)
>> println(-p)
Point(x=-4, y=-2)

3.3. инкремент

Мы можем увеличить каждую координату на единицу, просто реализовав операторную функцию с именемinc:

operator fun Point.inc() = Point(x + 1, y + 1)

Постфиксный оператор“++” сначала возвращает текущее значение, а затем увеличивает значение на единицу:

>> var p = Point(4, 2)
>> println(p++)
>> println(p)
Point(x=4, y=2)
Point(x=5, y=3)

Напротив, оператор префикса“++” сначала увеличивает значение, а затем возвращает новое увеличенное значение:

>> println(++p)
Point(x=6, y=4)

Такжеsince the “++” operator re-assigns the applied variable, we can’t use val with them.

3.4. декремент

Как и при увеличении, мы можем уменьшить каждую координату, реализовав операторную функциюdec:

operator fun Point.dec() = Point(x - 1, y - 1)

dec также поддерживает знакомую семантику для операторов до и после декремента, как и для обычных числовых типов:

>> var p = Point(4, 2)
>> println(p--)
>> println(p)
>> println(--p)
Point(x=4, y=2)
Point(x=3, y=1)
Point(x=2, y=0)

Также, как++ we нельзя использовать сvals _._

3.5. Not

Как насчет того, чтобы перевернуть координаты только на!p? We can do this with not:

operator fun Point.not() = Point(y, x)

Проще говоря, компилятор переводит любой“!p” в вызов функции унарной операторной функции“not”:

>> val p = Point(4, 2)
>> println(!p)
Point(x=2, y=4)

4. Перегрузка для двоичных операций

Binary operators, as their name suggests, are those that work on two operands. Итак, функции, перегружающие бинарные операторы, должны принимать хотя бы один аргумент.

Начнем с арифметических операторов.

4.1. Плюс Арифметический Оператор

Как мы видели ранее, мы можем перегрузить основные математические операторы в Kotlin. Мы можем использовать“+”, чтобы сложить дваPoints вместе:

operator fun Point.plus(other: Point): Point = Point(x + other.x, y + other.y)

Тогда мы можем написать:

>> val p1 = Point(1, 2)
>> val p2 = Point(2, 3)
>> println(p1 + p2)
Point(x=3, y=5)

Посколькуplus является бинарной операторной функцией, мы должны объявить параметр для функции.

Теперь большинство из нас испытали неэлегантность сложения двухBigIntegers:

BigInteger zero = BigInteger.ZERO;
BigInteger one = BigInteger.ONE;
one = one.add(zero);

As it turns out, есть лучший способ добавить дваBigIntegers в Kotlin:

>> val one = BigInteger.ONE
println(one + one)

Это работает, потому чтоKotlin standard library itself adds its fair share of extension operators on built-in types like BigInteger.

4.2. Другие арифметические операторы

Аналогичноplus,subtraction, multiplicationdivision, and the remainder are working the same way:

operator fun Point.minus(other: Point): Point = Point(x - other.x, y - other.y)
operator fun Point.times(other: Point): Point = Point(x * other.x, y * other.y)
operator fun Point.div(other: Point): Point = Point(x / other.x, y / other.y)
operator fun Point.rem(other: Point): Point = Point(x % other.x, y % other.y)

Затем компилятор Kotlin переводит любой вызов“-“,“*”,“/”, or “%” в“minus”,“times”,“div”, or “rem” соответственно:

>> val p1 = Point(2, 4)
>> val p2 = Point(1, 4)
>> println(p1 - p2)
>> println(p1 * p2)
>> println(p1 / p2)
Point(x=1, y=0)
Point(x=2, y=16)
Point(x=2, y=1)

Или, как насчет масштабированияPoint с помощью числового коэффициента:

operator fun Point.times(factor: Int): Point = Point(x * factor, y * factor)

Таким образом, мы можем написать что-то вроде“p1 * 2”:

>> val p1 = Point(1, 2)
>> println(p1 * 2)
Point(x=2, y=4)

Как мы можем заметить из предыдущего примера, не обязательно, чтобы два операнда были одного типа. The same is true for return types.с

4.3. Перестановочность

Перегруженные операторы не всегдаcommutative., то естьwe can’t swap the operands and expect things to work as smooth as possible.

Например, мы можем масштабироватьPoint на интегральный коэффициент, умножив его наInt, скажем,“p1 * 2”, но не наоборот.

Хорошей новостью является то, что мы можем определять операторные функции для встроенных типов Kotlin или Java. Чтобы заставить“2 * p1” работать, мы можем определить оператор дляInt:

operator fun Int.times(point: Point): Point = Point(point.x * this, point.y * this)

Теперь мы тоже можем использовать“2 * p1”:

>> val p1 = Point(1, 2)
>> println(2 * p1)
Point(x=2, y=4)

4.4. Составные Подборки

Теперь, когда мы можем добавить дваBigIntegers к“”_ operator, we may be able to use the compound assignment for _“”, который равен“+=”.. Давайте попробуем эту идею:

var one = BigInteger.ONE
one += one

По умолчанию, когда мы реализуем один из арифметических операторов, скажем“plus”, Kotlin не только поддерживает знакомый оператор _ «» _ *, он также делает то же самое для соответствующего _составного присваивания_, то есть «=». *

Это означает, что без дополнительной работы мы также можем выполнить:

var point = Point(0, 0)
point += Point(2, 2)
point -= Point(1, 1)
point *= Point(2, 2)
point /= Point(1, 1)
point /= Point(2, 2)
point *= 2

Но иногда это поведение по умолчанию - не то, что мы ищем. Предположим, мы собираемся использовать“+=”, чтобы добавить элемент вMutableCollection. 

Для этих сценариев мы можем указать это явно, реализовав операторную функцию с именемplusAssign:

operator fun  MutableCollection.plusAssign(element: T) {
    add(element)
}

For each arithmetic operator, there is a corresponding compound assignment operator which all have the “Assign” suffix. То есть естьplusAssign, minusAssign, timesAssign, divAssign, иremAssign:

>> val colors = mutableListOf("red", "blue")
>> colors += "green"
>> println(colors)
[red, blue, green]

Все функции оператора составного присваивания должны возвращатьUnit.

4.5. Конвенция о равных

If we override the equals method, then we can use the “==” and “!=” operators тоже:

class Money(val amount: BigDecimal, val currency: Currency) : Comparable {

    // omitted

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Money) return false

        if (amount != other.amount) return false
        if (currency != other.currency) return false

        return true
    }

    // An equals compatible hashcode implementation
}

Kotlin переводит любой вызов операторов“==” и“!=” в вызов функцииequals, очевидно, чтобы заставить“!=” работать, результат вызова функции инвертируется. Note that in this case, we don’t need the operator keyword.с

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

Пора снова нанести удар поBigInteger!

Предположим, мы будем запускать некоторую логику условно, если одинBigInteger больше другого. В Java решение не совсем чистое:

if (BigInteger.ONE.compareTo(BigInteger.ZERO) > 0 ) {
    // some logic
}

Используя тот же самыйBigInteger в Kotlin, мы можем волшебным образом написать это:

if (BigInteger.ONE > BigInteger.ZERO) {
    // the same logic
}

Эта магия возможна, потому чтоKotlin has a special treatment of Java’s Comparable.

Проще говоря, мы можем вызвать методcompareTo в интерфейсеComparable с помощью нескольких соглашений Kotlin. Фактически, любые сравнения, сделанные «<“, “⇐”, “>”, или“>=”, будут преобразованы в вызов функцииcompareTo».

Чтобы использовать операторы сравнения для типа Kotlin, нам необходимо реализовать его интерфейсComparable:

class Money(val amount: BigDecimal, val currency: Currency) : Comparable {

    override fun compareTo(other: Money): Int =
      convert(Currency.DOLLARS).compareTo(other.convert(Currency.DOLLARS))

    fun convert(currency: Currency): BigDecimal = // omitted
}

Тогда мы можем сравнить денежные значения так же просто, как:

val oneDollar = Money(BigDecimal.ONE, Currency.DOLLARS)
val tenDollars = Money(BigDecimal.TEN, Currency.DOLLARS)
if (oneDollar < tenDollars) {
    // omitted
}

Поскольку функцияcompareTo в интерфейсеComparable уже отмечена модификаторомoperator, нам не нужно добавлять ее самостоятельно.

4.7. В конвенции

Чтобы проверить, принадлежит ли элементPage, мы можем использовать соглашение“in”:

operator fun  Page.contains(element: T): Boolean = element in elements()

И сноваthe compiler would translate “in” and “!in” conventions to a function call to the contains operator function:

>> val page = firstPageOfSomething()
>> "This" in page
>> "That" !in page

Объект в левой части“in” будет передан в качестве аргументаcontains, а функцияcontains будет вызываться в правом операнде.

4.8. Получить индексатор

Indexers allow instances of a type to be indexed just like arrays or collections. Предположим, мы собираемся смоделировать разбитую на страницы коллекцию элементов какPage<T>, беззастенчиво копируя идею изSpring Data:

interface Page {
    fun pageNumber(): Int
    fun pageSize(): Int
    fun elements(): MutableList
}

Обычно, чтобы получить элемент изPage, we, сначала нужно вызвать функциюelements:

>> val page = firstPageOfSomething()
>> page.elements()[0]

ПосколькуPage сам по себе является просто причудливой оболочкой для другой коллекции, мы можем использовать операторы индексатора для улучшения его API:

operator fun  Page.get(index: Int): T = elements()[index]

Компилятор Kotlin заменяет любойpage[index] вPage на вызов функцииget(index):

>> val page = firstPageOfSomething()
>> page[0]

Мы можем пойти еще дальше, добавив столько аргументов, сколько захотим, к объявлению методаget.

Предположим, мы собираемся получить часть обернутой коллекции:

operator fun  Page.get(start: Int, endExclusive: Int):
  List = elements().subList(start, endExclusive)

Затем мы можем нарезатьPage следующим образом:

>> val page = firstPageOfSomething()
>> page[0, 3]

Такжеwe can use any parameter types for the get operator function, not just Int.

4.9. Установить индексатор

В дополнение к использованию индексаторов для реализацииget-like semantics,we can utilize them to mimic set-like operations тоже. Все, что нам нужно сделать, это определить операторную функцию с именемset как минимум с двумя аргументами:

operator fun  Page.set(index: Int, value: T) {
    elements()[index] = value
}

Когда мы объявляем функциюset всего с двумя аргументами, первый следует использовать внутри скобок, а другой - послеassignment:

val page: Page = firstPageOfSomething()
page[2] = "Something new"

Функцияset также может иметь более двух аргументов. Если это так, последний параметр является значением, а остальные аргументы должны быть переданы в скобках.

4.10. Соглашение об итераторах

Как насчет повторенияPage, как и других коллекций? Нам просто нужно объявить операторную функцию с именемiterator сIterator<T> в качестве возвращаемого типа:

operator fun  Page.iterator() = elements().iterator()

Затем мы можем перебратьPage:

val page = firstPageOfSomething()
for (e in page) {
    // Do something with each element
}

4.11. Конвенция о диапазоне

В Котлинеwe can create a range using the “..” operator. Например,“1..42” создает диапазон с числами от 1 до 42.

Иногда имеет смысл использовать оператор диапазона для других нечисловых типов. The Kotlin standard library provides a rangeTo convention on all Comparables:

operator fun > T.rangeTo(that: T): ClosedRange = ComparableRange(this, that)

Мы можем использовать это, чтобы получить несколько дней подряд в качестве диапазона:

val now = LocalDate.now()
val days = now..now.plusDays(42)

Как и другие операторы, компилятор Kotlin заменяет любой“..” вызовом функцииrangeTo .

5.  Используйте операторов разумно

Operator overloading is a powerful feature in Kotlin, который позволяет нам писать более краткие и иногда более читаемые коды. Однако с большой властью приходит большая ответственность.

Operator overloading can make our code confusing or even hard to read, когда он используется слишком часто или иногда неправильно.

Таким образом, прежде чем добавлять новый оператор к определенному типу, сначала спросите, подходит ли этот оператор семантически для того, чего мы пытаемся достичь. Или спросите, можем ли мы достичь того же эффекта с помощью обычных и менее магических абстракций.

6. Заключение

В этой статье мы узнали больше о механизме перегрузки операторов в Kotlin и о том, как он использует ряд соглашений для достижения этой цели.

Реализацию всех этих примеров и фрагментов кода можно найти вGitHub project - это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.