Статьи Kotlin. Перегрузка операторов
Post
Cancel

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

Когда вы в своём коде используете какой-либо оператор, за кулисами вызывается соответствующая ему функция. При этом каждому оператору соответствует функция со строго определённым именем. Например, оператору + соответствует функция plus.

Kotlin позволяет перегружать операторы, тем самым становится возможным их использование по своему усмотрению.

Набор операторов, которые могут быть перегружены, ограничен. Каждому такому оператору соответствует имя функции, которую нужно определить в своём классе. Функции, которые перегружают операторы должны быть отмечены модификатором operator.


Перегрузка арифметических операторов

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

ВыражениеИмя функции
a * btimes
a / bdiv
a % bmod - в Kotlin 1.0
a % brem - с Kotlin 1.1
a + bplus
a - bminus

Допустим у нас есть класс Tree.

1
data class Tree(val name: String, val age: Int)

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

1
2
3
4
5
6
7
8
9
10
11
12
data class Tree(val name: String, val age: Int) {
    operator fun plus(other: Tree): Int {
        return age + other.age
    }
}

...

val pine = Tree("Сосна", 2)
val apple = Tree("Яблоня", 4)

println(pine + apple)  // 6

В классе данных мы объявили функцию plus и отметили её модификатором operator, который сообщает о вашем намерении использовать эту функцию совместно с оператором +. Теперь объекты можно складывать с помощью оператора +, а за кулисами на место оператора компилятор будет подставлять вызов функции plus.

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

Составные операторы присваивания

ВыражениеИмя функции
a += bplusAssign
a -= bminusAssign
a *= btimesAssign
a /= bdivAssign
a %= bmodAssign

Не стоит одновременно перегружать составной оператор присваивания и соответствующий ему бинарный оператор (например, plusAssign и plus), так как компилятор сообщит об ошибке. Ошибку можно исправить заменив оператор на вызов функции. Либо заменив var на val, тогда операция plusAssign станет недоступной (неизменяемая переменная).

Но лучше при проектировании класса следовать следующему принципу: если класс неизменяемый, то добавлять в него только операции, возвращающие новые значения (бинарные); если класс изменяемый, то добавлять в него только модифицирующие операции (составные операторы присваивания).

Также в таких функциях возвращаемым значением должен быть Unit, иначе будет фиксироваться ошибка.

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

ВыражениеИмя функции
+aunaryPlus
-aunaryMinus
!anot

Эти функции не принимают никаких аргументов, а принцип объявления аналогичен предыдущим.

1
2
3
4
5
6
7
8
9
10
data class Tree(val name: String, val age: Int) {
    operator fun unaryMinus(): Int {
        return -age
    }
}

...

val pine = Tree("Pine", 2)
println(-pine)   // -2

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

ВвыражениеИмя функции
a++, ++ainc
a–, –adec

Постфиксная операция a++ сначала вернёт текущее значение, затем увеличит его. Префиксная операция ++a сначала увеличит текущее значение, затем вернёт его.

1
2
3
4
5
6
7
8
9
data class Tree(val name: String, var age: Int) {
    operator fun inc() = Tree(name, ++age)

}

...

var pine = Tree("Pine", 2)
println(++pine)  // 3

Возвращаемым значением должен быть класс, в котором объявлена функция. Если эта функция является функцией-расширением, применимой для приёмника типа T, то возвращаемым значением должен быть подтип T.


Перегрузка операторов сравнения

Равенство и неравенство (equals)

ВыражениеИмя функции
a == bequals
a != bequals

Оба оператора используют функцию equals, но с одним различием: для оператора != результат инвертируется. Делается это автоматически, поэтому дополнительно ничего делать не требуется.

В отличие от остальных операторов, операторы == и != можно использовать со значениями равными null. Сравнение a == b сначала проверит операнды на равенство null. Если результат получился отрицательным, то вызовется функция equals.

