Перегрузка операторов
В языках программирования есть множество различных операторов. Например, есть операторы для математических операций (+
, -
и др.), сравнения (==
, >
и др.), извлечения элемента по индексу с помощью квадратный скобок.
В ряде случаев эти операторы используются для разных типов данных. Так в случае оператора +
операндами могут быть как числа, так и строки (3 + 4
, "hello " + "world"
). Также оператор индекса используется как для строки, так массива и списка.
В Kotlin операторы – это своего рода сокращенный вариант записи методов своих классов. Причем на уровне языка имеется соглашение, как этот метод должен называться. Так, если класс имеет метод plus
, то для его вызова можно использовать знак +
.
fun main() { val a = 10 println(a.plus(12)) println(a + 12) val b = "Hello" println(b.plus(" world")) println(b + " world") }
Результат выполнения программы:
22 22 Hello world Hello world
В данном примере видим, что вызов метода plus
и результат операции сложения дают один и тот же результат для одинаковых исходных данных и разный для разных типов данных. Причем разный в первую очередь по смыслу. Сложение строк – это вовсе не сложение, а банальное присоединение второй к первой. Все это значит, что +
– это то же самое, что plus
, а то, что делает plus
, то есть код тела этого метода, зависит от класса.
Наличие в разных классах методов с одинаковыми именами, но разным кодом, – проявление так называемого полиморфизма – еще одной ключевой концепции объектно-ориентированного программирования, наряду с наследованием и инкапсуляцией. Внешне полиморфизм выглядит так, как будто одна и та же функция способна работать с разными типами данных.
Соответственно, и для своего класса мы можем определить метод plus
со своим телом.
class NumInc(var number: Int, var step: Int) { fun inc() {number += step} fun dec() {number -= step} operator fun plus(numInc: NumInc): NumInc { val n = number + numInc.number val s = step + numInc.step return NumInc(n, s) } }
fun main() { val a = NumInc(10, 2) val b = NumInc(6, 4) val c = a + b val d = a.plus(b) val e = b.plus(NumInc(20, -5)) val f = NumInc(1,2) + b println("${c.number}, ${c.step}") // 16, 6 }
В функции main
показаны разные способы вызова метода plus
: через оператор +
, обращением по имени метода, когда в функцию объект передается через переменную и напрямую.
Рассмотрим метод plus
нашего класса:
... operator fun plus(numInc: NumInc): NumInc { val n = number + numInc.number val s = step + numInc.step return NumInc(n, s) } ...
В каком-то смысле мы переопределяем оператор +
по аналогии с тем, как переопределяли родительские функции-члены в дочерних классах. Только здесь вместо ключевого слова override
используется operator
.
На самом деле это не переопределение, потому что в родительском классе (в данном случае Any
) такого метода нет. Мы просто следуем соглашению, что если у класса есть методы, определение которых начинается со слова operator
, то они переопределяют какие-то существующие в языке операции. Поэтому обычно используют термин "перегрузка операторов", а не "переопределение операторов".
Метод-оператор plus
языка Kotlin должен обязательно иметь один параметр. При этом не обязательно, чтобы он был того же класса (в нашем случае он того же). Не обязателен и возврат из функции значения. Вот пример другой реализации plus
:
operator fun plus(n: Int) { number += n }
Такой метод будет вызываться, когда к объекту класса NumInс будут прибавлять обычное целое число. Компилятор языка присвоит это число параметру n.
В классе могут быть два метода с одинаковыми именами. (Это касается не только методов, перегружающих операторы языка.) В данном контексте класс может выглядеть так:
class NumInc(var number: Int, var step: Int) { fun inc() {number += step} fun dec() {number -= step} operator fun plus(numInc: NumInc): NumInc { val n = number + numInc.number val s = step + numInc.step return NumInc(n, s) } operator fun plus(n: Int) { number += n } }
Какая именно функция plus()
будет вызвана, зависит от передаваемого аргумента. Если это объект NumInc, будет вызываться первая, если целое число – вторая.
fun main() { val a = NumInc(10, 2) val b = NumInc(6, 4) val c = a + b a + 3 println("${c.number}, ${c.step}") // 16, 6 println("${a.number}, ${a.step}") // 13, 2 }
Обратите внимание, вторая функция plus
не создает нового объекта, она меняет имеющийся.
В программировании подобное явление, когда функция с одним именем имеет несколько реализаций в одном классе, называется перегрузкой функций. Это явление также можно отнести к концепции полиморфизма. Еще раз отметим, что перегружать можно любую функцию, а не только функцию-оператор. Также подобное мы уже видели у конструкторов. У класса может быть несколько конструкторов, но вызывается только один, в зависимости от того, какие переданы аргументы.
Если родительский класс имеет метод, перегружающий оператор, то в дочернем этот метод уже не перегружается, а переопределяется (если возникает такая необходимость), то есть его надо объявлять через слово override
.
open class NumInc(var number: Int, var step: Int) { fun inc() {number += step} fun dec() {number -= step} open operator fun plus(n: Int) { number += n } }
class NumDouble (n: Int, s: Int): NumInc(n, s) { override fun plus(n: Int) { number += 2 * n } }
fun main() { val a = NumDouble(3, 1) a + 5 println("${a.number}") // 13 }
Подобное происходит и с методом equals
, который перегружает оператор проверки на равенство ==
. Он уже определен в классе Any
, поэтому, если мы хотим переопределить его, делать это надо через override
.
class NumInc(var number: Int, var step: Int) { fun inc() {number += step} fun dec() {number -= step} operator fun plus(n: Int) { number += n } override fun equals(other: Any?): Boolean { if (other is NumInc) { return number == other.number } else return false } }
fun main() { val a = NumInc(3, 1) val b = NumInc(3, 2) val c: Any = 3 println(a == b) // true println(a == c) // false println(a == NumInc(4,1)) // false }
Рассмотрим перегрузку извлечения элемента по индексу и присвоение значения по индексу с помощью квадратных скобок, то есть перегрузку оператора индекса. Пусть к полям нашего класса можно будет обращаться не только по их именам, но и по индексам. Полю number будет соответствовать индекс 0, а step – индекс 1.
Имя функции-оператора для извлечения по индексу – get
. Она должна иметь один параметр – в нашем контексте индекс элемента. У функции присвоения по индексу – set
– должно быть как минимум два параметра. Первый – в данном случае индекс элемента, второй – присваиваемое значение.
class NumInc(var number: Int, var step: Int) { fun inc() {number += step} fun dec() {number -= step} operator fun get(i: Int): Int? { when (i) { 0 -> return number 1 -> return step else -> return null } } operator fun set(i: Int, v: Int) { when (i) { 0 -> number = v 1 -> step = v } } }
fun main() { val a = NumInc(3, 1) println("${a.number}, ${a.step}") // 3, 1 println("${a[0]}, ${a[1]}") // 3, 1 a[0] = 10 a[1] = 2 println("${a[0]}, ${a[1]}") // 10, 2 }
Обратите внимание, что перегружающие оператор индекса методы get
и set
не имеют отношения к геттерам и сеттерам полей свойств.
Перечень всех операторов, которые можно перегрузить в Kotlin, и имена соответствующих им методов можно посмотреть в официальной документации языка: https://kotlinlang.org/docs/operator-overloading.html
Практическая работа:
В нашем классе NumInc функции inc()
и dec()
не переопределяют никаких операторов. Это обычные функции-члены класса. Однако в Kotlin есть операции инкремента (++
) и декремента (--
). По отношению к целым числам инкремент увеличивает значение переменной на единицу, декремент – уменьшает. Например, если i равна пяти, то выражение i++
сделает i равной шести.
Операциям инкремента и декремента в Kotlin соответствуют функции с именами inc()
и dec()
. Измените функции-члены inc()
и dec()
нашего класса так, чтобы они перестали быть "обычными", а перегружали соответствующие им операторы. При этом инкремент экземпляра класса NumInc должен также выполнять увеличение number на step, декремент – уменьшение на step.
Могут ли в классе быть одноименные методы, один из которых перегружает оператор, а другой – нет?
PDF-версия курса с ответами к практическим работам