--- title: Инфиксные и операторные функции description: infix-нотация, перегрузка операторов, invoke, деструктуризация и делегирование свойств tags: - kotlin - functional --- Kotlin позволяет задать для своих типов тот же синтаксис, что у встроенных: `a + b`, `a[i]`, `a()`, `a in b`. Делается это обычными функциями с модификатором `operator` или `infix`. ## infix ```kotlin infix fun Int.упаковатьВ(unit: String): String = "$this $unit" data class Money(val amount: Long, val currency: String) infix fun Long.of(currency: String) = Money(this, currency) fun main() { println(5 упаковатьВ "кг") // 5 кг println(100L of "RUB") // Money(amount=100, currency=RUB) println(100L.of("RUB")) // Money(amount=100, currency=RUB) — обычный вызов тоже работает } ``` Требования к `infix`: ровно один параметр, не `vararg`, без значения по умолчанию, и функция должна быть методом или расширением. В stdlib `infix` используется для операций, читающихся как фраза: ```kotlin fun main() { println(setOf(1, 2) union setOf(2, 3)) // [1, 2, 3] println(setOf(1, 2) intersect setOf(2, 3)) // [2] println("a" to 1) // (a, 1) println(5 shl 2) // 20 println(5 and 3) // 1 println(1 until 5) // 1..4 println(5 downTo 1) // 5 downTo 1 step 1 println(1..10 step 2) // 1..9 step 2 } ``` Приоритет инфиксных функций ниже арифметики, но выше сравнений - это источник ошибок: ```kotlin infix fun Int.plusTen(x: Int) = this + x + 10 fun main() { println(1 plusTen 2 * 3) // 17 — это 1 plusTen (2*3) println((1 plusTen 2) * 3) // 39 } ``` При сомнении ставьте скобки. ## Перегрузка операторов Каждый оператор отображается в функцию с фиксированным именем: | Оператор | Функция | |---|---| | `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` / `-a` | `a.unaryPlus()` / `a.unaryMinus()` | | `!a` | `a.not()` | | `a++` / `a--` | `a.inc()` / `a.dec()` | | `a += b` | `a.plusAssign(b)` или `a = a.plus(b)` | | `a == b` | `a?.equals(b) ?: (b === null)` | | `a > b` | `a.compareTo(b) > 0` | | `a[i]` | `a.get(i)` | | `a[i] = v` | `a.set(i, v)` | | `a()` | `a.invoke()` | | `a in b` | `b.contains(a)` | | `a..b` | `a.rangeTo(b)` | | `a.. x 1 -> y else -> throw IndexOutOfBoundsException("индекс $index") } val length: Double get() = kotlin.math.sqrt(x * x + y * y) } fun main() { val a = Vec(1.0, 2.0) val b = Vec(3.0, 4.0) println(a + b) // Vec(x=4.0, y=6.0) println(b - a) // Vec(x=2.0, y=2.0) println(a * 2.0) // Vec(x=2.0, y=4.0) println(-a) // Vec(x=-1.0, y=-2.0) println(b[0]) // 3.0 println(b.length) // 5.0 } ``` Умножение на скаляр справа - метод класса; чтобы работало и слева, нужно расширение: ```kotlin data class Vec2(val x: Double, val y: Double) { operator fun times(scalar: Double) = Vec2(x * scalar, y * scalar) } operator fun Double.times(v: Vec2) = v * this fun main() { val v = Vec2(1.0, 2.0) println(v * 3.0) // Vec2(x=3.0, y=6.0) println(3.0 * v) // Vec2(x=3.0, y=6.0) } ``` ## Операторы как расширения Оператор можно добавить к чужому типу: ```kotlin operator fun String.times(n: Int) = repeat(n) operator fun List.get(range: IntRange): List = subList(range.first, range.last + 1) fun main() { println("ab" * 3) // ababab println(listOf(1, 2, 3, 4, 5)[1..3]) // [2, 3, 4] } ``` Так в stdlib добавлены `Map.get` через `[]`, `List.plus` и операции над множествами. ## invoke `operator fun invoke` позволяет вызывать объект как функцию: ```kotlin class Multiplier(private val factor: Int) { operator fun invoke(x: Int) = x * factor } fun main() { val triple = Multiplier(3) println(triple(5)) // 15 println(triple.invoke(5)) // 15 — то же самое } ``` Практическое применение - объект с состоянием, который ведёт себя как функция: ```kotlin class RateLimiter(private val maxCalls: Int) { private var calls = 0 operator fun invoke(): Boolean { calls++ return calls <= maxCalls } } fun main() { val limiter = RateLimiter(2) println("${limiter()} ${limiter()} ${limiter()}") // true true false } ``` `invoke` на `companion object` даёт фабрику с синтаксисом конструктора: ```kotlin class Email private constructor(val value: String) { companion object { operator fun invoke(raw: String): Email? = if (raw.contains("@")) Email(raw.trim().lowercase()) else null } override fun toString() = value } fun main() { println(Email(" Ivan@Example.COM ")) // ivan@example.com println(Email("не почта")) // null } ``` Это способ сделать «конструктор, возвращающий null» - настоящий конструктор так не может. ## Сравнение: compareTo ```kotlin class Version(val major: Int, val minor: Int) : Comparable { override fun compareTo(other: Version): Int = compareValuesBy(this, other, { it.major }, { it.minor }) override fun toString() = "$major.$minor" } fun main() { val a = Version(1, 5) val b = Version(2, 0) println(a < b) // true println(a >= Version(1, 5)) // true println(listOf(b, a, Version(1, 9)).sorted()) // [1.5, 1.9, 2.0] println(maxOf(a, b)) // 2.0 println(a in Version(1, 0)..Version(1, 9)) // true } ``` Одна функция `compareTo` даёт `<`, `>`, `<=`, `>=`, сортировку, `maxOf`/`minOf` и диапазоны. `compareValuesBy` сравнивает по нескольким ключам по порядку. ## in: contains ```kotlin data class DateRange(val from: Int, val to: Int) { operator fun contains(day: Int) = day in from..to } fun main() { val week = DateRange(1, 7) println(3 in week) // true println(10 in week) // false println(10 !in week) // true — !in получается автоматически } ``` ## Итератор ```kotlin class Countdown(private val from: Int) { operator fun iterator(): Iterator = object : Iterator { private var current = from override fun hasNext() = current > 0 override fun next() = current-- } } fun main() { for (i in Countdown(3)) print("$i ") println() // 3 2 1 } ``` `iterator` можно добавить и расширением - тогда `for` заработает на чужом типе: ```kotlin operator fun ClosedRange.iterator(): Iterator = object : Iterator { private var current = start override fun hasNext() = current <= endInclusive override fun next() = current.also { current = current.plusDays(1) } } fun main() { val from = java.time.LocalDate.of(2026, 1, 1) val to = java.time.LocalDate.of(2026, 1, 3) for (d in from..to) print("${d.dayOfMonth} ") println() // 1 2 3 } ``` ## get и set ```kotlin class Matrix(private val rows: Int, private val cols: Int) { private val data = DoubleArray(rows * cols) operator fun get(i: Int, j: Int): Double = data[i * cols + j] operator fun set(i: Int, j: Int, value: Double) { data[i * cols + j] = value } override fun toString() = (0 until rows).joinToString("\n") { i -> (0 until cols).joinToString(" ") { j -> this[i, j].toString() } } } fun main() { val m = Matrix(2, 2) m[0, 0] = 1.0 m[1, 1] = 2.0 println(m[0, 0]) // 1.0 println(m) // 1.0 0.0 // 0.0 2.0 } ``` `get` и `set` принимают любое число аргументов - это даёт многомерную индексацию, которой нет в Java. ## plusAssign против plus ```kotlin class Basket { private val items = mutableListOf() operator fun plusAssign(item: String) { items.add(item) } override fun toString() = items.toString() } fun main() { val basket = Basket() basket += "хлеб" basket += "молоко" println(basket) // [хлеб, молоко] } ``` Если объявлены оба (`plus` и `plusAssign`) и переменная `var` - компилятор сообщит об неоднозначности. Правило: `plusAssign` для изменяемых коллекций-подобных типов, `plus` для значений. ## Деструктуризация ```kotlin class Pair3(val first: Int, val second: String) { operator fun component1() = first operator fun component2() = second } fun main() { val (num, text) = Pair3(1, "один") println("$num = $text") // 1 = один } ``` `data class` объявляет `componentN` автоматически. Подробнее: [Деструктуризация](../Синтаксис%20и%20типы/Работа%20с%20данными/Деструктуризация.md) ## Делегирование свойств Операторы `getValue`/`setValue` позволяют подменить реализацию свойства: ```kotlin import kotlin.reflect.KProperty class Trimmed { private var value: String = "" operator fun getValue(thisRef: Any?, property: KProperty<*>): String = value operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) { value = newValue.trim() } } class Form { var login: String by Trimmed() } fun main() { val f = Form() f.login = " ivan " println("[${f.login}]") // [ivan] } ``` Подробнее о делегатах и готовых реализациях (`lazy`, `observable`, `map`): [Делегирование](../ООП/Делегирование.md) ## Когда перегружать оператор | Ситуация | Стоит ли | |---|---| | математический тип (вектор, матрица, деньги, дата) | да | | коллекция-подобный тип (`[]`, `in`, `for`) | да | | сравнение по естественному порядку | да, через `Comparable` | | «красивее выглядит» | нет | | смысл оператора неочевиден (`user + role`) | нет | | операция может бросить исключение неожиданно | нет | Критерий простой: оператор должен означать то же, что для встроенных типов. `+` обязан быть коммутативным по смыслу и не иметь побочных эффектов; `==` - соответствовать `hashCode`. > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | перегрузки операторов нет | `operator fun` | > | `a.add(b)` | `a + b` | > | `list.get(i)` | `list[i]` | > | `map.put(k, v)` | `map[k] = v` | > | `a.compareTo(b) < 0` | `a < b` | > | `a.equals(b)` | `a == b` | > | `BigDecimal.add().multiply()` | `a + b * c` | > | инфиксной нотации нет | `infix fun` | > > **Java не поддерживает перегрузку операторов принципиально** - кроме `+` для строк, который встроен в компилятор. Это сознательное решение дизайнеров Java: операторы с произвольным смыслом усложняют чтение. Kotlin выбрал компромисс: перегрузка есть, но набор операторов фиксирован и имена функций заданы жёстко, так что `plus` всегда означает `+` и нельзя придумать свой оператор. > > **Разница заметнее всего на числовых типах.** `BigDecimal` в Java: > ```java > BigDecimal result = a.multiply(b).add(c).divide(d, RoundingMode.HALF_UP); > ``` > В Kotlin те же операторы уже объявлены в stdlib как расширения: > ```kotlin > import java.math.BigDecimal > > fun main() { > val a = BigDecimal("10.5") > val b = BigDecimal("2") > println(a + b) // 12.5 > println(a * b) // 21.0 > println(a > b) // true > } > ``` > Для денежных расчётов это главный практический выигрыш: формула в коде выглядит как формула. > > **`==` в Kotlin - это `equals`, а не сравнение ссылок.** Самое важное отличие на практике: в Java `a == b` для объектов сравнивает ссылки, и забытый `.equals()` - классическая ошибка со строками. В Kotlin `==` вызывает `equals` (с корректной обработкой null), а для ссылок есть отдельный `===`: > ```kotlin > fun main() { > val a = StringBuilder("x").toString() > val b = StringBuilder("x").toString() > println(a == b) // true — по значению > println(a === b) // false — разные объекты > } > ``` > > **`infix` даёт читаемость без DSL-библиотек.** В Java тестовые фреймворки имитируют инфиксную запись цепочкой методов: `assertThat(x).isEqualTo(y)`. В Kotlin это записывается как `x shouldBe y` - подробнее: [Kotest](../Тестирование/Kotest.md) > > **`invoke` аналога не имеет.** В Java объект нельзя вызвать как функцию; ближайшее - интерфейс с методом `apply`, вызываемый явно. > > Сравнить: [Операторы и выражения в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Операторы%20и%20выражения.md), [Класс Object](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Класс%20Object.md) > [!warning] Подводные камни > **Приоритет `infix` ниже арифметики:** > ```kotlin > infix fun Int.append(x: Int) = "$this$x" > > fun main() { > println(1 append 2 + 3) // 15 — это 1 append (2+3) > println((1 append 2) + 3) // 123 > } > ``` > Выражение выглядит как последовательность слева направо, но арифметика вычисляется первой. > > **`infix` нельзя использовать без получателя и с `this` неявно:** > ```kotlin > class Calc { > infix fun add(x: Int) = x + 1 > fun demo(): Int { > // return add 5 // ошибка > return this add 5 // так можно > } > } > > fun main() { println(Calc().demo()) } // 6 > ``` > > **`plus` не меняет объект, `plusAssign` меняет - и перепутать легко:** > ```kotlin > fun main() { > var list1 = listOf(1, 2) > list1 += 3 // новый список: plus + присваивание > println(list1) // [1, 2, 3] > > val list2 = mutableListOf(1, 2) > list2 += 3 // изменение на месте: plusAssign > println(list2) // [1, 2, 3] > } > ``` > Результат одинаковый, стоимость разная: первый вариант копирует весь список. > > **Объявление и `plus`, и `plusAssign` даёт ошибку компиляции при `var`:** > ```kotlin > class Both(val v: Int) { > operator fun plus(x: Int) = Both(v + x) > operator fun plusAssign(x: Int) { } > } > > fun main() { > val b = Both(1) > b += 1 // выбран plusAssign — для val это единственный вариант > println(b.v) // 1 > } > ``` > Для `var`-переменной компилятор не сможет выбрать между `b = b + 1` и `b.plusAssign(1)`. > > **`inc`/`dec` должны возвращать новый объект:** > ```kotlin > class Counter4(val value: Int) { > operator fun inc() = Counter4(value + 1) > } > > fun main() { > var c = Counter4(0) > c++ > println(c.value) // 1 > } > ``` > Если `inc` изменит объект и вернёт `this`, постфиксный `c++` сломается: он должен вернуть **старое** значение, а вернёт изменённое. > > **`equals` нельзя объявить расширением:** > ```kotlin > class NoEquals(val v: Int) > > // operator fun NoEquals.equals(other: Any?) = ... // не заработает > > fun main() { > println(NoEquals(1) == NoEquals(1)) // false — сравнение ссылок из Any > } > ``` > `equals` должен быть `override` в классе: он объявлен в `Any`, а расширение не переопределяет член. Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md) > > **`compareTo` обязан быть согласован с `equals`.** Если `a.compareTo(b) == 0`, но `a != b`, то `TreeSet` и `sortedSetOf` начнут терять элементы - они используют сравнение, а не равенство. > > **`get` без `set` создаёт асимметричный API:** > ```kotlin > class ReadOnlyBox(private val items: List) { > operator fun get(i: Int) = items[i] > } > > fun main() { > val b = ReadOnlyBox(listOf(1, 2)) > println(b[0]) // 1 > // b[0] = 5 // ошибка: нет set > } > ``` > Это нормально (так работает `List`), но пользователь увидит `[]` и может ожидать присваивания. > > **Оператор на расширении не виден, если есть член класса** - действует то же правило затенения, что у обычных расширений. > > **Операторы с неочевидным смыслом ухудшают код:** > ```kotlin > data class User13(val name: String, val roles: List) > > // operator fun User13.plus(role: String) = copy(roles = roles + role) > fun User13.withRole(role: String) = copy(roles = roles + role) > > fun main() { > println(User13("Иван", emptyList()).withRole("админ")) > // User13(name=Иван, roles=[админ]) > } > ``` > `user + "админ"` короче, но непонятно: что значит «сложить пользователя со строкой»? Именованный метод честнее. > > **`invoke` на обычном классе запутывает:** `service()` не говорит, что делает объект. Оправдано для функциональных обёрток (`UseCase`, `Validator`), где у объекта ровно одна операция. > > **Нельзя придумать новый оператор.** Набор фиксирован: `+`, `-`, `*`, `/`, `%`, `[]`, `()`, `in`, `..`, сравнения, `++`/`--`, унарные. Произвольные символы вроде `<*>` или `|>` недоступны - только `infix`-функция со словесным именем. Далее: [Receivers и context parameters](Receivers%20и%20context%20parameters.md)