Для классов, отмеченных модификатором data (классы данных), equals генерируется автоматически. Но его можно определить самостоятельно, в том и числе и в классе данных.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data class Tree(val name: String, var age: Int) {
    override fun equals(other: Any?): Boolean {
        if (other === this) return true
        if (other !is Tree) return false
        return other.name == name && other.age == age
    }

    ...

    val pine = Tree("Pine", 2)
    val apple = Tree("Apple", 4)

    println(pine == apple)   // false
}

Функция equals не отмечается модификатором operator. Вместо этого она переопределяется (override), потому что реализация этого метода есть в классе Any и базовый метод уже отмечен модификатором operator.

Также стоит отметить, что оператор строгого равенства, или идентичности (===, !==), не может быть перегружен.

Сравнение (compareTo)

ВыражениеИмя функции
a > bcompareTo
a < bcompareTo
a >= bcompareTo
a <= bcompareTo

Классы в Kotlin могут использовать интерфейс Comparable, если нужна возможность сравнения их экземпляров: функция compareTo определяет, какой из двух объектов больше. Данная функция может вызываться не только напрямую, но и с использованием операторов сравнения, указанных в таблице выше.

Все операторы транслируются в вызов функции compareTo, которая должна возвращать значение типа Int.

1
2
3
4
5
6
7
8
9
10
11
12
13
data class Tree(val name: String, var age: Int) : Comparable<Tree> {
    override fun compareTo(other: Tree): Int {
        return compareValuesBy(this, other, Tree::name, Tree::age)
    }

}

...

val pine = Tree("Pine", 2)
    val apple = Tree("Apple", 4)

    println(pine > apple)  // true

Функция compareTo не отмечается модификатором operator, так как он уже применён к функции базового интерфейса. Также здесь используется функция из стандартной библиотеки Kotlin compareValuesBy, которая принимает список функций обратного вызова, результаты которых подлежат сравнению. В данном примере функции передаются ссылки на свойства, но ещё можно передавать лямбда–выражения.


Перегрузка операторов для коллекций и диапазонов

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

ВыражениеИмя функции
a[i]get
a[i] = bset

В Kotlin к элементам коллекций можно обращаться с помощью квадратных скобок. Такой же синтаксис можно добавить для использования и в собственный класс.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
data class Tree(var name: String, var age: Int) {
    operator fun get(index: Int): Any {
        return when(index) {
            0 -> name
            1 -> age
            else -> throw IndexOutOfBoundsException("Invalid coordinate $index")
        }
    }

    operator fun set(index: Int, value: Int) {
        when(index) {
            1 -> age = value
            else -> throw IndexOutOfBoundsException("Invalid coordinate $index")
        }
    }

    operator fun set(index: Int, value: String) {
        when(index) {
            0 -> name = value
            else -> throw IndexOutOfBoundsException("Invalid coordinate $index")
        }
    }

}

...

val pine = Tree("Сосна", 2)
println(pine[0])  // Сосна

pine[0] = "Ель"
println(pine[0])  // Ель

Оператор in

ВыражениеИмя функции
a in bcontains
a !in bcontains

Оба оператора in и !in используются для проверки того, входит ли объект в коллекцию. Они транслируются в функцию contains, но результат для оператора !in инвертируется.

1
2
3
4
5
6
7
8
9
10
data class Tree(var name: String, var age: Int) {
    operator fun contains(char: Char): Boolean {
        return char in name
    }
}

...

val pine = Tree("Сосна", 2)
println('с' in pine.name)  // true

Мультидекларации

Мультидекларации - это особенность, которая позволяет распаковать объект и использовать его значения для инициализации сразу нескольких переменных.

1
2
3
val pine = Tree("Сосна", 2)
val(name, age) = pine
println(name)                  // Сосна

Достигнуть такого результата можно объявив в своём классе функции componentN(), где N - номер позиции переменной в конструкторе.

1
2
3
4
class Tree(var name: String, var age: Int) {
    operator fun component1() = name
    operator fun component2() = age
}

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


Полезные ссылки

Operator overloading - официальная документация.
Перегрузка операторов - перевод на русский язык.

This post is licensed under CC BY 4.0 by the author.